Perfect Method to Use systemd in WSL2

發表於
分類於 心得

This article is automatically translated by LLM, so the translation may be inaccurate or incomplete. If you find any mistake, please let me know.
You can find the original article here .

The major difference between WSL2 and general Linux is that it uses Microsoft's own init, rather than the commonly seen systemd in typical Linux distros. This can cause some issues with package managers that rely on systemd, such as Arch Linux.

This article briefly documents the method to perfectly set up a systemd environment in ArchWSL within WSL2. Other distros should also be able to use this method.

Reference dotfiles: maple3142/dotfiles

Update on 2022/11/17: Since WSL officially launched version 1.0.0 on the Microsoft Store today, I can finally install the new version of WSL on Windows 10 and successfully use the built-in systemd. You can refer to the last paragraph for this part.

systemd bottle

To run systemd in WSL2, we use the namespace feature, requiring tools written by others to wrap systemd in that environment, allowing it to function as PID 1. You can choose one of the following two tools to install:

I've used both, and their usage is quite similar. Due to performance considerations, I ultimately used subsystemctl. The rest of this article will introduce subsystemctl.

Assuming the shell used is zsh

Basic Operations

After installation, you need to use sudo subsystemctl start to enable systemd. You need to do this every time you restart WSL. You can automate this by adding the following to your .zshrc:

if [[ -v WSL_DISTRO_NAME ]] then
	if (( $+commands[subsystemctl] )); then
		if ! subsystemctl is-running; then
			sudo subsystemctl start
		fi
	fi
fi

This way, it will automatically enable systemd based on the situation, so you only need to enter the sudo password once.

You can then use subsystemctl shell to enter an environment where systemd can run, or use subsystemctl exec to directly execute commands, such as subsystemctl exec -- systemctl status docker.

It's best to add -- when using subsystemctl exec, otherwise the flags of the subsequent commands will be interpreted as flags for subsystemctl

For example, to update Arch Linux, remember to use subsystemctl exec -- pacman -Syu, so that packages requiring systemd can function properly.

This setup allows basic use of systemd. If you're satisfied with this, you can stop reading here. The rest of the article explains how to automate the subsystemctl shell process.

Issues with subsystemctl shell

subsystemctl shell seems convenient, so you might want to use .zshrc to enter the systemd environment directly. However, there are many small issues to handle.

For example, after typing exit, you'll find it only exits subsystemctl shell without exiting the entire shell.

Another issue is typing cat, then abc, and pressing Backspace twice might result in abc\cb: arkane-systems/genie#145

Some WSL environment variables like WSL_DISTRO_NAME disappear, causing many WSL features to malfunction, such as executing .exe files. This is the biggest and most difficult issue to fix.

Fixing exit

This is easy to handle. Change subsystemctl shell to exec subsystemctl shell. The former forks a new process to execute the command, while the latter replaces the current process. In the latter case, exiting will terminate the entire process, solving the issue.

if ! subsystemctl is-inside; then
	exec subsystemctl shell --quiet
fi

Fixing Backspace

The solution to this problem is directly mentioned in the issue. Executing stty -echoprt or stty sane will fix it.

Although it's said to be an Ubuntu-specific issue, I encountered it in Arch Linux as well. Fortunately, the solution is the same.

Fixing Environment Variables

This is the biggest issue. After executing subsystemctl shell, many environment variables disappear, including PWD, PATH, and WSL_DISTRO_NAME. Fixing this is crucial.

Genie's readme mentions it supports copying environment variables. Reading the source code reveals it writes environment variables and related information to a file, then loads them back with another script.

Although subsystemctl doesn't support this feature, you can implement it yourself. I implemented it directly in .zshrc, avoiding the need to modify Rust code (which I'm not good at).

The complete implementation is as follows. Place it at the beginning of .zshrc:

# Start genie in WSL if exists
if [[ -f ~/.subsystemctl_env ]] then
	source ~/.subsystemctl_env
	rm ~/.subsystemctl_env
	stty -echoprt # fix backspace
fi
if [[ -v WSL_DISTRO_NAME ]] then
	if (( $+commands[subsystemctl] )); then
		if ! subsystemctl is-running; then
			sudo subsystemctl start
		fi
		if ! subsystemctl is-inside; then
			cat > ~/.subsystemctl_env << EOF
export PATH="$PATH"
export WSL_DISTRO_NAME="$WSL_DISTRO_NAME"
export WSL_INTEROP="$WSL_INTEROP"
export WSLENV="$WSLENV"
export DISPLAY="$DISPLAY"
export WAYLAND_DISPLAY="$WAYLAND_DISPLAY"
export PULSE_SERVER="$PULSE_SERVER"
cd "$PWD"
EOF
			exec subsystemctl shell --quiet
			rm ~/.subsystemctl_env # should never reach here, but it is convenient for testing...
		fi
	fi
fi

The only drawback of this approach is that WSL-related environment variables won't be present when connecting to WSL via ssh. Everything else works fine.

WSLg

If you want to use WSLg instead of a general X server, some additional adjustments are needed.

To use WSLg, DISPLAY should be set to :0. However, it won't work directly in the subsystemctl shell namespace. You can find a solution on Google, which involves linking /tmp/.X11-unix/X0 to /mnt/wslg/.X11-unix/X0.

Add the following code:

if [[ -v WSL_DISTRO_NAME ]] then
	if [[ -S /mnt/wslg/.X11-unix/X0 ]] then
		WSLG_EXIST=1  # prefer wslg if it exists
		if [[ ! -S /tmp/.X11-unix/X0 ]] then
			# fix wslg not working in subsystemctl namespace
			# https://github.com/arkane-systems/genie/issues/175#issuecomment-922526126
			ln -s /mnt/wslg/.X11-unix/X0 /tmp/.X11-unix/X0
		fi
	fi
	if (( $+commands[subsystemctl] )); then
		# omitted...

The WSLG_EXIST variable is used to decide whether to change DISPLAY to the X server:

	if [[ "1" != "$WSLG_EXIST" ]] then
		export DISPLAY=$(ip route show default | awk '{print $3}'):0
	fi

[Update] Using the New WSL Built-in systemd

Edit /etc/wsl.conf and add the following lines:

[boot]
systemd=true

If you previously used subsystemctl, change the check condition to:

    if [[ "$(ps --no-headers -o comm 1)" != "systemd" ]] && (( $+commands[subsystemctl] )); then

Then, after wsl --shutdown and restarting, use ps --no-headers -o comm 1 to check if it's systemd. You can then test commands like systemctl status to ensure they work properly.