macOS Big Sur、および執筆時点のSelf Serviceの最新バージョンである 10.26.0 において、Self Serviceアプリケーションがクラッシュして起動できない事象が発生しています。

@rotomxさんがmacOS Big SurでJamf ProのSelf Serviceがクラッシュする問題の暫定対応の記事で原因と解決方法を書かれているので、詳しくはそちらを参照してください。

こちらでは解決方法として、Self Serviceアプリケーションがクラッシュしたら~/Library/Application Support/com.jamfsoftware.selfservice.mac/CocoaAppCD.storedataのファイルを都度消すという方法が取られています。

しかし、都度消すのは面倒ですので、今回はこの作業から解放されるべく対応を自動化する方法を紹介します。

暫定対応の自動化

以下のスクリプトのLAUNCH_AGENT_IDに任意のID(例えば、io.github.kenchan0130.SelfServiceRecover)を設定して実行すると暫定対応が完了します。

もちろんこのスクリプトはJamf Pro経由でも使用できます。 スクリプトを登録し、登録したスクリプトをアサインしたポリシーを配布するだけです。

#!/bin/zsh

export PATH=/usr/bin:/bin:/usr/sbin:/sbin

LAUNCH_AGENT_ID="YOUR_AGENT_ID"
DEPLOY_FOR_ALL_USERS=false # You can set 'true' if you want to deploy agent for every users
IS_REMOVE=false
APPLICATION_NAME="Self Service" # If you have changed your Self Service application name, please change this value
THROTTLE_INTERVAL="10" # 10 or more is recommended

# Functions

run_as_user() {
  local uid
  if [[ "$(whoami)" == "${CURRENT_USER}" ]];then
    "$@"
  else
    uid=$(id -u "${CURRENT_USER}")
    launchctl asuser "${uid}" sudo -u "${CURRENT_USER}" "$@"
  fi
}

print_info_log(){
  local timestamp
  timestamp=$(date +%F\ %T)

  echo "$timestamp [INFO] $1"
}

print_error_log(){
  local timestamp
  timestamp=$(date +%F\ %T)

  echo "$timestamp [ERROR] $1"
}

# Main

if [[ "${1}" = "/" ]];then
  # Jamf uses sends '/' as the first argument
  print_info_log "Shifting arguments for Jamf."
  shift 3
fi

if "${1}";then
  print_info_log "Override 'DEPLOY_FOR_ALL_USERS' as ${1}..."
  DEPLOY_FOR_ALL_USERS="${1}"
fi

if [[ "${2}" ]];then
  IS_REMOVE=true
fi

if "${DEPLOY_FOR_ALL_USERS}" && [[ "$(whoami)" != "root" ]];then
  print_error_log "This script needs to be run with root privileges when 'DEPLOY_FOR_ALL_USERS' is enabled."
  exit 1
fi

CURRENT_USER=$(stat -f%Su /dev/console)
LAUNCH_AGENT_PLIST_PATH="/Library/LaunchAgents/${LAUNCH_AGENT_ID}.plist"

if ! "${DEPLOY_FOR_ALL_USERS}";then
  if [[ ! "${CURRENT_USER}" ]];then
    print_error_log "No one is logged in. Either run it after the user logs in, or set 'DEPLOY_FOR_ALL_USERS' variable of this script to true and distribute it to all users."
    exit 1
  fi

  CURRENT_USER_HOME_DIRECTORY=$(dscl /Local/Default read "/Users/${CURRENT_USER}" NFSHomeDirectory | awk '{print $2}')
  # Override LAUNCH_AGENT_PLIST_PATH
  LAUNCH_AGENT_PLIST_PATH="${CURRENT_USER_HOME_DIRECTORY}${LAUNCH_AGENT_PLIST_PATH}"
fi

if "${IS_REMOVE}";then
  print_info_log "Removing ${LAUNCH_AGENT_ID} agent..."
  if [[ ! "${CURRENT_USER}" ]];then
    run_as_user launchctl remove "${LAUNCH_AGENT_ID}"
  fi
  rm -rf "${LAUNCH_AGENT_PLIST_PATH}"
  print_info_log "Removed ${LAUNCH_AGENT_ID} agent."

  exit 0
fi

