initial commit

This commit is contained in:
matthias@quintern.xyz 2025-03-20 19:01:20 +01:00
parent b1a42e3d89
commit 64193c8d7b
4 changed files with 350 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
build
udev-filter
*.vimspector.json
*.clangd

67
readme.md Normal file
View File

@ -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 <subsystem>` to udevadm, and later `--command-add="-s <subsystem>"` 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`

56
src/Makefile Executable file
View File

@ -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)

223
src/main.cpp Normal file
View File

@ -0,0 +1,223 @@
#include <cstdio>
#include <iostream>
#include <stdexcept>
#include <streambuf>
#include <string>
#include <print>
#include <unordered_map>
#include <vector>
#include <ranges>
/**
* @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<typename T, class CharT=char>
concept FormatableRange = std::ranges::forward_range<T> && std::formattable<std::ranges::range_value_t<T>, CharT>;
/**
* @brief Format ranges
* @details
* Format string accepts c = {curly brackets}, b = [box brackets], a = <angle brackets>, r = (round brackets)
*/
template<typename U, class CharT> requires FormatableRange<U, CharT>
struct std::formatter<U, CharT> {
using T = std::ranges::range_value_t<U>;
constexpr auto parse(std::format_parse_context& ctx){
auto pos = ctx.begin();
while (pos != ctx.end() && *pos != '}') {
if (*pos == 'a') { useChar = 0; } // <a>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<char>(c) == '=') {*/
/* c = static_cast<int>('B');*/
/*}*/
ch = static_cast<char>(c);
setg(&ch, &ch, &ch+1); // point to area
return traits_type::to_int_type(static_cast<char>(ch));
}
private:
FILE* file;
char ch;
};
int main(int argc, const char** argv) {
bool debug=false;
// parse args
std::unordered_map<std::string_view, std::vector<unsigned>> properties;
// name and how many properties need to match
std::vector<std::pair<std::string_view, unsigned>> 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=<event>
if (events.empty()) {
throw std::invalid_argument(std::format("Properties must come after a '--event=<event name>', 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<unsigned>{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<unsigned, unsigned> 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;
}