Java Web Start版 Groovy Console をちょっとだけ変えてみた その3

残念ながら 参加はできませんでしたが...
12/20 の JavaFX & GlassFish 合同勉強会id:nobeans さんが FxBuilder を使って Groovy から JavaFX を利用する方法を発表されていたのを受けて、Java Web Start 版 Groovy Console でも FxBuilder を利用できるようにしてみました。
id:nobeans さんの発表内容は こちら

今回の変更点は JNLP ファイルのみ...
Groovy Console 用 JNLP ファイルの resources に JavaFX Runtion の JNLP ファイルと

<extension name="JavaFX Runtime" href="http://dl.javafx.com/1.2/javafx-rt.jnlp" />
FxBuilder で必要な JAR ファイル
<jar href="fxbuilder-0.2.jar" />
<jar href="JFXtras-0.5.jar" />
<jar href="miglayout-3.6.3-swing.jar" />
を追加しただけ...
※ 実際には他の修正も一緒に行っていますが 今回は省略。詳細は次回以降で...

しかし JNLP の extension 機能はとっても便利ですね。
JavaFX Runtime のように既に公開された JNLP コンポーネントがあれば、何も考えなくても自分のアプリケーションに必要なライブラリを全て取り込んでくれるのです。
Groovy や Griffon も JNLPコンポーネントとして公開されないのだろうか。

もちろん、この Java Web Start 版 Groovy Console を利用すれば...
面倒な インストール作業なしに すぐに JavaFX の世界を体験することができます。
JavaFX を試してみたいけど インストール面倒だし やめとこ...」なんて方には とってもおススメ!!

興味のある方は右下のメニューの Groovy Console の [Launch] ボタンをクリックしてみてください。

注意!!
Groovy Console はオレオレ証明書で署名されており、全ての権限が与えられています。
利用される方はあくまで自己責任で...

あとは こんな感じでコーディングして実行するだけ...

import griffon.builder.fx.*;
import javafx.lang.*;
import javafx.animation.*;

def fx = new griffon.builder.fx.FxBuilder();

def rt = fx.parallelTransition {
    fx.rotateTransition(
        duration: Duration.valueOf(3000),
        interpolator: Interpolator.$LINEAR,
        byAngle: 360,
        repeatCount:Timeline.$INDEFINITE
    )
    fx.scaleTransition(
        duration: Duration.valueOf(3000),
        interpolator: Interpolator.$LINEAR,
        fromX: 0.1, fromY: 0.1,
        toX: 2.0, toY: 2.0,
        autoReverse: true,
        repeatCount:Timeline.$INDEFINITE
    )
};

fx.edt {
    stage(width: 200, height: 200) {
        scene {
            stack (width: 200, height: 200) {
                text = text(content: "Hello, World.")
            }
            button = button(
                text: "PAUSE",
                layoutX: 10,
                layoutY: 10,
                action: {
                    if (button.text == "PAUSE") {
                        rt.pause();
                        button.text = "PLAY";
                    } else {
                        rt.play();
                        button.text = "PAUSE";
                    }
                }
            )
        }
    }
    
    rt.node = text;
    rt.play();
}

ただ、残念なのは FxBuilder が 最新の JavaFX 1.3 に対応していないってことかな...

JavaFX Script 再び...

JavaFX 2.0 から 単なる Java ライブラリ になり JavaFX Script がサポートされなくなるということで、
Groovy で JavaFX しようと考えていたのも束の間...

なんでも JavaFX Scriptオープンソース となって帰ってくるらしい...
しかも名前を変えて!!
その名も Visage...

詳細は プロジェクトオーナーでもある Stephen Chin さんが
Announcing Visage – The DSL for Writing UIs』 でアナウンスされています。

プロジェクトページは http://code.google.com/p/visage/
既に一部のコードが JavaFX Script Compiler から fork されているみたい。

まだこれから活動を始めるようなので どうなるかわからないが
Visage - A Refreshing Change』 によると 最初のターゲットは Android らしく、
Google を開発に巻き込もうという計画らしい。
もし、本当にこの計画がうまく行くようなら かなり面白いことになりそうだ。

JavaFX 2.0 roadmap and features overview

先月開催された JavaOne 2010 で 次期リリース JavaFX 2.0 についての発表がありました。
JavaFX 2.0 のロードマップについては

http://javafx.com/roadmap/
で公開されています。
全体的には機能拡張されるようでよいのですが...
なんと 次期 JavaFX 2.0 からは単なる Java のライブラリになってしまうらしい...

と言うことで...
あえなく JavaFX Script の歴史に終止符がうたれることに...

過去 JavaFX には何度も裏切られてきたが、今回のはちょっと半端ない。
それでも Java や Groovy、JRuby と言ったJVM で実行可能な あらゆる言語で 利用できるようになるのは ちょっとうれしかったりするのがちょっと悲しい...

