在无头系统上启动分享会话 D-Bus 的 systemd 服务。

我需要帮助在一个无界面的Linux系统上启动通过会话(而不是系统)D-Bus通信的服务。关键是没有人会登录到无界面系统上。
到目前为止,我已经能够代表一个未登录的用户("otheruser")在三个不同的终端中启动D-Bus守护进程并测试D-Bus通信:
在第一个终端中,我为"otheruser"启动一个D-Bus守护进程:
$ sudo -u otheruser dbus-daemon --session --print-address 1
unix:abstract=/tmp/dbus-a5cU7r4IHc,guid=6c0a9bbfd02f5f68da0fe32f5a5e0a48

在第二个终端中,我使用上述DBUS_SESSION_BUS_ADDRESS响应启动D-Bus服务器应用程序。
$ sudo -u otheruser DBUS_SESSION_BUS_ADDRESS="unix:abstract=/tmp/dbus-a5cU7r4IHc,guid=6c0a9bbfd02f5f68da0fe32f5a5e0a48" /usr/bin/my-dbus-service

然后,在第三个终端上,我可以测试连接:
$ sudo -u otheruser DBUS_SESSION_BUS_ADDRESS="unix:abstract=/tmp/dbus-a5cU7r4IHc,guid=6c0a9bbfd02f5f68da0fe32f5a5e0a48" gdbus introspect --session --dest com.mycompany.myappname --object-path /com/mycompany/interface

但是,我想通过systemd启动D-Bus服务器应用程序以及一些客户端D-Bus服务。我该如何通过systemd启动D-Bus会话,以便将其DBUS_SESSION_BUS_ADDRESS环境变量传递给D-Bus服务器和客户端服务,供"otheruser"使用?
一个可能的解决方案是将dbus-daemon的输出导入到一个"somefile"中,然后在启动D-Bus服务器和客户端之前设置DBUS_SESSION_BUS_ADDRESS=$(cat somefile)。对我来说,这似乎有点笨拙;特别是因为我知道systemd服务文件中的"Busname"指令对于系统级D-Bus连接有一些魔法。我该如何正确地启动针对"otheruser"的systemd服务,以便这些systemd服务可以与会话D-Bus接口进行通信?
1个回答

你需要几个东西来使这个工作起来。
1. 启用用户服务在启动时无需用户登录(systemd linger)。 2. 一个 systemd socket 文件来指定 D-Bus socket 给 systemd 分配。 3. 一个 systemd 服务来启动 D-Bus 会话总线,然后设置 DBUS_SESSION_BUS_ADDRESS 环境变量给其他 systemd 服务使用。 4. 确保你的 systemd 的 my-dbus-client.service 文件是 Type=dbus 或依赖于 dbus.socket 单元,以确保它们分配 dbus 会话总线套接字并启动 dbus 会话服务(如果尚未启动)。
首先,要使特定用户的Systemd服务在启动时无需登录即可启动,您需要启用systemd用户延迟 - 这只需要在配置为为用户启用它时作为root执行一次即可。
# loginctl enable-linger otheruser

接下来,如果您使用的是基于Debian的系统,对于接下来的两个步骤,您可以简单地安装dbus-user-session软件包。
# apt-get install dbus-user-session

如果您使用其他发行版,想要手动操作,或者只是想了解它的工作原理,请继续阅读。否则,请跳过创建dbus.servicedbus.socket的步骤。
创建文件/usr/lib/systemd/user/dbus.socket(注意,在某些发行版中,用户目录可能位于/lib而不是/usr/lib)并添加以下内容:
[Unit]
Description=D-Bus User Message Bus Socket

[Socket]
ListenStream=%t/bus
ExecStartPost=-/bin/systemctl --user set-environment DBUS_SESSION_BUS_ADDRESS=unix:path=%t/bus

[Install]
WantedBy=sockets.target
Also=dbus.service

传播DBUS_SESSION_BUS_ADDRESS到所有服务,这是您的主要关注点,可以通过下面的ExecPostStart行来解决 - 所有后续的服务都将设置该值。 %t将被替换为XDG_RUNTIME_DIR - 这是一个在/run目录下由systemd特定于用户会话创建的临时目录,您可以在其中存放文件。如果您希望在其他地方创建此套接字,没有任何问题。只需确保它是临时的,或者在重新启动/会话拆除时进行清理。
我尝试将dbus unix套接字设置为抽象套接字时遇到了一些问题 - 由于某种原因,systemd似乎不喜欢unix:abstract=@前缀的形式。
现在,请创建文件/usr/lib/systemd/user/dbus.service,并使用以下内容填充:
[Unit]
Description=D-Bus User Message Bus
Requires=dbus.socket

[Service]
ExecStart=/usr/bin/dbus-daemon --session --address=systemd: --nofork --nopidfile --systemd-activation
ExecReload=/usr/bin/dbus-send --print-reply --session --type=method_call --dest=org.freedesktop.DBus / org.freedesktop.DBus.ReloadConfig

[Install]
Also=dbus.socket

这里有一点魔法在幕后由systemd完成,将已创建的Unix套接字传递给dbus-daemon。Systemd使用dbus.socket中的信息来创建套接字,并将其文件描述符设置在环境变量LISTEN_FDS中,然后传递给dbus-daemon。上面列出的特殊选项使dbus-daemon使用传入的文件描述符而不是创建新的文件描述符。这样,dbus客户端可以在dbus-daemon启动时并行启动,而不必担心套接字不存在。
最后,创建自己的systemd用户服务,确保将类型设置为Type=dbus,将BusName=设置为将由此服务注册的dbus服务名称之一,或者确保在Unit部分中指定Requires=dbus.socket。 以下是一个示例:
[Unit]
Description=Config Server Startup

[Service]
Type=dbus
BusName=com.example.app.configuree
ExecStart=/opt/example/app/configuration_server
Restart=on-failure

[Install]
WantedBy=default.target

你可以把它们放在几个地方之一:
  • $HOME/.config/systemd/user
  • /usr/lib/systemd/user
使用systemctl --user enable <service name>命令启用您的服务,并重新启动,一切应该正常工作。
要使systemctl --user ..命令生效,您需要拥有完整的systemd登录环境,以便/run/user/{uid}目录存在。通过su - .. --login或sudo创建的轻量级环境无法设置此项。您需要通过ssh登录、在控制台登录,或者如果您运行的是正确设置的systemd发行版,可以使用machinectl shell命令在当前shell中创建一个完整的systemd环境。

参考资料:

  • man loginctl 了解 linger 的用法
  • man pam_systemd 获取 XDG_RUNTIME_DIR 相关信息
  • man systemd.service 查看 Type=dbus、BusName= 和对 dbus.socket 的隐式依赖
  • man sd_listen_fds 获取 LISTEN_FDS 环境变量的相关信息
  • https://wiki.archlinux.org/index.php/Systemd/User - 关于 systemd 用户会话的一般信息

2这份文件资料有点不够详细,很难将所有内容拼凑起来。我很高兴它对某人有所帮助!也许将来它还会再次帮到我。 :D - Keithel
@mianos,你能帮我编辑一下我的回答以澄清这个问题吗?一个更好的回答,不需要读评论就能明白的话,那就太棒了。 - Keithel