项目使用到一套自己搭建的web操作平台。该平台用python+flask+redis+git+svn进行自动化操作。其启动脚本采用的是supervisor来管理。
今天想让开机自动运行supervisor,于是进行了一番尝试。
¶我们的目标
当然,不是“没有蛀牙”。
下文模拟的目标是
系统启动时 无需 登陆,用supervisord启动某supervisord.conf。
看似很简单,其实踩了不少坑。
添加plist脚本
sudo vim /Library/LaunchDaemons/com.agendaless.supervisord.plist
复制以下内容
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
</dict>
<key>Label</key>
<string>com.agendaless.supervisord</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/supervisord</string>
<string>-n</string>
<string>-c</string>
<string>/etc/supervisord.d/supervisord.conf</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
接着注册plist
sudo launchctl load /Library/LaunchDaemons/com.agendaless.supervisord.plist
重启之后就发现可以自动运行了。
But, 事情哪有那么简单
首先解答下为何放在这个目录,以及agent和daemon的区别
根据apple文档,plist文件可以存在于如下几个地方:
类型 | 位置 | 执行条件 |
---|---|---|
用户的agent | ~/Library/LaunchAgents | 当前登录的用户 |
全局agent | /Library/LaunchAgents | 当前登录的用户 |
全局Daemons | /Library/LaunchDaemons | root 或者用User 制定的用户 |
系统agent | /System/Library/LaunchAgents | 当前登录的用户 |
系统daemon | /System/Library/LaunchDaemons | root 或者用User 制定的用户 |
Agent和Daemon最大的区别在于,Agent是需要用户登录才能触发,Daemon是不需要用户登录。
这样,排除system用的目录,只剩下全局Daemons(Global agent),也就是/Library/LaunchDaemons了。
But, 事情哪有那么简单
这样启动supervisor之后,是默认用的root用户启动。所以,当你的脚本中有用到类似于Python中os.path.expanduser('~')的命令时,其实你拿到的是/Users/root。
这一定不是你想要的结果。
根据supervisor的文档,你可以指定user
[program:test]
user=yourname
But, 事情哪有那么简单
查看supervisor的源代码,这个命令只是把进程移交给了这个user。
os.path.expanduser('~')这种命令,是获取的环境变量$HOME。而user=yourname并不会影响任何环境变量。没错,现在os.path.expanduser('~')的结果应该还是/Users/root,但是如果你执行print os.system("whoami") 那就会是yourname。
so? byebye supervisor。 我转而查找launchd的方法。
launchd这个玩意是mac用来替代cron的。我想,一定不会太挫吧。于是在开发文档中又看到:
UserName <string>
This optional key specifies the user to run the job as. This key is only applicable when launchd is running as root.
哦,只在root触发时才有用。没问题。另外也发现了UserGroup这个key,大同小异。那我们加上:
在/Library/LaunchDaemons/com.agendaless.supervisord.plist中最外层dict里面添加:
<key>UserName</key>
<string>yourname</string>
<key>GroupName</key>
<string>staff</string>
然后直接暴力sudo reboot(其实也可以先用launchctl unload com.agendaless.supervisord.plist再launchctl load com.agendaless.supervisord.plist)
ok,程序进来了,一切ok。os.path.expanduser('~')也对了。
But, 事情哪有那么简单
可能到这儿对大多数人都没问题了。但是我的程序是要用这套东西build ios和android的游戏。
命令中有一句,xcodebuild,是在一个Makefile中,这个Makefile的执行,是靠os.system执行。
当程序跑到这里时,会报出错误:
[BEROR]Code Sign error: Provisioning profile does not match bundle identifier: The provisioning profile specified in your build settings (“farm_dev”) has an AppID of “com.wemomo.game.farm” which does not match your bundle identifier “com.ejoy.test.farm”.
[BEROR]CodeSign error: code signing is required for product type 'Application' in SDK 'iOS 8.4'
根据网上内容,可能由于钥匙串没有解锁。所以我们用
os.system('security unlock -p "pwd" ~/Library/Keychains/login.keychain')
os.system('security set-keychain-settings -t 3600 -l ~/Library/Keychains/login.keychain')
来解锁。
可是错误依旧存在
搜了搜,可以强制导入某程序可用的key,用security import后加参数-T
os.system('security import login.keychain -P "pwd" -T /usr/bin/codesign')
依然不行。我差点就放弃了。可是我看到了这篇回复。
回复中说:
我通过添加SessionCreate这个key到org.jenkins-ci.plist中去解决了这个问题。
抱着试一试的心态,我也添加了。终于,成功了。
最后,我的plist是这样的
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>EnvironmentVariables</key>
<dict>
<key>ANDROID_NDK_DIR</key>
<string>/Users/farmbuilder/android/android-ndk-r9d</string>
<key>ANDROID_TOOLS_DIR</key>
<string>/Users/farmbuilder/android/android-sdks/tools</string>
</dict>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
</dict>
<key>Label</key>
<string>com.agendaless.supervisord</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/supervisord</string>
<string>-n</string>
<string>-c</string>
<string>/Users/farmbuilder/Projects/packer/supervisord.conf</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>UserName</key>
<string>farmbuilder</string>
<key>GroupName</key>
<string>staff</string>
<key>SessionCreate</key>
<true />
</dict>
</plist>
可以看到,我使用了EnvironmentVariables来添加环境变量。这个效果,可以等同于在supervisord.conf文件中的[supervisord]段添加environment=ANDROID_NDK_DIR="/Users/farmbuilder/android/android-ndk-r9d", ANDROID_TOOLS_DIR ="/Users/farmbuilder/android/android-sdks/tools"。
那么,SessionCreate是什么东西?
在官方文档中,有这么几行字
sshd is a launchd daemon with the SessionCreate property set, which means that it runs in its own bootstrap namespace. Once a user logs in, the launchd PAM session module (/usr/lib/pam/pam_launchd.so, as configured by /etc/pam.d/sshd) moves the bootstrap namespace to within the appropriate per-user bootstrap namespace.
Uses the global bootstrap namespace unless the SessionCreate property is specified in the property list file, in which case the daemon runs in its own per-session bootstrap namespace.
Uses the global security context unless the SessionCreate property is specified in the property list file, in which case the daemon runs in its own per-session security context
Context Cross Refrence
原来,mac系统没用户登录时创建的namespace是叫做一个global bootstrap,当有用户登录时,sshd就会启动,sshd会创建用户自己的namespace, 并替代global bootstrap成为当前的namespace(security context 安全内容也如此,包含我们用到的codesign)。由于我们是创建的launchd daemon,如果不添加SessionCreate,那么默认是会用global bootstrap的。这个namespace,是肯定不会有我们的security context以及namespace的。