リモートマシンからGrowl通知をできるようにしようとして挫折した話

Growl大好きなので色んなところから通知出来るようにしたい、と常々思っておりまして。
ふと、「sshで入って作業しているリモートマシンで時間かかるコマンド実行したときに終了通知をGrowlで受けたいじゃん?」と。
ちょろっと調べてみると以下のエントリが。

前者はGrowlのNetwork通知を使うもの。UDP 9887番ポートで受け取って通知させることができるので、sshポートフォワードとUDP-TCP変換を使ってリモートのUDPをローカルのUDPに持ってくる。後者はsshポートフォワードを利用してHTTP経由でローカルに通知させる、というもの。
その後id:punitanさんに教えていただいたのが以下の方法。
SSHだけでリモートサーバからローカルMacにGrowl通知したい! - punitan (a.k.a. punytan) のメモ
専用ログファイルを用意してtail -fを使ってローカル通知させる、という手法。


出来る限り汎用的にどんなリモートマシンでも使えるように、というのを考えるとやはりログファイルを使用する方法、になるでしょうか。wget的なものが無い環境もそうそう無いと思うのでHTTP経由もアリだと思います。ローカルでHTTPサーバを立ち上げる必要があるものの、

のような簡単な方法でそれは用意出来るので。あとポートフォワードもssh_configで設定しておくことができますね。


…で、気になったのが「UDPじゃなくてTCPでの通知もできる…?」
昔は出来なかったのかも知れないけど、Growlの設定画面でも"TCP port 23052 and UDP port 9887"って書いてあるし、http://growl.info/documentation/developer/protocol.phpにもTCPサポートしているって書いてある。
"Listen for incoming notifications"にチェックしてあれば、

$ telnet 127.0.0.1 23052
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.

となるので23052番がlistenされているのは間違い無い。
ので、UDPで流しているパケットと同じものを試しにTCP 23052番ポートに流してみたが、ウンともスンとも言わない。そのへんは仕組みが違うようだ。
network通知を使うライブラリも幾つかソース覗いてみたけど、どれもUDP 9887番を使うものでTCPを使うサンプルが見当たらない。
こうなったらGrowl自体のソースを覗いてやる…!とGrowl 1.2.1のソースをダウンロードしてきてObjective-Cのソースを見てみる。
GrowlRemotePathwayクラスを継承したGrowlTCPPathwayクラスとGrowlUDPPathwayクラスがあって、後者の方には受け取ったパケットを検証して通知させるロジックがずらーっと書いてあったのだけど、前者の方は確かに23052番ポートを使ったNSConnectionが作られていてlistenしているようだけど…データを受けて処理する部分のロジックが見当たらない…これはダミーなのだろうか?ジックリ見たわけでもないので自信ないけど。。


ということでTCPでの通知ができればsshポートフォワードだけで通知出来そうで良いかなーと思っていたけどここはちょっと諦めた。


あと試してみようと思ったのがUDPのNAT越え。リモートマシンはglobal IPがあるがローカルマシンはprivate IPしかない、という場合にリモート->ローカルのUDPデータ送信ができない。のだけど、UDP hole punchingという技術があったりして、NATの向こう側にUDPデータ送信することも可能らしい(先日まで全然しらなかった)。
上記のようなケースの場合、リモート側で一度適当なポートでUDPサーバを立ち上げておいてローカルから自分のポートを指定してUDPのパケットを送信。するとリモートでは送信元の情報として送信元ルータのIPアドレスと、NATによって変換されたポート番号がわかる。ここにUDPを送り返してやればNATの向こう側にあるローカルマシンに届く、という仕組みらしい。

#!/usr/bin/env perl
use strict;
use warnings;
use IO::Socket::INET;

my $sock = IO::Socket::INET->new(
    LocalPort => 15873,
    Proto     => 'udp',
) or die $!;

while (1) {
    my $buf;
    my $ca = $sock->recv($buf, 65535);
    my ($port, $addr) = unpack_sockaddr_in($ca);
    my $ip = inet_ntoa($addr);
    print "from $ip:$port\n";
}

などのようなUDPサーバをリモート側で立ち上げておいて、ローカル側では

#!/usr/bin/env perl
use strict;
use warnings;
use IO::Socket::INET;

my $sock = IO::Socket::INET->new(
    LocalPort => '9887',
    PeerAddr => '<リモートマシンのIPアドレス>'
    PeerPort => '15873',
    Proto    => 'udp',
) or die $!;

print $sock 'hoge';

と送ってやる。と、

$ perl udp_server.pl
from ***.***.***.***:49002

のように送信元の情報がとれるので、ここにUDPパケットを送ってやれば良いということになる。
ローカルでは

#!/usr/bin/env perl
use strict;
use warnings;
use Growl::Any;
my $growl = Growl::Any->new(
    appname => 'MyApp',
    events  => ['ev1']
);
$growl->notify('ev1', 'title', 'message');

などでapplicationを先に登録しておき、リモート側ではNet::GrowlClient(おそらく送信先portまで指定出来る唯一のライブラリ?)を使って送信する。

#!/usr/bin/env perl
use strict;
use warnings;
use utf8;
use Net::GrowlClient;

my $growl = Net::GrowlClient->init(
    CLIENT_TYPE_NOTIFICATION => 1,
    CLIENT_PEER_HOST => '<送信元(ローカルマシン側)のIPアドレス>',
    CLIENT_PEER_PORT => 49002,
    CLIENT_APPLICATION_NAME => 'MyApp',
    CLIENT_PASSWORD         => '***********',
) or die $!;

$growl->notify(
    title        => 'Notification from Remote!',
    message      => 'こんにちはこんにちは!',
    notification => 'ev1',
);


出来た!!
…けどコレはNATの変換テーブルがポート番号の対応表をいつ更新するか分からないので頻繁にローカルからUDPパケット送信してポート確かめる必要があったり、そもそもリモート側でNet:GrowlClientに依存するしこれ使わないとUDPパケット作るの大変だし、もう全然現実的じゃない。

結論

やっぱりログファイル用意してtail -fするか、httpサーバ立ち上げてポートフォワードしておいてwget、が良いと思います。