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_configStreamLocalBindUnlink yesStreamLocalBindMask 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に通知を送れるようになる。

終わりだよ~

Hugoに前後記事のリンクを追加する デレステ: LIVE Carnival 202401 Rank SS突破編成メモ