udev-act

udev-act is a small program you can run inside your X session to respond to usb udev events. I use it to run a script that configures my mouse buttons when I plug it in and also to start up the 3d printer control program when I plug in my 3d printer:

udev-act.c

// The udev-act program to be run on a per user basis to execute
// commands when udev usb devices appear, disappear, etc.
//
#include <stdlib.h>
#include <string>
#include <iostream>
#include <fstream>
#include <sstream>
#include <unistd.h>
#include <libudev.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

using namespace std;

static const char * debug_file = NULL;

struct config_info {
   config_info * next;
   string        action;
   string        usb_id;
   string        command;
};

struct config_info * config_list = NULL;
struct config_info ** config_end = &config_list;

// path under $HOME where the config file is located.
//
#define CONFIG_FILE_NAME "/.config/udev-act.conf"

static void
write_debug(string dbgstr) {
   ofstream outfile;
   outfile.open(debug_file, ios::app);
   outfile << dbgstr;
   outfile << endl;
}

// Read the config file and build config_list records
//
static void
read_config_file() {
   char * homedir = getenv("HOME");
   string cfile = homedir;
   cfile.append(CONFIG_FILE_NAME);
   ifstream s;
   s.open(cfile);
   if (s.is_open()) {
      string line;
      while (getline(s,line)) {
         if (line[0] != '#') {
            istringstream iss(line);
            string act;
            string id;
            string prog;
            if (iss >> act >> id >> prog) {
               config_info * ci = new config_info;
               ci->next = *config_end;
               *config_end = ci;
               config_end = &ci->next;
               ci->action = act;
               ci->usb_id = id;
               ci->command = prog;
            }
         }
      }
      s.close();
   } else {
      cerr << "Missing config file.\n";
   }
   config_info * ci = config_list;
}

// Run a command using the double fork technique so I don't need to
// wait on kids.
//
static void
run_command(string command) {
   pid_t kid = fork();
   if (kid == 0) {

      // I am the kid, fork again to do the exec then exit, let
      // the system wait on the actual command, I won't have to.

      pid_t grandkid = fork();
      if (grandkid == 0) {

         // Setup /dev/null as stdin, stdout, and stderr, close any
         // other file descriptors that might be hanging around.

         int null_out_fd = open("/dev/null", O_WRONLY);
         int null_in_fd = open("/dev/null", O_RDONLY);
         dup2(null_out_fd, 1 /* stdout */);
         dup2(null_out_fd, 2 /* stderr */);
         dup2(null_in_fd, 0 /* stdin */);
         for (int i = 3; i < 60000; ++i) {
            close(i);
         }

         // Now run the command

         if (debug_file != NULL) {
            string dbgstr;
            dbgstr.append("In run_command with: ");
            dbgstr.append(command);
            write_debug(dbgstr);
         }
         execl("/bin/sh", "/bin/sh", "-c", command.c_str(), NULL);

         // And if that somehow didn't work, exit

         _exit(2);
      } else {
         _exit(0);
      }
   } else {

      // Wait on the kid to exit (which it should do almost
      // immediately - as soon as it forks again).

      int status;
      waitpid(kid, &status, 0);
   }
}

// Extract first line of file and append to str, return true if
// file existed and there was something appended, false otherwise
//
static bool
append_line(string file, string & str) {
   bool rval = false;
   ifstream s;
   s.open(file);
   if (s.is_open()) {
      string line;
      if (getline(s,line)) {
         if (line.length() > 0) {
            str.append(line);
            rval = true;
         }
      }
      s.close();
   }
   return rval;
}

// Monitor usb udev events and if they are associated with a path
// containing a vendor and product id, compare to the config records
// and run the commands given in the config. Loop forever doing this.
//
static void
monitor_events() {
   struct udev *udev;
   struct udev_device *dev;
   struct udev_monitor *mon;
   int fd;

   udev = udev_new();
   if (!udev) {
      cerr << "Can't create udev\n";
      return;
   }

   mon = udev_monitor_new_from_netlink(udev, "udev");
   udev_monitor_filter_add_match_subsystem_devtype(mon, "usb", NULL);
   udev_monitor_enable_receiving(mon);
   fd = udev_monitor_get_fd(mon);

   while (1) {
      fd_set fds;
      int ret;

      FD_ZERO(&fds);
      FD_SET(fd, &fds);

      ret = select(fd+1, &fds, NULL, NULL, NULL);
      if (ret > 0 && FD_ISSET(fd, &fds)) {
         dev = udev_monitor_receive_device(mon);
         if (dev) {
            string dbgstr;
            string curact = udev_device_get_action(dev);
            string dirname = "/sys";
            dirname.append(udev_device_get_devpath(dev));
            udev_device_unref(dev);
            string prodfile = dirname;
            prodfile.append("/idProduct");
            string vendfile = dirname;
            vendfile.append("/idVendor");
            if (debug_file != NULL) {
               dbgstr.append("curact: ");
               dbgstr.append(curact);
               dbgstr.append(", dirname: ");
               dbgstr.append(dirname);
            }
            string usbid;
            if (append_line(vendfile, usbid)) {
               if (debug_file != NULL) {
                  dbgstr.append(", vendor id: ");
                  dbgstr.append(usbid);
               }
               usbid.append(":");
               if (append_line(prodfile, usbid)) {
                  if (debug_file != NULL) {
                     dbgstr.append(", product id: ");
                     dbgstr.append(usbid);
                  }
                  config_info * ci = config_list;
                  while (ci != NULL) {
                     if ((ci->action.compare(curact) == 0) &&
                         (ci->usb_id.compare(usbid) == 0)) {
                        if (debug_file != NULL) {
                           dbgstr.append(", run command: ");
                           dbgstr.append(ci->command);
                        }
                        run_command(ci->command);
                     }
                     ci = ci->next;
                  }
               }
            }
            if (debug_file != NULL) {
               write_debug(dbgstr);
            }
         }
      }
   }
   udev_unref(udev);
}

int
main(int argc, char ** argv) {
   if ((argc > 2) && (strcmp(argv[1], "-d") == 0)) {
      debug_file = argv[2];
   }
   read_config_file();
   monitor_events();
   return 2;
}

Here's my config file as a sample:

udev-act.conf

# Sample config file for udev-act user daemon
#
# When action shows up with the given usb device ID run command:
#
# Action ID command
#
bind	047d:8018	/home/tom/scripts/new-mouse-settings
# bind    1949:0324       /home/tom/scripts/paperwhite-plugged
bind    1949:0004       /home/tom/scripts/kindle-plugged
Page last modified Fri Sep 4 15:51:59 2020