モチベーション
macOSでlaunchctl
を使って、起動直後からいい感じにemacs daemonが立ち上がってほしい!
環境
- macOS (11.2.1 arm64)
- homebrew (3.0.1)
- Emacs (27.1 x86_64、
brew install --cask emacs
でインストールされるモノ)
launchdを使う
launchd
はmacOSで動いているsystemd
のようなものです。(wikipedia)
launchctl
というコマンドがあり、これを使って管理をします。
せっかくmacOSに備え付けられている機能なので、これを使っていきます。
設定ファイルの作成
launchctl
で扱う設定ファイルの中身はxmlで、拡張子はplist
です。
ユーザごとの設定は~/Library/LaunchAgents/
に保存され、ログイン時に自動で読み込まれます。
今回はgnu.emacs.daemon.plist
というファイル名を使用するため、設定ファイルは~/Library/LaunchAgents/gnu.emacs.daemon.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>Label</key>
<string>gnu.emacs.daemon</string>
<key>ProgramArguments</key>
<array>
<string>/Applications/Emacs.app/Contents/MacOS/Emacs-x86_64-10_14</string>
<string>--eval</string>
<string>(cd (concat (getenv "HOME") "/"))</string>
<string>--fg-daemon</string>
</array>
<key>StandardOutPath</key>
<string>/tmp/gnu.emacs.daemon.stdout.log</string>
<key>StandardErrorPath</key>
<string>/tmp/gnu.emacs.daemon.stderr.log</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
<key>Crashed</key>
<true/>
</dict>
<key>ServiceDescription</key>
<string>Gnu Emacs for OSX daemon (x86_64)</string>
</dict>
</plist>
以下、各項目の設定について説明していきます。
詳細なフォーマットについてはman launchd.plist
で確認できます。
ラベル
<key>Label</key>
<string>gnu.emacs.daemon</string>
見ての通り、サービスの名称です。launchctl
からstart、stopを行なう際に使用します。
実行対象の指定
<key>ProgramArguments</key>
<array>
<string>/Applications/Emacs.app/Contents/MacOS/Emacs-x86_64-10_14</string>
<string>--eval</string>
<string>(cd (concat (getenv "HOME") "/"))</string>
<string>--fg-daemon</string>
</array>
ここで、実行する対象であるemacsのパス、そして引数を指定します。
以下に各引数について説明していきます。
emacsの実行ファイル
homebrew caskからインストールしたemacsは、rubyスクリプトのラッパー越しに起動しています。
その際にアーキテクチャも確認するようになっていますが、apple silicon macだとここでハネられてしまい起動しません。
そこで、
<string>/Applications/Emacs.app/Contents/MacOS/Emacs-x86_64-10_14</string>
と書いて直接emacsのx86バイナリを指定すれば、rosetta2上で何事もなかったかのように動かすことができます。
ついでに起動時に$HOME
にcdしてほしい
<string>--eval</string>
<string>(cd (concat (getenv "HOME") "/"))</string>
このオプションを加えずに実行すると、emacsは/
ディレクトリで起動してしまい、いちいち移動するのも手間です。そこで--eval
オプションから続く式で、起動時に$HOME
へ移動しておくように設定します。
さらに、launchctl start
、launchctl stop
させたい
どうやら--daemon
やら--bg-daemon
のオプションでは、launchd
から立ち上がりバックグラウンドに移行するために子プロセスを立ち上げた後、せっかく呼んだ親が死んでしまってlaunchctl
から生死がわからなくなってしまいます。
これを回避するため、--fg-daemon
を指定しておきます。
stderr
、stdout
の出力先
<key>StandardOutPath</key>
<string>/tmp/gnu.emacs.daemon.stdout.log</string>
<key>StandardErrorPath</key>
<string>/tmp/gnu.emacs.daemon.stderr.log</string>
stdout
、stderr
の出力先を/tmp
以下に作成したファイルに指定します。
ログの確認などは指定したこれらのファイルから。
読み込みと同時に起動
<key>RunAtLoad</key>
<true/>
これをtrue
とするとlaunchd
に読み込みと同時にサービスを立ちあげます。
~/Library/LuanchAgents
への配置とあわせれば、ログインと同時にemacs daemonが起動します。
クラッシュ時などの挙動を設定
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
<key>Crashed</key>
<true/>
</dict>
基本的にemacs daemonには動作を維持してほしいのでKeepAlive
を指定、終了時のふるまいをdict
キーの中で設定していきます。
SuccessfulExit
での指定は見ての通り正常終了時の挙動です。今回はfalse
としています。
Crashed
での指定はクラッシュ時(exit statusがマイナスの時)の挙動です。
残念ですが、自分の環境だとちょいちょいemacs for osxは落ちてしまうので、自動で復活してもらうためにtrue
にします。
説明
<key>ServiceDescription</key>
<string>Gnu Emacs for OSX daemon (x86_64)</string>
サービスに関する説明です。適当に。
環境変数(主にPATH
)を引き継ぎたい
しかし、これだけだと普段使っているシェルのPATH
を引き継がないため(path helperによるPATH
になる?)、emacsから外部コマンドを実行する際にシェル上から呼び出す場合と異なる挙動をしてしまいます。
そこで、いい感じに環境変数をemacsに渡してくれるpluginがあったため利用しました。
purcell/exec-path-from-shell: Make Emacs use the $PATH set up by the user's shell
use-package
を使っているため、以下のようにinit.el
などに追加します。
PATH
以外の環境変数も追加したい場合は上記のリンクを参考に設定しましょう。
(use-package exec-path-from-shell
:ensure t
:config
(when (daemonp)
(exec-path-from-shell-initialize))
)
ここではdaemon起動時にexec-path-from-shell-initialize
を実行し、PATH
の設定を行ないます。
launchctl
コマンドで設定の読み込み
それでは、~/Library/LaunchAgents/gnu.emacs.daemon.plist
をlaunchctl
に読み込ませてみましょう。
% launchctl load ~/Library/LaunchAgents/gnu.emacs.daemon.plist
launchctl
のサブコマンドについてはここなども参考に。
これでemacs daemonが動いていればemacsclient -c
などで接続してみましょう。
launchctl
からemacs daemonを止める
先ほどのサイトを参考に
% launchctl stop gnu.emacs.daemon
を実行すれば、emacs daemonを停止させることができます。
もちろんemacs上からM-x kill-emacs
を実行してもいいですが、launchctl
から停止させるとemacsが固まってしまったときにも便利です。
また、同様に
% launchctl start gnu.emacs4.osx.daemon
を実行すれば、止めたemacs daemonを再度開始できます。
launchctl
からemacs daemonの状態を確認する
% launchctl list | grep gnu.emacs.daemon
を実行すると
74376 0 gnu.emacs.daemon
のような出力が得られます。
これは、左から順に
- 現在の動いているemacs daemonのPID
- 前回のexit status
となります。
もし現在emacs daemonが停止している場合、PIDには-
と表示されます。
もっと詳細な情報が欲しい場合は、grep
せず直接ラベルを指定します。
% launchctl list gnu.emacs.daemon
これによって、以下のような出力が得られます。
{
"StandardOutPath" = "/tmp/gnu.emacs.daemon.stdout.log";
"LimitLoadToSessionType" = "Aqua";
"StandardErrorPath" = "/tmp/gnu.emacs.daemon.stderr.log";
"MachServices" = {
"org.gnu.Emacs.ServiceProvider" = mach-port-object;
};
"Label" = "gnu.emacs4osx.daemon";
"OnDemand" = true;
"LastExitStatus" = 0;
"PID" = 74376;
"Program" = "/Applications/Emacs.app/Contents/MacOS/Emacs-x86_64-10_14";
"ProgramArguments" = (
"/Applications/Emacs.app/Contents/MacOS/Emacs-x86_64-10_14";
"--eval";
"(cd (concat (getenv "HOME") "/"))";
"--fg-daemon";
);
"PerJobMachServices" = {
"com.apple.tsm.portname" = mach-port-object;
"com.apple.coredrag" = mach-port-object;
"com.apple.axserver" = mach-port-object;
};
};
オチ
さて、ここまでいろいろ書いてきましたが、ふと
「Apple Silicon macネイティブで動くemacsはないの?」
と思い調べてみたらEmacs Plusというものを発見。
説明に従いbrew tap
して導入すると、なんとlaunchctl
用にplist
ファイルも同梱されておりました……。
現在は、起動時の$HOME
へのcd
やKeepAlive
の設定を追加して利用しています。
皆様も良いemacsライフを。
ほんの少し追記 2021/3/6
先日導入したEmacs Plusですが、添付されているplistファイルで指定するemacsの実行ファイルへのパスが
/opt/homebrew/opt/emacs-plus@27/bin/emacs
になっていると思います。
しかしGUIでフレーム描画をする場合、macOSのセキュリティ機能により~/Documents
などのディレクトリへのアクセスが制限されてしまうようです。
そこで、/opt/homebrew/opt/emacs-plus/Emacs.app
ディレクトリのシンボリックリンクを/Applications
ディレクトリ下に張り、plistから指定するemacsを
/Applications/Emacs.app/Contents/MacOS/Emacs
とすれば、~/Documents
ほかのディレクトリへのアクセスが可能になるようです。
(逆に、iTermなど、macOSからアクセス権を付与したターミナルからemacs -nw
で起動した場合、制限は発生しないようです。)