0

I am working on a systemd/udev setting that I would like to share upstream, however I can't get it to work in a non-hacky way. Essentially, I have this script as the exec of a systemd service:

      ICON="somepath/dslr-camera-white.png"
  function on-display() {
      local sdisplay=$(echo $XDG_SESSION_TYPE)
      if [ "$sdisplay" == "wayland" ]; then
      local display=":$(echo $WAYLAND_DISPLAY)"
      else
      local display=":$(ls /tmp/.X11-unix/* | sed 's#/tmp/.X11-unix/X##' | head -n 1)"
      fi

      local user=$(who | grep '('$display')' | awk '{print $1}' | head -n 1)

      local uid=$(id -u $user)

      sudo -u $user DISPLAY=$display DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$uid/bus "$@"
  }

  cleanup() {
      on-display notify-send -i $ICON "Disconnected" "The DSLR Camera has been turned off." --app-name="DSLR Webcam"
      trap - SIGTERM && kill -- -$$
  }

  trap cleanup SIGINT SIGTERM EXIT

  on-display notify-send -i $ICON "Connected" "The DSLR Camera has been turned on and it is ready to use." --app-name="DSLR Webcam"
  on-display yad --window-icon=$ICON --image=$ICON --no-buttons --title="DSLR Webcam" --notification --listen &

  output=$(v4l2-ctl --list-devices)
  line=$(echo "$output" | grep "Virtual Camera")
  vdevice=$(echo "$output" | sed -n "/$line/{n;s/^\t\+//p;}")
  gphoto2 --stdout --capture-movie | ffmpeg -i - -vcodec rawvideo -pix_fmt yuv420p -threads 0 -f v4l2 $vdevice

The point of the service is to be started by an udev rule when an specific gphoto2 supported camera is plugged in, therefor I have these udev rules:

ACTION=="add", ATTR{idVendor}=="04a9", ATTR{idProduct}=="3218", RUN+="systemctl start dslr-webcam.service"
ACTION=="remove", ATTR{idVendor}=="04a9", ATTR{idProduct}=="3218", RUN+="systemctl stop dslr-webcam.service"

So far so "ok", because I have read on alternatives to using RUN for a systemd service, but regardless... The issue here is specifically the call to yad.

yad is a program that allows you to display dialogue boxes from the CLI, I am leveraging it for its ability to make a systrey icon, as I would like for one to show up when the camera is active.

The problem

Unlike notify-send, which probably works off some common socket, yad requires the appropriate XAUTHORITY set in order for it to work, otherwise you will get cannot open display: :0. The hacky solution in my case is to simply set it the right Xauthority, since I am using SDDM (display manager), it resides in the /tmp directory, so I can add this to the script:

XAUTHORITY=$(ls /tmp/xauth*)

And then it works... But this is terrible, it makes a lot of assumptions, in fact the whole on-display function also seems like a bad idea. If I took this to a different system, it probably wouldn't work, because the proper Xauthority could be in a myriad of places, and I haven't even tried Wayland yet (I will leave that for later).

What about xauth

I thought I could somehow use xauth to retrieve the right Xauthority, but that doesn't seem to be the case... This script is a system service, so my xauth info returns Authority file: /root/.xauthWV7OfU, and running it as the right user sudo -u $user xauth info gives me Authority file: /home/myuser/.Xauthority, none of which work when given to yad. The correct XAUTHORITY is set by the display manager, so I think I could only get it from child processes of it.

I have also tried all the approaches give by this answer, but the first one would not work, as XAUTHORITY isn't actually in the system env, and the second one (aside from the mentioned pitfalls), doesn't work, it says the xauth file is at /run/sddm/xauth_KvyuHd, but trying to use it doesn't work, so it is not the same xauth file as /tmp/xauth_FzoQqz

From the same question as the previous one, this approach seems to work, but I don't know how portable it is. And still seems hacky.


Run as user service

Perhaps this is the most promising one, as nothing in that script prevents it from being run as a user (it also gets rid of on-display), I did try this approach, and while if the camera was plugged-in and I ran systemctl --user start dslr-webcam.service it would work as expected (including yad), now I have a problem with the udev rule. I searched many places, including here, but I cannot find how to run a systemd user service from an udev rule, and to me that doesn't really make any sense either, how would udev know what user to use?

1 Answers1

1

You don't directly run user services from udev rules; instead, you should have udev tell systemd that the device wants a particular user service to run.

With a TAG+="systemd", ENV{SYSTEMD_USER_WANTS}="dslr-webcam.service" like this:

ACTION=="add", ATTR{idVendor}=="04a9", ATTR{idProduct}=="3218". TAG+="systemd", ENV{SYSTEMD_USER_WANTS}="dslr-webcam.service"
ACTION=="remove", ATTR{idVendor}=="04a9", ATTR{idProduct}=="3218", TAG+="systemd", ENV{SYSTEMD_USER_WANTS}="dslr-webcam.service"

Since your dslr-webcam.service requires GUI access, it should declare itself as BindsTo=graphical-session.target and After=graphical-session.target. When declared this way, the service should get the right DISPLAY and XAUTHORITY variables automatically.

By the way, thanks for sharing your idea; it gave me inspiration to set up something very similar...

telcoM
  • 96,466
  • Oh so that is what I had to do! I was running around in circles for ages for this one. Just one follow-up question, how does systemd know that the second call from udev is to stop the service? The two rules are identical, expect for the action that triggers them. – Mathias Sven Oct 28 '23 at 20:02
  • 1
    The add event causes systemd to create a *.device unit with a "Wants" dependency on the user service unit, the remove event causes that device unit to go away, and then the service unit will no longer be wanted by anything. If you need to make the stop behavior stronger, you might add something like Requisite=<dslr>.device to the dslr-webcam.service, assuming that the DSLR's *.device unit gets a stable name. If the name is awkward, add ENV{SYSTEMD_ALIAS}="/sys/subsystem/usb/dslr" or similar to the udev rules to get a device unit with a more useful name. – telcoM Oct 28 '23 at 20:18