Mac を触ったら重いバックグラウンド処理を中断し、しばらく触らなかったら再開するようにする


Mac OS X でバックグラウンド処理（主にネットワークを利用する処理を想定している）を行うのですが、人がキーボードやタッチパッドに触ったら、人間が行なう処理を優先するために、バックグラウンドの処理を一旦停止し、しばらくさわらなかったら処理を再開するスクリプトを書きました。

コードは Gist にあります。 ∥ [Run processes while you do not touch your Mac.](https://gist.github.com/knaka/d44f4b4e301f13314080dbf63b5159aa)

# 動機

Mac に定期的に遠方から rsync でファイルをフェッチさせているのですが、いかんせんうちは田舎で ADSL しか来ておらず、さて作業をするぞとなると偉くネットが遅く、ああ、バックグラウンドで処理をさせていたんだっけ、ということによくなりましたので、UI に触っている間はバックグラウンドの処理を止めて、サクサクと作業ができるようにしたかったのです。

CPU 負荷の高いような処理でも使えるっちゃあ使えるかな？ Mac でそんなワークロードがあるかどうかは分かりませんが。

で、何をしようとしたか（あるいは、何をしなかったのか）を忘れそうなのでメモしました。

# 実行方法

コマンド名は `darwinbg` としています。このスクリプトを置いたのと同じ場所に `darwinbg.d/` ディレクトリを作成しその下に実行可能ファイルを置いておけば、darwinbg を実行するたびに配下の実行ファイルを順次実行します。

ですので、適当な間隔で cron にでも仕掛けておくと良いかと思います。

UI に触れてはならない時間は `idlethresh` に、その確認の間隔は `checkinterval` に、ともに秒で指定します。

そこへ接続している間は処理を実行して欲しくない Wi-Fi の SSID は `badssids` に、同じく処理を実行して欲しくないネットワークサービスは `badnwsvcs` に、それぞれ配列で指定します。後者は、Bluetooth PAN でテザリングをしている間などが相当します。

環境変数 `FORCE` に何かを入れて起動すれば、UI へのタッチがあっても中断しません。

要点としては、そんなところか。

~~~~bash:darwinbg
#!/bin/bash
# -*- coding: utf-8 -*-

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

## Avoid reentrance.
exec 9< $0
perl -mFcntl=:flock -e "open(LOCK,'<&=9');exit(!flock(LOCK,LOCK_EX|LOCK_NB))" || exit 0

## Exit for error or undefined.
set -eu

idlethresh=$((60 * 5))
checkinterval=10
# idlethresh=5
# checkinterval=5
## Do not start nor continue with these SSIDs.
badssids=("Too Slow", "Too Expensive")
## Do not start nor continue with these network services.
badnwsvcs=("Bluetooth PAN")

contains () {
  local e
  for e in "${@:2}"
  do
    test "$e" == "$1" && return 0
  done
  return 1
}

function badnetworkp() {
  local ssid
  local badnwsvc
  ssid=$(/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -I | sed -n -e "s/^ *SSID: //p")
  if test -n "$ssid" && contains "$ssid" "${badssids[@]}"
  then
    echo "Bad Wi-Fi is active."
    return 0
  fi
  for badnwsvc in "${badnwsvcs[@]}"
  do
    if test -n "$(/usr/sbin/networksetup -getinfo "Bluetooth PAN" | sed -ne s/"^IP address: //p")"
    then
      echo "Bad network service is active."
      return 0
    fi
  done
  return 1
}

if badnetworkp
then
  echo "Network connection is bad one. Exiting."
  exit 0
fi

: ${FORCE:=""}

## Get UI idle time in second.
function idlesec() {
  test -n "$FORCE" && echo 86400 && return
  echo $(($(ioreg -c IOHIDSystem | grep HIDIdleTime | head -n 1 | sed 's/.* = //') / 1000000000))
}

## Stop for a while if UI is recently used.
echo Waiting.
while :
do
  test "$(idlesec)" -ge "$idlethresh" && break
  sleep $checkinterval
done

## Get child process IDs.
function children() {
  for pid in $(pgrep -P $1)
  do
    echo $pid
    children $pid
  done
}

## Send signal to all child processes.
function killtree() {
  kill -s $1 $pid $(children $2) && :
}

## Absolute path of this script.
self="$(p="$0";while :;do t="$(readlink "$p")";cd "$(dirname "$p")";test -z "$t"&&break||p="$t";done;echo "$(pwd -P)/$(basename "$p")")"

pid=

function onexit() {
  trap - SIGTERM # Reset to default handler
  kill -- -$$ && : # Not option but process group
  ## command line - What is a stopped process in linux? - Super User http://superuser.com/questions/403200/what-is-a-stopped-process-in-linux
  kill --CONT -- -$$ && :
}

# function onexit() {
#   test -n "$pid" && killtree TERM "$pid" || :
# }

trap onexit SIGINT SIGTERM EXIT

## Run all the executables under $self.d/ sequently.
for file in $self.d/*
do
  base=$(basename "$file")
  test "${base:0:1}" = "_" && continue
  echo "$base" | grep -q -e "~" -e "#" && continue
  $file &
  pid=$!
  while :
  do
    if badnetworkp
    then
      echo Network connection has been changed to bad one. Exiting.
      killtree TERM $pid
      wait
      exit 0
    fi
    stat="$(ps auxww $pid | tail -n +2 | awk '{print $8}' | sed -e 's/\(.\).*/\1/')"
    test -z "$stat" && pid="" && break
    case $stat in
      R | S | D ) # Running
        if test "$(idlesec)" -lt "$idlethresh"
        then
          killtree STOP $pid
          echo "Stopped subprocess(es)."
        fi
        ;;
      T ) # Stopped
        if test "$(idlesec)" -ge "$idlethresh"
        then
          killtree CONT $pid
          echo "Restarted subprocess(es)."
        fi
        ;;
    esac
    sleep $checkinterval
  done
done
~~~~

# やらなかったこと＆やめたこと

* 停止・再開を別プロセスから行う。
	* 処理本体と、処理の停止・再開を行うプロセスを当初は別にしておいて双方を cron で起動していたのですが、よく考えたら必要が無かったののでやめました
	* ただしこの方法であれば、本処理とその制御とを別コマンドにできるので、それはそれでメリットなんですよね。やや複雑になりますが。それについては、右記に書きました。 ∥ [cron から起動されたジョブを、一時停止・再開する - Qiita](http://qiita.com/knaka/items/a98b877981c952f1a8aa)
* 定期的なタイマー割り込みで、処理の停止・再開の要不要のチェックをする。
	* USR1 をチマチマと発行してそのたびにチェックをする仕組みもやりかけたのですが、これまたよく考えたら不要だったのでやめました。

# できれば

早くうちへも、高速な光ケーブルが来てくれるといいんですけどね。