initial commit
This commit is contained in:
parent
b1a42e3d89
commit
64193c8d7b
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
build
|
||||
udev-filter
|
||||
*.vimspector.json
|
||||
*.clangd
|
67
readme.md
Normal file
67
readme.md
Normal 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
56
src/Makefile
Executable 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
223
src/main.cpp
Normal 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;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user