由于各种原因,我们有一个 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
输出是的一部分稳定承诺