From 64193c8d7b8ed3a69ad974c5f4a8fde387f5d7cf Mon Sep 17 00:00:00 2001 From: "matthias@quintern.xyz" Date: Thu, 20 Mar 2025 19:01:20 +0100 Subject: [PATCH] initial commit --- .gitignore | 4 + readme.md | 67 ++++++++++++++++ src/Makefile | 56 +++++++++++++ src/main.cpp | 223 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 350 insertions(+) create mode 100644 .gitignore create mode 100644 readme.md create mode 100755 src/Makefile create mode 100644 src/main.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c5338df --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +build +udev-filter +*.vimspector.json +*.clangd diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..dbbb15e --- /dev/null +++ b/readme.md @@ -0,0 +1,67 @@ +# udev-filter +Super fast and efficient way of only getting udev events you desire. +It provides an efficient way to perform automatic tasks when any device related event occurs, +entirely in userspace and *without adding any udev rules*! + +**udev-filter** listens to events via `udevadm monitor -p`. +It matches the udev events to event properties that are passed as command arguments. +When all properties of an event are registered, the program ouputs the name of the event, as defined by the user. + +## Examples +```shell +udev-filter --event=USB_REMOVED ACTION=remove +USB_REMOVED +USB_REMOVED +... +``` +prints `USB_REMOVED` any time a USB device is removed. +The name after --event can be freely chosen. +Another, more useful example might be: + +```shell +udev-filter --event=YUBIKEY_ADDED ACTION=add ID_USB_VENDOR=Yubico +``` +prints `YUBIKEY_ADDED` every time a device with vendor id "Yubico" is added. + +You can, of course, also listen to many events add once. To combine the previous two: +```shell +udev-filter \ + --event=USB_REMOVED ACTION=remove SUBSYSTEM=USB \ + --event=YUBIKEY_ADDED ACTION=add ID_USB_VENDOR=Yubico" +``` + +### Performing actions on events +To automatically open yubico-authenticator every time a yubikey is connected, create +this shell script and run it in the background: +```shell +# stdbuf is necessary to force buffer flushing on newlines +stdbuf -oL ./udev-filter --command-add="-s usb" \ + --event=YUBIKEY_ADDED ACTION=add ID_USB_VENDOR=Yubico | \ +while read EVENT; do + # in this example the only possible event is YUBIKEY_ADDED, so the switch-case is not necessary, + # but it is useful for further expansion + case $EVENT in + YUBIKEY_ADDED) + notify-send "Yubikey Added!" + { pgrep yubico || yubico-authenticator & } + ;; + *) + notify-send "Unhandled event: $EVENT" + ;; + esac +done +``` +Because **udev-filter** relies on events instead of polling, this script will consume near zero resources. +It will only do something whenever a udev event occurs. + + +To find out which properties you need to query, run `udevadm monitor -p`. +To limit the events to a certain subsystem, add `-s ` to udevadm, and later `--command-add="-s "` to `udev-filter`. + +## Installation +gcc, with glibc++ for C++23 and GNU make are required, which will be available in most modern Linux distributions already. +``` +cd src +make release +``` +will create `../udev-filter`. Copy it anywhere you want, for example `~/.local/bin` diff --git a/src/Makefile b/src/Makefile new file mode 100755 index 0000000..fc18336 --- /dev/null +++ b/src/Makefile @@ -0,0 +1,56 @@ +CXX = /usr/bin/g++ +CXXFLAGS = -std=c++23 -MMD -MP -Wall -Wpedantic -Wextra +LDFLAGS = + +OBJECT_DIR = ../build +EXEC = ../udev-filter + +SRC = $(wildcard *.cpp) $(wildcard */*.cpp) +OBJECTS = $($(notdir SRC):%.cpp=$(OBJECT_DIR)/%.o) +OBJECT_DIRS = $(OBJECT_DIR) $(foreach dir,$(SRCDIRS), $(OBJECT_DIR)/$(dir)) +DEPENDS = ${OBJECTS:.o=.d} + +CXXFLAGS += $(IFLAGS) + + +default: $(EXEC) + echo $(OBJECTS) + +.PHONY: release +release: CXXFLAGS += -O3 +release : default + +# rule for the executable +$(EXEC): $(OBJECT_DIRS) $(OBJECTS) + $(CXX) $(OBJECTS) -o $@ $(CXXFLAGS) $(LDFLAGS) $(LDLIBS) +# include the makefiles generated by the -M flag +-include $(DEPENDS) + +# rule for all ../build/*.o files +$(OBJECT_DIR)/%.o: $(shell echo $<) %.cpp + $(CXX) -c $< -o $@ $(CXXFLAGS) $(LDFLAGS) + + +$(OBJECT_DIRS): + mkdir -p $@ + +# +# Extra Options +# +# with debug flags +.PHONY += install debug run clean clean_all docs + +debug: CXXFLAGS += -g # -DDEBUG +debug: default + +# make with debug flags and run afterwards +run: CXXFLAGS += -g +run: default + $(CXX) $(OBJECTS) -o $(EXEC) $(CXXFLAGS) $(LDFLAGS) $(LDLIBS) + ./$(EXEC) + +# remove all object and dependecy files +clean: + -rm -r $(OBJECT_DIR) + -rm $(EXEC) + diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..294df61 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,223 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * @brief Filter udev events + * @details + * Watches udevadm monitor -p and prints the name of a defined event when it occurs. + * You can define an event with a name and a list of properties. + * All of the properties must occur in the udev event for the defined event to occur. + * You can find out which properties you need by running `udevadm monitor -p`. + * Each property is full match of a line in one event block, for example `DRIVER=usb`, `ACTION=add` or `ID_USB_VENDOR=Company_Inc.` + */ + + +template +concept FormatableRange = std::ranges::forward_range && std::formattable, CharT>; + +/** + * @brief Format ranges + * @details + * Format string accepts c = {curly brackets}, b = [box brackets], a = , r = (round brackets) + */ +template requires FormatableRange +struct std::formatter { + using T = std::ranges::range_value_t; + constexpr auto parse(std::format_parse_context& ctx){ + auto pos = ctx.begin(); + while (pos != ctx.end() && *pos != '}') { + if (*pos == 'a') { useChar = 0; } // ngle + else if (*pos == 'b') { useChar = 1; } // [b]ox + else if (*pos == 'c') { useChar = 2; } // {c}urly + else if (*pos == 'p') { useChar = 3; } // (r)ound + else { + throw std::format_error("Invalid format args for forward range."); + } + ++pos; + } + return pos; // expect `}` at this position, otherwise its an error + } + auto format(const U& v, auto& ctx) const { + auto out = ctx.out(); + char c = openChars.at(useChar); + out = std::format_to(out, "{}", c); + + auto it = v.begin(); + if (it != v.end()) { + out = std::format_to(out, "{}", *it); + } + it++; + for (; it != v.end(); it++) { + out = std::format_to(out, ", {}", *it); + } + c = closeChars.at(useChar); + return std::format_to(out, "{}", c); + // return out; + } + public: + size_t useChar = 2; + static constexpr std::string_view openChars = "<[{()"; + static constexpr std::string_view closeChars{">]})"}; +}; + + +const char* help = + "udev-filter [--help] [--debug] [--command-add=FLAGS] {--event=EVENT PROPERTIES...}\n\n" + "Example:\n" + "udev-filter --command-add=\"-s usb\" --event=USB_REMOVED ACTION=remove\n" + "udev-filter --command-add=\"-s usb\" --event=YUBIKEY_ADDED ACTION=add ID_USB_VENDOR=Yubico" +; + +/** + * @brief Wrap the output of a command in a streambuffer + */ +class procbuf : public std::streambuf { +public: + procbuf(const std::string& cmd) { + file = popen(cmd.c_str(), "r"); + if (!file) { + throw std::runtime_error("Failed to run"); + } + setg(&ch, &ch+1, &ch+1); // point to end of area + } + ~procbuf() { + pclose(file); + } + +private: + int_type underflow() override { + if (feof(file) || ferror(file)) { + return traits_type::eof(); + } + + int c = fgetc(file); + if (c == EOF) { + return traits_type::eof(); + } + // now you could apply transforms on the char + /*if (static_cast(c) == '=') {*/ + /* c = static_cast('B');*/ + /*}*/ + ch = static_cast(c); + setg(&ch, &ch, &ch+1); // point to area + return traits_type::to_int_type(static_cast(ch)); + } +private: + FILE* file; + char ch; +}; + + +int main(int argc, const char** argv) { + bool debug=false; + // parse args + std::unordered_map> properties; + // name and how many properties need to match + std::vector> events; + + std::string command("udevadm monitor -p"); + for (int i = 1; i < argc; ++i) { + std::string_view arg(argv[i]); + if (arg.starts_with("--command-add=")) { + int cmdbeg = arg.find('=')+1; + std::string_view cmd(arg.begin()+cmdbeg, arg.end()); + command += " "; + command += cmd; + } + else if (arg.starts_with("--event=")) { + int eventbeg = arg.find('=')+1; + std::string_view event(arg.begin()+eventbeg, arg.end()); + if (event.size() == 0) { + throw std::invalid_argument("An event must be at least one character long"); + } + events.push_back({event, 0}); + } + else if (arg == "--help") { + std::println("{}", help); + return 0; + } + else if (arg == "--debug") { + debug=true; + } + else { + // property, must come after a --event= + if (events.empty()) { + throw std::invalid_argument(std::format("Properties must come after a '--event=', but '{}' does not.", arg)); + } + unsigned eventIndex = events.size() - 1; + events.at(eventIndex).second += 1; + if (properties.contains(arg)) { + properties.at(arg).push_back(eventIndex); + + } else { + properties.emplace(arg, std::vector{eventIndex}); + } + } + } + if (events.size() == 0) { + throw std::invalid_argument("At least one event must be given"); + } + if (properties.size() == 0) { + throw std::invalid_argument("At least one property must be given"); + } + + // events without any properties are now technically allowed + if (debug) { + int i = 0; + for (auto& [name, nRequired] : events) { + std::println("Event {}: {} ({})", i, name, nRequired); + i += 1; + } + for (auto& [prop, eventIdx] : properties) { + std::println("Property of {:c}: {}", eventIdx, prop); // TODO + } + std::println("Command: {}", command); + } + + + // testing + /*properties = {*/ + /* {"EVENT=remove", 0},*/ + /* {"ID_MODEL=Flipper_Hediago", 0},*/ + /* {"EVENT=add", 1}*/ + /*};*/ + /*events = {{"FLIPPER REMOVED", 1}};*/ + + // index into events and how many properties were found + std::unordered_map foundEvents; + + procbuf buf(command); + std::istream stream(&buf); + std::string line; + while (stream) { + std::getline(stream, line); + /*std::println("{}", line);*/ + if (properties.contains(line)) { + for (unsigned eventIndex : properties.at(line)) { + unsigned& nFound = foundEvents[eventIndex]; + nFound += 1; + } + /*std::println("Event={}, found nr. {} = {}", eventIndex, nFound, line);*/ + } + // With -p, one udev event ends with two newlines + // -> event end + if (line.size() == 0) { + // print all event names for which all properties were found + for (const auto& [index, nFound] : foundEvents) { + unsigned nRequired = events.at(index).second; + if (nFound == nRequired) { + std::println("{}", events.at(index).first); + } + } + foundEvents.clear(); + } + } + return 0; +}