でも それって Swing と何が違うの...
と言ってても 仕方がないので、今から JavaFX 2.0 に移行できるように準備しておくとしよう。

JavaFX 2.0 を利用するには、まず使用する言語を決めないと...
今更 Scala に手を出すのも厳しいし、Java という選択肢もあり得ない。
となると Groovy か JRuby なのだが JavaOne のプレゼン資料

を見ると どちらかと言うと Groovy の方がしっくりくるかな...

JavaFX Script だと こんな感じが...

Stage {
    title: "Hello Rectangle (Groovy FxBuilder 2)"
    width: 600 
    height: 450
    scene: Scene {
        fill: Color.LIGHTSKYBLUE
        content: Rectangle {
            x: 25, y: 40
            width: 100, height: 50
            fill: Color.RED
        }
    }
}

Groovy だと こんな感じになるらしい...
FxBuilder.build { 
    stage = stage(
        title: "Hello Rectangle (Groovy FxBuilder 2)", 
        width: 600, 
        height: 450, 
        scene: scene(fill: Color.LIGHTSKYBLUE) {
            rectangle( 
                x: 25, y: 40,
                width: 100, height: 50, 
                fill: Color.RED
            )
        }
    )

    stage.visible = true;
}
と言うことで Groovy で行ってみよう!!

Wake on LAN してみる Part 4 (SOCKS5 を越えて)

Wake on LAN をしてみる Part 2』 で Wake on LANJavaFX で実装する方法について書いた。
未だ完成には いたらないのものの、とりあえず、居間にいながらにして 別の部屋にある Linux サーバの電源の ON/OFF を行えるぐらいにはなった。

とりあえず、当初の目的は十分達成できたので 後は適当に画面を作れば 何となくアプリケーションは完成するはずだったのだが...

人間の欲望というのは恐ろしいもので、これだけ便利だと 居間からだけではなく、 会社からも ON/OFF できるようにしたくなる。
ネットワークにつながっていれば どこからでも一緒でしょ!! なんて言う甘い考えで試してみることに...

会社のデスクから 自宅の PC を起動するためには 会社の PC から自宅のプライベートネットワークに マジックパケットをブロードキャストしなくてはならない。
しかも

  • 自宅のルータ
  • 会社のプロキシ
という大きな壁を2つも越えて...
こんな感じで...
会社PC ----> [会社のプロキシ] ---- インターネット ----> [自宅のルータ] ----> 自宅PC
巷で ルータ越え とか プロキシ越え とか言われているやつ。

前者は利用しているルータにもよるが、大抵の場合 ルータの設定を変更するだけでなんとかなるはず...
もし、不幸にも なんともならなかったら、そんな ルータ は買い替えてしまえばよい。
で、実際何を設定すれば良いかと言うと

任意のポートに送信されてきた UDP のパケットを プライベートネットワークのブロードキャストアドレス宛に転送する
ように設定するだけで良い。
例えば、
5555 -- 転送 --> 192.168.0.255:5555
のように設定すると...
ルータの 5555 ポート (5555 でなくてもよい) 宛に マジックパケットを送信するだけで ルータが勝手にプライベートネットワークに ブロードキャストしてくれる。

早速、自宅のルータでも設定してみたのだが、なんと!!
『IP アドレス は 255 より小さな値でなければなりません。』とか言われて 255 への転送を許可してくれない。
確かに、下手に ブロードキャストアドレスに転送できてしまったら Smurf 攻撃の踏み台にされかねないので、しかたがないのだが...
とは言え、簡単にはあきらめられないので、試しに サブネットマスクを 255.255.255.128 に変更してみた。
これは、単にブロードキャストアドレスの4バイト目を 255 以外にしてみれば 良いんじゃない という安易な考えだったのだが...
なんと!!

5555 -- 転送 --> 192.168.0.127:5555
という感じで ブロードキャストアドレス 192.168.0.127 に転送するように設定できた。
家庭用ルータなんてこんなもんなんだね。^^;)

と言うことで...
早速 Linux サーバが起動されるかテストしてみたが、 もちろん問題なく起動してくれた。
ちなみに ルータのグローバルアドレスは 大抵 DHCP で割り当てられているので、 固定のドメイン名でアクセスできるように ダイナミックDNS に登録しておけば完璧だ。
これで 公衆無線LANからなら どこからでも 自宅のサーバを起動することができてとっても便利だ。

これで1つ目の壁は無事乗り越えることができたのだが...
会社から 自宅の Linux サーバを立ち上げるには まだもう一つ超えなくてはならない 『プロキシ』 という壁がある。
通常、JavaFXJava Web Start や Applet として起動するため アプリケーション側で プロキシ のことを気にする必要はない。
Java Preferences でプロキシの設定ができるからである。ちなみに 通常は システムの設定がそのまま利用される。
しかし、これは HTTP 等の TCP に限った話だ。 TCP については J2SE 5.0 で プロキシをサポートするようになったのだが、UDP については Java 6 でもサポートされていない。(詳細は こちら)

