WSLg: On Making WSLg and systemd Play Nicely Together

Three things must ye do; and the number of your doings must be three.

(in genie, anyway; also, credit for a lot of this should go to @diddledan , who figured it out first for his systemd solution)

There are three things genie does to play nicely with WSLg:

1. Copy the environment variables.

When systemd is started up inside its nice new pid namespace, it doesn't inherit the environment of the user distro namespace; which is what you want, really. Starting a systemd-as-pid-1 is essentially booting a Linux system, so it's starting from a clean slate. For this reason, genie already included mechanisms to clone certain environment variables (WSL_DISTRO_NAME,WSL_INTEROP,WSLENV) and the outside path into the "bottle" to keep essential functions working, so it was as simple as adding the WSLg environment variables (DISPLAY,WAYLAND_DISPLAY,PULSE_SERVER) to the default list to be cloned.

2. Map the /tmp/.X11-unix/X0 socket

Systemd, in its default configuration, does a lot of things to initialize the system. Unfortunately for us, one of them is to clean up the /tmp directory, via a unit named systemd-tmpfiles-setup, which blows away the link WSLg has established to the XWayland socket at /mnt/wslg/.X11-unix/X0. (Ordinarily, this is desirable - on a regular Linux system, it would be cleaning up detritus left over from previous boots.) This is a very important link, since X will look for the Unix sockets for local displays in /tmp/.X11-unix and nowhere else.

Now, we could just disable that unit, or edit the configuration files for systemd-tmpfiles not to touch that folder, but ideally we would like to make this work without having to modify bits of existing distros, especially bits that are fairly deeply embedded in their startup sequence. So instead, we make and install a couple of systemd units of our own, taking advantage of systemd-socket-proxyd:

wslg-xwayland.socket

[Unit]
Requires=systemd-tmpfiles-setup.service
After=systemd-tmpfiles-setup.service 
ConditionVirtualization=wsl
VonditionPathExists=/mnt/wslg/.X11-unix/X0

[Socket]
ListenStream=/tmp/.X11-unix/X0

[Install]
WantedBy=sockets.target 

And wslg-xwayland.service

[Unit]
Requires=wslg-xwayland.socket
After=wslg-xwayland.socket
ConditionVirtualization=wsl
ConditionPathExists=/mnt/wslg/.X11-unix/X0

[Service]
ExecStart=/lib/systemd/systemd-socket-proxyd /mnt/wslg/.X11-unix/X0 

which arranges for systemd itself to proxy connections to the X server to the proper location for WSLg.

3. Mount the runtime directory in the expected place

WSLg locates the various other sockets and files used to access parts of it under /mnt/wslg/runtime-dir. For most purposes, this is fine, since the environment variables inform clients of that, and everything works. But then you get to things like snaps, or certain apparmor profiles, or other things that expect the user runtime directory to exist under /usr/run/{UID} and will break if it doesn't find it there.

To deal with this one, we need the systemd unit that sets up the user runtime directories to instead use the WSLg runtime directory, but only for the primary user . That criterion is because things expect the user runtime directory to be owned by the user it is for, which (for /mnt/wslg/runtime-dir) is UID 1000 (wslg, inside the system distro); conveniently, though, user distros tend to set up the first user created as UID 1000 also, and the /mnt/wslg/runtime-dir will be perceived as owned by this user from inside the user distro.

To achieve this, we add an override file to the user-runtime-dir@.service, as follows:

user-runtime-dir@.service.d/override.conf

[Service]
ExecStartPost=/usr/libexec/genie/map-user-runtime-dir.sh %i
ExecStop=/usr/libexec/genie/unmap-user-runtime-dir.sh %i 

running one of these two scripts:

map-user-runtime-dir.sh

#!/bin/sh
if [ ! -d /mnt/wslg/runtime-dir ]
then
  # WSLg is not present, so do nothing over-and-above previous
  exit
fi

WUID=$(stat -c "%u" /mnt/wslg/runtime-dir)

if [ $1 -eq $WUID ]
then
  # We are the WSLg user, so map the runtime-dir
  mount --bind /mnt/wslg/runtime-dir /run/user/$1
  exit
fi 

map-user-runtime-dir.sh

#!/bin/sh
if [ ! -d /mnt/wslg/runtime-dir ]
then
  # WSLg is not present, so do nothing over-and-above previous
  exit
fi

WUID=$(stat -c "%u" /mnt/wslg/runtime-dir)

if [ $1 -eq $WUID ]
then
  # We are the WSLg user, so unmap the runtime-dir
  umount /run/user/$1
  exit
fi 

These don't replace the default user-runtime-dir@.service; it still creates the user runtime directory under /run/user. What they do do is check if WSLg is present on the machine, and then check if the user they are currently creating a runtime directory for is the user who owns the /mnt/wslg/runtime-dir directory (i.e., has UID 1000) If so, they bind mount the WSLg runtime directory over the /run/user/{UID} directory, and then everything is in the right place for the purposes of those apps with expectations about these things.

Note: a side-effect of this is that other applications that use the user runtime directory will be creating their sockets, files, etc., under the WSLg runtime directory - for example, my /mnt/wslg/runtime-dir looks like this, with the various other things I run:

total 0
srw-rw-rw- 1 avatar avatar   0 May  2 11:11 bus=
drwx------ 3 avatar users   60 May  2 11:10 dbus-1/
drwx------ 2 avatar avatar  60 May  2 11:11 dconf/
drwx------ 2 avatar avatar  60 May  2 11:13 emacs/
drwx------ 2 avatar avatar 140 May  2 11:11 gnupg/
dr-x------ 2 avatar avatar   0 May  2 11:11 gvfs/
drwx------ 3 avatar avatar 140 May  2 11:13 keybase/
drwx------ 2 avatar avatar 100 May  2 11:14 keyring/
srw-rw-rw- 1 avatar avatar   0 May  2 11:11 pk-debconf-socket=
drwxr-xr-x 2 avatar avatar  60 May  2 11:11 podman/
drwx------ 2 avatar users   80 May  2 11:10 pulse/
srw-rw-rw- 1 avatar avatar   0 May  2 11:11 snapd-session-agent.socket=
drwxr-xr-x 6 avatar avatar 160 May  2 11:14 systemd/
srwxrwxrwx 1 avatar users    0 May  2 11:10 wayland-0=
-rw-rw---- 1 avatar users    0 May  2 11:10 wayland-0.lock 

I've observed no adverse or indeed non-adverse side-effects from this (and it is, after all, a tmpfs), but it's probably worth noting for reference.

I hope this helps you if you're trying to put together your own systemd/WSLg hack. Of course, you could just use mine... 😁