如何在 shell 脚本中确定它是否被 systemd 调用?

如何在 shell 脚本中确定它是否被 systemd 调用?

由于各种原因,我们有一个 shell 脚本来包装供应商的应用程序。我们的系统管理员和应用程序所有者对 systemd 的熟悉程度参差不齐。因此,在应用程序失败的情况下(systemctl 也表明了这一点),某些最终用户(包括“root”系统管理员)可能会使用包装器脚本“直接”启动应用程序,而不是使用systemctl restart.这可能会在重新启动期间导致问题,因为 systemd 不会调用正确的关闭脚本 - 因为就其而言,应用程序已经停止。

为了帮助指导向 systemd 的过渡,我想更新包装器脚本以确定它是由 systemd 还是由最终用户调用;如果它被调用外部systemd,我想向调用者打印一条消息,告诉他们使用 systemctl。

如何在 shell 脚本中确定它是否被 systemd 调用?

您可能会假设:

  • 包装脚本的 bash shell
  • 包装器脚本成功启动和停止应用程序
  • systemd 服务按预期工作

systemd 服务的示例如下:

[Unit]
Description=Vendor's Application 
After=network-online.target

[Service]
ExecStart=/path/to/wrapper start
ExecStop=/path/to/wrapper stop
Type=forking

[Install]
WantedBy=multi-user.target

我不感兴趣检测init系统,因为我已经知道它是 systemd。

答案1

卢卡斯·韦克梅斯特内容丰富回答服务器故障:

  • 对于 systemd 版本 231 及更高版本,有一个 JOURNAL_STREAM 变量是为 stdout 或 stderr 连接到日志的服务设置的。
  • 对于 systemd 版本 232 及更高版本,设置了一个 INVOCATION_ID 变量。

如果您不想依赖这些变量,或者对于 231 之前的 systemd 版本,您可以检查父 PID 是否等于 1:

if [[ $PPID -ne 1 ]]
then
  echo "Don't call me directly; instead, call 'systemctl start/stop service-name'"
  exit 1
fi >&2

答案2

简短的回答

if ! grep -qEe '[.]service$' /proc/self/cgroup; then
    echo "This script should be started with systemctl" >&2
    exit 1
fi

...或者,如果您知道预期运行的特定服务名称,并且希望能够抵御阻止创建用户会话的错误配置:

if ! grep -qEe '/myservice[.]service$' /proc/self/cgroup; then
    echo "This service should be started with systemctl start myservice" >&2
    exit 1
fi

为什么它有效

确定哪个服务(如果有)启动当前进程的一种方法是检查/proc/self/cgroup。对于systemd触发的服务,这将包含服务名称;例如:

12:pids:/system.slice/dhcpcd.service
11:rdma:/
10:memory:/system.slice/dhcpcd.service
9:blkio:/system.slice/dhcpcd.service
8:devices:/system.slice/dhcpcd.service
7:hugetlb:/
6:cpuset:/
5:freezer:/
4:cpu,cpuacct:/system.slice/dhcpcd.service
3:net_cls,net_prio:/
2:perf_event:/
1:name=systemd:/system.slice/dhcpcd.service
0::/system.slice/dhcpcd.service

...而对于与用户会话关联的进程,cgroup 会更像这样/user.slice/user-1000.slice/session-337.scope(假设这是 UID 1000 的用户自上次重新启动以来在系统上的第 337 个会话)。


更奇特的实现

如果想要检测正在运行的特定服务,也可以从 中提取该服务/proc/self/cgroup。例如考虑:

cgroup_full=$(awk -F: '$1 == 0 { print $3 }' /proc/self/cgroup)
cgroup_short=${cgroup_full##*/}
case $cgroup_full in
  /system.slice/*.service) echo "Run from system service ${cgroup_short%.*}";;
  /user.slice/*.service)   echo "Run from user service ${cgroup_short%.*}";;
  *.service)               echo "Service ${cgroup_short%.*} type unknown";;
  *)                       echo "Not run from a systemd service; in $cgroup_full";;
esac

答案3

我想到的另一个明显的解决方案是添加类似的内容

Environment=FROM_SYSTEMD=1

到服务文件,并在该环境变量上进行测试。

答案4

另一种方法可能是systemd显式查询,以获得更紧密耦合的检查。

例如,对于类似的用例,我一直在这样做:

if [ "$(systemctl show -p ControlPID vendorservice)" != "ControlPID=$$" ]; then
    echo 'no go'
    exit 1
fi

上面的代码片段仅适用于type=forking服务,因为它利用了特定的状态一种工作可以在其一生中存在。

具体来说,它查询ControlPID由此设置的值systemd,对于type=forking作业,表示进程产生直接地systemd同时它本身正在等待出现PIDFile(或GuessMainPID检索某些内容),之后该ControlPID值被设置回0。这种特殊的行为systemd还应该保护该检查免受可能的 PID 环绕的影响。

可以通过 检索许多属性systemctl show,其中一些属性根据具体type=使用的情况而有所不同,因此对于 和/或根据特定用例以外的服务类型type=forking,可能需要查询最合适的属性( s) 执行等效的“网络共享”检查。

因此,这里的解决方案不是通用解决方案,但它可能更强大、向后兼容1以及面向未来的2

另请注意,您必须已经知道要查询的正确服务名称,特别是对于实例服务,您需要知道vendorservice@1要查询的确切实例名称(例如 )。


1 ControlPID财产命令systemctl show以其选项-p自第一个版本发布以来就一直存在systemd

2 systemctl show输出是的一部分稳定承诺

相关内容