プロキシには HTTP や SOCKS など いくつかの種類があるが UDP で利用できるプロキシは SOCKS5 以外にない。
SOCKS5 は rfc1928 でちゃんとスペックが決まっているので、自前で実装することもできるのだが...
google で検索したら jsocks という Java のライブラリが見つかったので それを使うことにした。
JSch もそうだが 既存の Java のライブラリがそのまま利用できるのは JavaFX のメリットの一つでもある。
このライブラリには SOCKS5 のプロキシ経由で UDP パケットを送信するための Socks5DatagramSocket というクラスが用意されている。
使い方は いたって簡単だ。以下のように 通常の DatagramSocket の代わりに使うだけ...

import java.lang.Integer.parseInt;
import java.net.DatagramSocket;

import socks.Socks5DatagramSocket;
import socks.Socks5Proxy;

// 起動する PC の NIC の MAC アドレス
public var macAddress = "aa:bb:cc:dd:ee:ff"

// マジックパケットの送信先ホスト名 (ルータのダイナミックDNSのドメイン名)
public var host = "host.to.wake.on.lan";

// マジックパケットの送信先ポート (ルータのポート)
public var port = 5555;

// SOCKS5 プロキシサーバのホスト名 又は IPアドレス
def proxyHost = "socks5.proxy";

// SOCKS5 プロキシサーバのポート番号
def proxyPort = 1080;
   

// マジックパケット
def magicPacket = [
    for (i in [1..6]) { 0xff }
    for (i in [1..16], b in macAddress.split("-|:")) { parseInt(b, 16) }
];

// プロキシサーバ
def proxy = new Socks5Proxy(proxyHost, proxyPort);
proxy.resolveAddrLocally(false);

// マジックパケットを任意のポートにブロードキャストするための UDPデータグラムパケット
// ここでは送信先のホスト名は指定してはいけない。
// プロキシの内側のネットワークでは外部のネットワークの名前解決ができない場合があるため...
def packet = new DatagramPacket(magicPacket, sizeof magicPacket, null, port);

// プロキシサーバ経由で マジックパケット 送信
def socket = new Socks5DatagramSocket(proxy, 0, null);
socket.send(packet, host);  // ここでホスト名を指定する。

Wake on LAN をしてみる Part 2』 のサンプルコードと見比べても そんなに違いはない。
たったこれだけのコードでプロキシを越えられるなんて少し拍子抜けしてしまったが...
これで いざというときに 会社から自宅の PC を起動することができてしまう。
なんて幸せなんだろう...
プロキシを越えられる WOL のアプリケーションは見たことがないので これはなかなかおもしろいかもしれない。

今回は起動だけしか試せなかったが 同様にサーバを停止することだってできるはずである。
これについては またの機会ということで...

SSH してみる Part 3 (コマンド実行編)

今回は SSH 経由で リモートサーバ上のコマンドを実行する方法です。
SSH してみる Part2 でも 書いている通り 最終的な目標はサーバのモニタリングツールを作ることですが、 まずは vmstat を実行して その結果をそのまま画面に表示することからはじめてみましょう。

今回もサーバには Ubuntu 10.04 LTS を使っています。
サーバ にログインして vmstat をオプションなしで実行してみると 以下のような情報がコンソールに標準出力されます。

procs -----------memory---------- ---swap-- -----io---- -system-- ----cpu----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa
 0  0      0 235708  24976 100720    0    0     6     3   13   31  0  0 100  0

これと同じことを JavaFX で実現してみましょう。
まずは表示する画面の作成から...
画面は 『SSH してみる (パスワード認証編)』 で作成したものをそのまま利用します。
もちろん パスワード認証部分も...
init 状態 (初期状態) の 真っ黒な画面に 識別子 "foreground" で ラベル を一つ追加します。

実行するとこんなイメージ...


後は 『SSH してみる (パスワード認証編)』 で作成したコード中の

Alert.inform("Auth success");
を以下のように変更するだけです。
もちろん、今回もエラー処理等は考慮していません。
// コマンドを実行するためのチャネルを開く
def channel = session.openChannel("exec") as ChannelExec;

channel.setCommand("vmstat");
channel.setErrStream(System.err);   // TODO エラー処理

def in = channel.getInputStream();

// コマンドを送信する
channel.connect();

// アプリケーション終了時にチャネルを閉じるためのフック
FX.addShutdownAction(function(): Void { channel.disconnect(); });