print_info_log "Deploying ${LAUNCH_AGENT_PLIST_PATH} file..."

cat > "${LAUNCH_AGENT_PLIST_PATH}" <<XML
<?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>${LAUNCH_AGENT_ID}</string>
  <key>ProgramArguments</key>
  <array>
    <string>/bin/sh</string>
    <string>-c</string>
    <string>ps axc | grep "${APPLICATION_NAME}" || rm -rf "\${HOME}/Library/Application Support/com.jamfsoftware.selfservice.mac/CocoaAppCD.storedata" &amp;&amp; sleep "${THROTTLE_INTERVAL}"</string>
  </array>
  <key>ThrottleInterval</key>
  <integer>${THROTTLE_INTERVAL}</integer>
  <key>WatchPaths</key>
  <array>
    <string>~/Library/Application Support/com.jamfsoftware.selfservice.mac/CocoaAppCD.storedata</string>
  </array>
  <key>KeepAlive</key>
  <true/>
</dict>
</plist>
XML

print_info_log "Deployed ${LAUNCH_AGENT_ID} agent."

if ! "${DEPLOY_FOR_ALL_USERS}";then
  CURRENT_USER_GROUP=$(stat -f "%Sg" "${CURRENT_USER_HOME_DIRECTORY}")
  chown "${CURRENT_USER}:${CURRENT_USER_GROUP}" "${LAUNCH_AGENT_PLIST_PATH}"
fi

if [[ "${CURRENT_USER}" ]];then
  run_as_user launchctl unload "${LAUNCH_AGENT_PLIST_PATH}" &>/dev/null
  run_as_user launchctl load "${LAUNCH_AGENT_PLIST_PATH}"
  print_info_log "Launched ${LAUNCH_AGENT_ID} agent by ${CURRENT_USER}."
fi

exit 0

DEPLOY_FOR_ALL_USERS変数をtrueに設定するとすべてのユーザー、falseだと現在のユーザーにのみ適用されます。 コマンドライン経由の場合は、第1引数、Jamf Pro経由の場合は、第4引数がこれに該当します。

もし、Jamf ProサーバーのSelf Serviceの設定のApplication Nameの項目を変更している場合は、APPLICATION_NAMEの変数を変更してください。

Jamf Proの設定のmacOSのSelf ServiceにおけるApplication Nameの設定項目

暫定対応を外す

この問題が解消したら、暫定対応を外すことが考えられます。 プログラムの内容的にそこまで悪さをするものではないですが、無駄なプロセスは動かしたくはないものです。

暫定対応を外したい場合は、IS_REMOVE変数をtrueにして実行します。 コマンドライン経由の場合は、第2引数、Jamf Pro経由の場合は、第5引数がこれに該当します。

スクリプトの解説

これで終わってしまうのは短いので、スクリプトの処理を解説します。

やっていることの概要は、「macOSに搭載されているlaunchdの機能(Agent)を使用して、原因のファイルが作られたら削除する」です。

Agentの設定ファイルの置き場所を決める

launchdには、

  • Daemon
    • OS起動時に、PID 1(つまりroot権限であると考えて良い)のlaunchdによって起動されるプログラム
    • GUIが使えない
  • Agent
    • ユーザー権限で起動するlaunchdによって起動されるプログラム

の2つがあります。

今回はroot権限で動作させる必要はないので、Agentを使用します。

また、Agentの設定ファイルは

  • ~/Library/LaunchAgents
    • 各ユーザーが管理する各ユーザーごとに実行するAgentの設定ファイル置き場
  • /Library/LaunchAgents
    • 管理者が管理する各ユーザーごとに実行するAgentの設定ファイル置き場
  • /System/Library/LaunchAgents
    • OSが管理する各ユーザーごとに実行するAgentの設定ファイル置き場
    • 基本的にはいじらないところ

の3箇所に置かれます。

このスクリプトでは、

LAUNCH_AGENT_PLIST_PATH="/Library/LaunchAgents/${LAUNCH_AGENT_ID}.plist"

