macOSのlaunchctlからemacs daemonを動かしたいでござるの巻


モチベーション

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ファイルの中身です。(こちらを参考に作成)

gnu.emacs.daemon.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で確認できます。

ラベル

gnu.emacs.daemon.plist
<key>Label</key>
<string>gnu.emacs.daemon</string>

見ての通り、サービスの名称です。launchctlからstart、stopを行なう際に使用します。

実行対象の指定

gnu.emacs.daemon.plist
<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だとここでハネられてしまい起動しません。
そこで、

gnu.emacs.daemon.plist
<string>/Applications/Emacs.app/Contents/MacOS/Emacs-x86_64-10_14</string>

と書いて直接emacsのx86バイナリを指定すれば、rosetta2上で何事もなかったかのように動かすことができます。

ついでに起動時に$HOMEにcdしてほしい

gnu.emacs.daemon.plist
<string>--eval</string>
<string>(cd (concat (getenv "HOME") "/"))</string>

このオプションを加えずに実行すると、emacsは/ディレクトリで起動してしまい、いちいち移動するのも手間です。そこで--evalオプションから続く式で、起動時に$HOMEへ移動しておくように設定します。

さらに、launchctl startlaunchctl stopさせたい

どうやら--daemonやら--bg-daemonのオプションでは、launchdから立ち上がりバックグラウンドに移行するために子プロセスを立ち上げた後、せっかく呼んだ親が死んでしまってlaunchctlから生死がわからなくなってしまいます。
これを回避するため、--fg-daemonを指定しておきます。

stderrstdoutの出力先

gnu.emacs.daemon.plist
<key>StandardOutPath</key>
<string>/tmp/gnu.emacs.daemon.stdout.log</string>
<key>StandardErrorPath</key>
<string>/tmp/gnu.emacs.daemon.stderr.log</string>

stdoutstderrの出力先を/tmp以下に作成したファイルに指定します。
ログの確認などは指定したこれらのファイルから。

読み込みと同時に起動

gnu.emacs.daemon.plist
<key>RunAtLoad</key>
<true/>

これをtrueとするとlaunchdに読み込みと同時にサービスを立ちあげます。
~/Library/LuanchAgentsへの配置とあわせれば、ログインと同時にemacs daemonが起動します。

クラッシュ時などの挙動を設定

gnu.emacs.daemon.plist
<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にします。

説明

gnu.emacs.daemon.plist
<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以外の環境変数も追加したい場合は上記のリンクを参考に設定しましょう。

init.el
(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.plistlaunchctlに読み込ませてみましょう。

% 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へのcdKeepAliveの設定を追加して利用しています。

皆様も良い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で起動した場合、制限は発生しないようです。)