try {

    def r = new BufferedReader(new InputStreamReader(in, "UTF-8"));

    // チャネルが開いている間 処理を続ける
    while (true) {

        // ブロックせずに読み込めるか確認
        while (r.ready()) {

            // 一行分のデータを読み込み画面に表示
            foreground.text = "{foreground.text}\n{r.readLine()}"
        }
        
        // チャネルが閉じていたら終了
        if (channel.isClosed()) {
            break;
        }

        // 暫く待ってもう一度データを読み込む        
        Thread.sleep(500);
    }

} catch (e: InterruptedException) {
    // ignore
}

ポイントは

  • コマンドを送信する前に InputStream を取得しておく
  • Reader#ready() で読み込み可能か確認する
の2つです。

1つ目ですが Channel#connect() を呼び出す前に InputStream を取得しておかないと タイミングによっては サーバからの出力が受け取れないことがあるからです。というのも...
サーバからの出力を受け取るための InputStream は Channel#getInputStream() を実行したときに初めて生成されるのです。
その為 Channel#connect() して サーバ上で コマンドを実行する前に準備しておかないとダメなのです。

2つ目ですが BufferedReader で いつもの

var line: Object;
while ((line = r.readLine()) != null) {
    ...
}
のように readLine() をいきなり呼んでしまっては readLine() でブロックされてしまいます。
その為 ready() を実行して ブロックされずに読み込めるか 確認してから読み込まないとダメなのです。
でないと サーバからの出力が返ってこない場合、永遠に readLine() で待たされてしまうことに...
今回はサンプルなので問題ないですが、通常 タイムアウト処理 や 再実行処理等 を ちゃんと実装しないといけないのでこれは重要です。

sudo をプログラムで使う際の注意点

Wake on LAN してみる Part 3 (shutdown)』 で Linux サーバを停止する際の sudo の使い方に問題があったので まとめておくことに...

問題1

sudo のバージョンによって標準入力からパスワードを渡す方法が異なるらしい。

Ubuntu 8.04 LTS の sudo (1.6.9p10) では

echo "pass" | ssh hoge@192.168.1.1 "sudo /sbin/shutdown -h now"
で問題がなかったが 最新の Ubuntu 10.04 LTS の sudo (1.7.2p1) では
sudo: no tty present and no askpass program specified
のようなエラーになってしまう。どうやら
echo "pass" | ssh hoge@192.168.1.1 "sudo -S /sbin/shutdown -h now"
のように "-S" オプションをつけて実行しないとダメらしい。

問題2

sudo に "-S" オプションをつけて実行すると プログラムから標準入力経由でパスワードを渡しても

[sudo] password for hoge: Sorry, try again.
[sudo] password for hoge: Sorry, try again.
[sudo] password for hoge: Sorry, try again.
sudo: 3 incorrect password attempts
のように認証エラーになる。

どうやらパスワードの最後に 改行文字 "\n" をつけて

channel.setInputStream(new ByteArrayInputStream("{password}\n".getBytes()));
としないとダメらしい。考えてみれば、当然だけど...
改行コードが無いと どこまでがパスワードなのか分からないからね。

無理して InputStream でパスワードを渡すより、ssh で実行するコマンド自体を

"echo {password} | sudo -S -p '' /sbin/shutdown -h now";
としてしまった方が簡単で良いかもしれない。
echo なら何も考えなくても改行コードつけてくれるから...

問題3

sudo を実行すると

[sudo] password for hoge: 
のように パスワード要求プロンプトが標準エラーに出力されてしまう。

パスワードを要求するプロンプトは "-p" オプションで指定できるので

sudo -p '' ...
のように空文字を指定すれば プロンプトの出力を消すことができます。

これらを踏まえると 最終的なコードは

import java.io.ByteArrayInputStream;
import java.lang.*;

import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSch;

// サーバ情報
def host = "192.168.1.1";
def port = 22;

// ユーザ情報
def username = "hoge";
def password = "pass";

// shutdown コマンド
def command = "sudo -S -p '' /sbin/shutdown -h now";

def jsch = new JSch();
def session = jsch.getSession(username, host, port);

try {

    // SchException: UnknownHostKey を防ぐための設定
    session.setConfig("StrictHostKeyChecking", "no");
    session.setPassword(password);
    session.connect(5000);

    var channel = session.openChannel("exec") as ChannelExec;

    try {
        channel.setCommand(command);

        channel.setInputStream(new ByteArrayInputStream("{password}\n".getBytes()));
        channel.setOutputStream(System.out);
        channel.setErrStream(System.err);
        channel.connect();

        // コマンドが終了するまで待機
        while (not channel.isClosed()) {
            Thread.sleep(1000);
        }

    } finally {
        channel.disconnect();
    }

} finally {
    session.disconnect();
}
のようになる。

もちろん 『Wake on LAN してみる Part 3 (shutdown)』 についても上記の内容でこっそり修正してあります。