if ! "${DEPLOY_FOR_ALL_USERS}";then
  if [[ ! "${CURRENT_USER}" ]];then
    print_error_log "No one is logged in. Either run it after the user logs in, or set 'DEPLOY_FOR_ALL_USERS' variable of this script to true and distribute it to all users."
    exit 1
  fi

  CURRENT_USER_HOME_DIRECTORY=$(dscl /Local/Default read "/Users/${CURRENT_USER}" NFSHomeDirectory | awk '{print $2}')
  LAUNCH_AGENT_PLIST_PATH="${CURRENT_USER_HOME_DIRECTORY}${LAUNCH_AGENT_PLIST_PATH}"
fi

のように、DEPLOY_FOR_ALL_USERS変数がfalseの場合は~/Library/LaunchAgentstrueの場合は/Library/LaunchAgentsにファイルを設置するようにしています。

Agentの設定ファイルを定義

設定ファイルを置く場所が決まったので、次に設定ファイルを定義します。 この設定ファイルはプロパティリスト(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>${LAUNCH_AGENT_ID}</string>
  <key>ProgramArguments</key>
  <array>
    <string>/bin/sh</string>
    <string>-c</string>
    <string>ps axc | grep "${APPLICATION_NAME}" || rm -rf "\${HOME}/Library/Application Support/com.jamfsoftware.selfservice.mac/CocoaAppCD.storedata" &amp;&amp; sleep "${THROTTLE_INTERVAL}"</string>
  </array>
  <key>ThrottleInterval</key>
  <integer>${THROTTLE_INTERVAL}</integer>
  <key>WatchPaths</key>
  <array>
    <string>~/Library/Application Support/com.jamfsoftware.selfservice.mac/CocoaAppCD.storedata</string>
  </array>
  <key>KeepAlive</key>
  <true/>
</dict>
</plist>

WatchPathsで、~/Library/Application Support/com.jamfsoftware.selfservice.mac/CocoaAppCD.storedataファイルの変更を監視しています。

監視に引っかかった場合、ProgramArgumentsで定義されている以下のプログラムが起動します。

/bin/sh -c ps axc | grep "設定したアプリケーション名" || rm -rf "${HOME}/Library/Application Support/com.jamfsoftware.selfservice.mac/CocoaAppCD.storedata" && sleep "設定したThrottle Interval"

一見単純そうなスクリプトですが、

  1. /bin/sh -c
  2. ps xc | grep "設定したアプリケーション名" ||
  3. &amp;
  4. sleep "設定したThrottle Interval"

の4つのポイントがあります。

1つ目は、/bin/sh -cです。

launchdProgramArgumentsのトップレベルではデフォルトで定義されている環境変数が直接使用できません。 そのため、/bin/sh -cとして、一度shをかますことで、デフォルトで定義されている$HOMEという環境変数を使えるようにしています。

2つ目は、ps xc | grep "設定したアプリケーション名" ||です。

Self Serviceのアプリケーションが起動中にファイルを消すと、アプリケーションに何かしらの不具合などが出る可能性があるため、Self Serviceのアプリケーションが起動していないときだけ該当のファイルを削除するようにしています。

3つ目は、&amp;です。

プロパティリストの場合、&&amp;にエスケープしないと期待通りに動作しません。

最後は、sleep "設定したThrottle Interval"です。

launchdの特徴として、即座に終了する処理が実行された場合、そのAgentは失敗したととらえられることがあります。 そのため、監視の繰り返し時間であるThrottleIntervalと同じ時間sleep処理を挟むようにしています。

Agentの起動

Agentの設定ファイルの置き場に設定ファイルを設置すると、ユーザーがログインした際に自動でAgentが起動します。

しかし、このスクリプトが実行された際に、すでにユーザーがログインしている場合は、Agentは起動されません。 そのため、

if [[ "${CURRENT_USER}" ]];then
  run_as_user launchctl unload "${LAUNCH_AGENT_PLIST_PATH}" &>/dev/null
  run_as_user launchctl load "${LAUNCH_AGENT_PLIST_PATH}"
  print_info_log "Launched ${LAUNCH_AGENT_ID} agent by ${CURRENT_USER}."
fi

のように、明示的にAgentを起動するようにしています。

終わりに

macOS Big SurでSelf Serviceアプリケーションがクラッシュしてしまう問題の暫定対応の自動化する方法を紹介しました。

もちろんこんなことをぜずに済むに越したことはないのですが、根本はSelf Serviceアプリケーションの問題ですので、Jamf社の対応に期待しましょう。