WSLやリモートのLinuxからWindowsにトースト通知を送る
目次
WSL2やSSHで長時間かかるようなタスクをやっていると、ちょくちょく見に行くことで他の作業に集中できなかったり逆にやったことを忘れてて終わったのに何時間も気づかなかったりする。そこでタスクが終わったらWindows側にトースト通知を送るようにして終わったら気づけるようにする。
WSLから通知を送る
WSLから通知を送る方法を作ってほしいというIssueは2017年には立っているが今でもOpen状態になっている。一方、そのIssueのコメントでBurntToastを使う方法が紹介されていたので実装してみる。
BurntToastはInstall-Module
でインストールする。なぜかpwsh.exe
だとだめだったので昔ながらのpowershell.exe
を使う。
1Install-Module BurntToast
使えるか確かめる。権限エラーが出る場合はRemoteSigned
とかにする。
1New-BurntToastNotification -Text 'テスト'
通知できたら、WSLから通知できるかも確認する。日本語も使えるはず。
1powershell.exe -ExecutionPolicy RemoteSigned -Command "New-BurntToastNotification -Text 'テスト'"
これをスクリプト化して、パス通ったところに置く。この時点ではalias
でもいいけど後のためにbashスクリプトで書く。
1#!/bin/bash
2powershell.exe -ExecutionPolicy RemoteSigned -Command "New-BurntToastNotification -Text '$1'"
時間かかるコマンドの後にnotify-win.sh
を呼び出せば通知してくれる。
1sleep 100; notify-win.sh '終わりだよ~'
SSH先から通知を送る
今度はSSH先から通知する方法を考える。ポートを開けてlistenしてもいいが、SSHできる前提なのでソケット転送を用いることにする。せっかくなのでポートを消費しないで済むUNIXドメインソケット転送にする。SSHのUNIXドメインソケット転送ではstream限定らしくdatagramではうまく行かなかった。事前準備として、SSHサーバーにログインシェルがnologin
なユーザーを作成し、そのユーザーにパスワードなしのSSH暗号鍵でログインできるようにしておく(要はパスワードなしでソケット転送だけできればいいので、安全のためシェルに入れないようにする)。WSL側のssh_config
は他のと混ざらないように~/.ssh/config_notify
みたいなファイルに分けて定義する。このファイルは以下のサーバースクリプトからも読み取る。また、SSHサーバーのsshd_config
にStreamLocalBindUnlink yes
とStreamLocalBindMask 0111
を設定する。
送受信にはsocat
を使う。最近知ったが便利。受信側(WSL)ではunix-listen
でstreamなUNIXドメインソケットを作成して受信した文字列をnotify-win.sh
に渡す。これによって受信した文字列を通知することができる。ssh
のオプションはそれぞれ-f
がバックグラウンド化、-N
がコマンドを実行しない、-F
がコンフィグファイル指定、-o ConnectTimeout
はタイムアウト指定、-R
はソケットファイルの組み合わせ設定(:
の左がリモート側、右がローカル側)、-S
と-M
はコントロールソケットを使って制御できるようにする、という意味になる。バックグラウンドにすると終了できなくなる問題をコントロールソケットを使うことによって解決する手法は、StackExchangeの質問の回答を使った。このサーバースクリプトをお好みの方法で(systemdとかなかったりするので…)自動または任意で起動する。
1#!/bin/bash
2
3NOTIFY_SEND_SOCK='/tmp/notify-send.sock'
4NOTIFY_RECV_SOCK='/tmp/notify-recv.sock'
5CLIENT_SSH_CONFIG="${HOME}/.ssh/config_notify"
6
7open-sockets() {
8 grep -i '^host\s' "${CLIENT_SSH_CONFIG}" | sed 's/^host\s*//gi' | while read -r host; do
9 nohup ssh -fN \
10 -F "${CLIENT_SSH_CONFIG}" \
11 -o 'ConnectTimeout 5' \
12 -R "${NOTIFY_SEND_SOCK}:${NOTIFY_RECV_SOCK}" \
13 -S "/tmp/notify-ssh-${host}.control" -M \
14 "${host}" >/dev/null 2>&1 &
15 done
16}
17
18close-sockets() {
19 grep -i '^host\s' "${CLIENT_SSH_CONFIG}" | sed 's/^host\s*//gi' | while read -r host; do
20 ssh -n -S "/tmp/notify-ssh-${host}.control" -O exit "${host}"
21 done
22}
23
24trap close-sockets EXIT
25
26open-sockets
27
28socat "unix-listen:${NOTIFY_RECV_SOCK},fork,mode=666" stdout | while read -r line; do
29 notify-win.sh "${line}"
30done
notify-win.sh
を改造して、ソケットファイルの有無によってpowershell.exe
を呼ぶかソケットに送るか判定する。socat
でstreamなUNIXドメインソケットに送るにはunix-connect
を使う。また、上のnotify-win.sh
は入力文字列を';
とかから始めたらインジェクションし放題なのでちょっと対策をする(自分しか使わないけど)。具体的にはpowershell.exe
コマンドの中に引数を埋め込むのではなく、標準入力から受け取るようにする。埋め込む場合はエンコード変換いらないけど標準入出力を使う場合はnkf
などでエンコード変換する必要がある。ついでにRead-Host
を使うとエコーされてしまうので/dev/null
にリダイレクトする。
1#!/bin/bash
2
3NOTIFY_SEND_SOCK='/tmp/notify-send.sock'
4NOTIFY_RECV_SOCK='/tmp/notify-recv.sock'
5
6if [ -S "${NOTIFY_SEND_SOCK}" ] && [ ! -S "${NOTIFY_RECV_SOCK}" ]; then
7 echo "${1}" | socat - "unix-connect:${NOTIFY_SEND_SOCK}"
8else
9 echo "${1}" | nkf -sc | powershell.exe -ExecutionPolicy RemoteSigned -Command 'New-BurntToastNotification -Text (Read-Host)' >/dev/null
10fi
これでSSH先にも同じnotify-win.sh
を置いておけば同じコマンドで自分のWindowsに通知を送れるようになる。