きっかけ
弊大学では学内ネットワークとして,学生が自分の持つアカウントで認証すれば使用できる認証付き無線LANが用意されています.ありがたいことに「自動プロキシ検出」さえオンにしていれば自動で設定されるため苦労はしませんが,自宅や研究室などプロキシの存在しない環境においてこの設定をしているとアプリケーションの通信に影響が出ることがあります(下記記事参照)
macOSには「ネットワーク環境」という設定があり,環境に応じて設定を一括で変更できるプロファイル変更機能が備わっているのですが,自動で切り替わるとかそいう頭の良いことをしてくれません.これをもっと簡単に,そして自動でやりたいなと思い最初は自作し始めたのですが良いツールを見つけたのでやめました.
Hammerspoonの紹介
Hammerspoonは上記ページにあるとおり,luaを用いて自分でロジックを書いて様々な動作を自動化するためのソフトウェアです.hs
というモジュールにhammerspoonによる独自の拡張が沢山含まれており,非常に幅広い動作を実現しています.
luaは簡単な言語ですが色々と癖があるので,慣れていないと引っかかるところがたまにあります.
環境
インストールは以下から可能です.
使用する機能
WiFi切り替え検知
実はこれは既に公式ページのGetting Startedに書いてあります.
しかしこのサンプルは一つのSSIDについて接続切断を見ているのみで,複数のSSIDについて接続・切断に対応させようと思うと割と面倒です.加えて言うと,新しいSSIDに対応させようとする度にこの~/.hammerspoon/init.lua
を編集して追加しないといけないとなると,やがて肥大化してパンクすることが目に見えています.
とはいえ基本は上記ページの hs.wifi.watcher
を使用します.
シェルスクリプト実行
hs.execute()
というメソッドによりシェルスクリプト実行が可能です.また,launchd
などを用いて実行させる場合などはパスが/usr/bin:/bin:/usr/sbin:/sbin
にしか通っていないのですが,先述のメソッドの第二引数にtrue
を渡すとどうも~/.bashrc
を読み込んでから実行してくれるようです*1.
ただしその場合はオーバーヘッドが大きいため,オプションにより制御できるようにしたいと思います.
実装
ちゃんと色々検証したわけではないので自己責任でお願いします.
構成
そこで下記のような構成にしたいと思います
~/.hammerspoon/config.json
を作成~/.hammerspoon/Scripts/
以下に実行するシェルスクリプトを格納init.lua
内で上記のjsonをロード(初回起動時および変更時のみ)- SSIDの変更を検知してネットワーク環境の変更およびシェルスクリプトの実行を行う
config.json
下記のようにし,~/.hammerspoon/
以下に配置します.
{ "base": { "notify": false, "shell": "/bin/bash", "with_user_env": true }, "networks": [ { "SSID": "ssid1", "profile": "proxy", "pre_hook": "hoge.sh", "post_hook": "fuga.sh" }, { "SSID": "ssid2", "pre_hook": "poyo.sh", "post_hook": "piyo.sh" } ] }
notify
: 通知を有効化するフラグ.今はconfig.iniを読み込むときに通知を出している.デフォルトはtrue
shell
: シェルスクリプトを実行するシェル.デフォルトは/bin/bash
with_user_env
:hs.execute()
の第二引数に渡す値.ユーザが定義したパスを読み込んだ状態で実行するかどうか.デフォルトはtrue
config.iniに変更があったときに自動で再読み込み
~/.hammerspoon/init.lua
に記述していきます.
-- init HOME = os.getenv("HOME") CONFIG_HOME = HOME .. "/.hammerspoon/" CONFIG_FILE = CONFIG_HOME .. "config.json" -- key NETWORKS_KEY = "networks" SHELL_KEY = "shell" BASE_CONFIG_KEY = "base" NOTIFY_KEY = "notify" USER_ENV_KEY = "with_user_env" SSID_KEY = "SSID" PROFILE_KEY = "profile" PRE_HOOK_KEY = "pre_hook" POST_HOOK_KEY = "post_hook" -- default settings NOTIFY = true SHELL = "/bin/bash" USER_ENV = true DEFAULT_NW_PROFILE = "Automatic" function hasKey(tbl, key) for k, v in pairs (tbl) do if k == key then return true end end return false end function readConfig(files) -- jsonファイルを開いて一行ずつ読み出し,decode local fp = io.open(files[1], "r") local json = fp:read("*a") fp:close() local config = hs.json.decode(json) if hasKey(config, BASE_CONFIG_KEY) then for k, v in pairs(config[BASE_CONFIG_KEY]) do hs.settings.set(k, v) end end -- Default valueをセット if not hasKey(config, NOTIFY_KEY) then hs.settings.set(NOTIFY_KEY, NOTIFY) end if not hasKey(config, SHELL_KEY) then hs.settings.set(SHELL_KEY, SHELL) end if not hasKey(config, USER_ENV_KEY) then hs.settings.set(USER_ENV_KEY, USER_ENV) end -- Networkプロファイルをロード if hasKey(config, NETWORKS_KEY) then hs.settings.set(NETWORKS_KEY, config[NETWORKS_KEY]) end if hs.settings.get(NOTIFY_KEY) then local notify_msg = string.format("display notification \"Reloaded\" with title \"WLAN Profiles\"") hs.osascript.applescript(notify_msg) end end -- exec readConfig({CONFIG_FILE}) hs.pathwatcher.new(CONFIG_FILE, readConfig):start()
Hammerspoonではhs.settings
というキーバリューストア的なものを提供していたのでこれを使えと言うことなのかと,動的なデータのほとんどはこれを介して出し入れしています*2.
ネットワーク環境切り替え
これはシェルスクリプト(nw_profile_changer.sh
)を置いておくことにします.
こちらからアイデアを頂きました.
currentlocation=`networksetup -getcurrentlocation` if test $currentlocation = $1; then return fi scselect `scselect | grep ${1} | cut -b 4-40`
これを~/.hammerspoon/Scripts
以下に置いておきます.
WiFi切り替え時に実行
SCRIPTS_PATH = CONFIG_HOME .. "Scripts/" PROFILE_CHANGE_SCRIPT = SCRIPTS_PATH .. "nw_profile_changer.sh" -- tmp prevSSID = hs.wifi.currentNetwork() -- シェルスクリプトを実行する関数 function execHook(path, profile) if path then local script = SCRIPTS_PATH .. path local shell = hs.settings.get(SHELL_KEY) local with_user_env = hs.settings.get(USER_ENV_KEY) hs.execute(shell .. " " .. script, with_user_env) end end -- ネットワーク環境を切り替える関数 function loadNwProfile(profile_name) local command = nil local shell = hs.settings.get(SHELL_KEY) local with_user_env = hs.settings.get(USER_ENV_KEY) if profile_name then command = shell .. " " .. PROFILE_CHANGE_SCRIPT .. " " .. profile_name else command = shell .. " " .. PROFILE_CHANGE_SCRIPT .. " " .. DEFAULT_NW_PROFILE end hs.execute(command, with_user_env) end function nwProfileChanger(ssid, profile, connection) if connection then -- 接続時にプロファイルを変更 loadNwProfile(profile[PROFILE_KEY]) if hasKey(profile, PRE_HOOK_KEY) then execHook(profile[PRE_HOOK_KEY]) end else if hasKey(profile, POST_HOOK_KEY) then execHook(profile[POST_HOOK_KEY]) end end end function ssidChangedCallback(watcher, msg, interface) local newSSID = hs.wifi.currentNetwork() if prevSSID ~= newSSID then for i, nw in ipairs(hs.settings.get(NETWORKS_KEY)) do if not prevSSID and newSSID and nw[SSID_KEY] == newSSID then -- 接続時 nwProfileChanger(newSSID, nw, true) break elseif prevSSID and not newSSID and nw[SSID_KEY] == prevSSID then -- 切断時 nwProfileChanger(prevSSID, nw, false) break end end end prevSSID = newSSID end -- SSID変更を監視 hs.wifi.watcher.new(ssidChangedCallback):start()
解説は省きますが,SSID変更を検知してそのSSIDに合致する設定が書かれていた場合,ネットワーク環境変更およびシェルスクリプト実行を行います.
全体は下記にあります.
任意のWiFi接続・切断時に任意のシェルスクリプトを実行するHammerspoonの設定 · GitHub
Hammerspoonをリロード
メニューバーまたはコンソールからReload Config
します.
これで準備は完了です!
応用
研究室のWiFiに接続する度に論文提出までの期限を通知
このためだけにGoで超簡単に書きました.
下記のようなシェルスクリプトを用意し,~/.hammerspoon/when_connect_lab.sh
として配置します.
limit=`go-sotsuron-counter` osascript -e 'display notification "'"${limit}です"'" with title "Welcome to Laboratory"'
config.json
に記述
{ "base": { "notify": false, "shell": "/bin/bash", "with_user_env": true }, "networks": [ { "SSID": "ラボのSSID", "pre_hook": "when_connect_lab.sh" } ] }
はい!こうなります!
思わず画面をたたき割らないように注意しましょう.
gitのプロキシ設定を行う
こういう用途が本命です. プロキシのオンオフスクリプトをそれぞれ書きます.
~/.hammerspoon/proxy_on.sh
# git git config --global http.proxy http://hoge.com:8080 git config --global https.proxy https://hoge.com:8080 # その他 launchctl setenv HTTP_PROXY http://hoge.com:8080 launchctl setenv http_proxy http://hoge.com:8080 launchctl setenv HTTPS_PROXY https://hoge.com:8080 launchctl setenv https_proxy https://hoge.com:8080 launchctl setenv ALL_PROXY http://hoge.com:8080 launchctl setenv all_proxy http://hoge.com:8080
~/.hammerspoon/proxy_off.sh
[2018/1/30 追記]
[http]
と[https]
セクションが.gitconfigに大量に追記されてしまう問題を修正しました
# git git config --global --unset http.proxy git config --global --unset https.proxy git config --global --remove-section http git config --global --remove-section https # その他 launchctl unsetenv HTTP_PROXY launchctl unsetenv http_proxy launchctl unsetenv HTTPS_PROXY launchctl unsetenv https_proxy launchctl unsetenv ALL_PROXY launchctl unsetenv all_proxy
config.json
に記述
{ "base": { "notify": false, "shell": "/bin/bash", "with_user_env": true }, "networks": [ { "SSID": "ProxyありのSSID", "profile": "プロキシ設定したネットワーク環境名", "pre_hook": "proxy_on.sh", "post_hook": "proxy_off.sh" } ] }
export
ではなくlaunchctl setenv
するのがミソです.
適当なWiFiで設定してみて実際に動くか確かめると良いと思います.
デバッグ目的だけならosascript -e 'display notification "hoge" with title "test"'
とか書くと実行された場合に通知が出て良いです.
課題
- 突貫で書いたので雑
- たまにjson読み込みでエラーを吐く(
init.lua
再読込で直る).原因不明.
2018-01-16 16:53:19: ******** 2018-01-16 16:53:19: 16:53:19 ERROR: LuaSkin: hs.pathwatcher callback error: The data couldn窶冲 be read because it isn窶冲 in the correct format. stack traceback: [C]: in function 'hs.json.internal.decode' /Users/pudding/.hammerspoon/init.lua:42: in function 'readConfig' 2018-01-16 16:53:19: ********
まとめ
自動で設定できるとはいえ,例えばシェルは開き直さないと環境変数が反映されないとかそういうのがあるので,移動時にちゃんと後始末しない(ウィンドウ開けたら開けっぱなし)な人は気をつけないとつらくなります.
…卒論書きます.