RIP Steve Jobs

僕はジョブズが作った開発者に対して閉鎖的な世界(iPhoneアプリまわり)はきらい。
だからObjectiveCも勉強しないし、iPhoneアプリ開発もやらないだろう。

僕はまったくApple信者ではない。

… しかし我が家にはなぜか

・ipod nano x 2(新旧)
・ipod touch
・iPhone(かみさん使用)
・iPad
・MacBook

があるw

プロダクトとしてはすばらしいし、周りの機器(オーディオアンプとか)もiPod前提のコネクタを揃えているものが多いので、何も考えずに使えて便利なのよね。

Appleの発表をリアルタイムで見たことは一度もないし、ジョブスのスピーチもほとんど聴いたことがない。

でも彼の挫折や成功のストーリーについては、本を読んだことがあり、少し知っている。

敵もたくさん作ったらしい。やはりすごいストレスと隣り合わせだったのだろう。それが癌と関係あるのかどうかはわからないけど。

ジョブズ信者ではないのに、今日はすごくショックを受けた。

56歳で死ぬにはあまりにももったいない人だ。


Android 3.1 on Asus Transformerで本気でサーバー管理するためのConnectBot

Asus TransformerはAndroid3.0(3.1にアップグレード可能)搭載の、タブレット型とノートPC型に変形可能な変態端末である。

キーボードは本気で実用性を考慮されて作られており、そのへんのノートPCより(筆者が昔使っていたLet’s NoteやMacBookより)格段に打ちやすい。ヨドバシカメラの店頭で出会った際、キーボードの打ちやすさに感心し、「ノート型のAndroidもこれだけ打ちやすいキーボードが付いてるなら悪くないナァ…」と思ったのだが、その後タブレット部が外れてそのまま使えるということに気がついてノックアウトされ、次の日には買っていた。(いちおう、2chで評判を確かめたので次の日なのです)

transformerのキーボードドック部は重く(620g)、タブレット側(680g)と合わせると1.3Kgくらいになってしまうので一般人が持ち歩くにはかなり重い。しかし、Thinkpad+デカバッテリーで1.9Kgを常に持ち歩いている筆者から見ると非常に軽い。しかも、電車の中等、移動中にはタブレット側のみ出して作業をすることができる。さらにさらに、異常にバッテリーの保ちがいいので、使い倒したくても使い倒せないという魅力がある。先日のMongoDB勉強会でも終始WiFiオンで使い続けていたが、結局ドック部のバッテリーだけで足りてしまい、タブレット側は終始97%〜99%バッテリーが残っているという驚異的な状況だった。

作りも非常にしっかりしていて好印象である。Amazonのレビューかどこかで誰かも書いていたが、隅々にまで剛性感があり、往年の(15年くらい前の)日本製品を思わせるような安心感がある。プラスチックぽくてぺらぺらしているSonyタブレットとは天と地の差である。(Sonyタブレットはその分軽いのかもしれないですけど…)

そんなわけでかなり気に入ったので、モバイルとして実用しようと思った際に問題になったのが、サーバ管理のSSHターミナルクライアントをどうするのかということだ。AndroidではConnectBotというよく知られたソフトウェアがあるのだが、transformerのハードウェアキーボードではCtrlキーやShiftキー、Escキー等が効かないためにviなどを実用レベルで使うことができないのである。どうしようかと思っていたところ、こちらのエントリでConnectBotの改造方法が紹介されているのを発見した。そこで、同じ方法でソースコードを書き換え、transformer用に改造したところ、各キーともにうまく動くようになり、viやscreenが問題なく使えるようになった。筆者以外にもtransformerでSSHを使いたい人はいるだろうと思うので、ここに置いておく。

APKファイル
Eclipseプロジェクトディレクトリ(全ソースコードを含む)

・「戻る」キー(キーボード上の一番左上)は、Escとして動作する
・「戻る」キーを本来の「戻る」キーとして動作させるには、Shift+「戻る」キーを押せばOK
・「半角/全角」キーは、Escとして動作する
・「カタカナ・ひらがな・ローマ字」キーも、Escとして動作する
・CtrlとShiftは普通にそのまま動く

ようになっている。なお、Simejiを使っている人はSimeji側を無効にするなどしないと、Shift+7でシングルクォートを打つことができないので注意。

書き換えを行ったファイルはTerminalKeyListener.javaで、2カ所にそれぞれ次のソースコードを追加した。

118行めから:

  //Ctrl
if( keyCode == 113 || keyCode == 114 )
  {
  if( event.getAction() == KeyEvent.ACTION_UP )
    {
    metaState &= ~META_CTRL_ON;
    bridge.redraw();
    }
  else if( event.getAction() == KeyEvent.ACTION_DOWN )
    {
      //disable ctrl_lock
    if ( ( metaState & META_CTRL_ON ) != 0)
      {
      return true;
      }

    metaPress( META_CTRL_ON );
    return true;
    }
  }

235行目から:

  //Escape
if( keyCode == 208 || keyCode == 204 )
  {
  sendEscape();
  return true;
  }

  //Shift+Number
else if( 8 <= keyCode && keyCode <= 16 )
  {
  final char[] _keyMap = new char[]{ '!', '"', '#', '$', '%', '&', '\'', '(', ')' };
  if ( ( metaState & META_SHIFT_ON ) != 0)
    {
    bridge.transport.write( _keyMap[ keyCode - 8 ] );
    metaPress(META_SHIFT_ON);
    metaPress(META_SHIFT_ON);

    }
  else
    {
    bridge.transport.write(key);
    }
  return true;
  }

  //Ctrl
if ((metaState & META_CTRL_MASK) != 0)
  {
  int k = event.getUnicodeChar(0);
  int oldK = k;
  k = keyAsControl( k );
  if( k != oldK )
    {
    bridge.transport.write(k);
    }
  return true;
  }

  //back
if( keyCode == 4 )
  {
  if ( ( metaState & META_SHIFT_ON ) == 0)
    {
    sendEscape();
    return true;
    }
  }

不具合の連絡など、フィードバックは@kinyukaまで。


SSL中間CA証明書を検索するウェブサービス、SSLDB.info

SSLの中間CA証明書を、サーバ証明書をキーに検索するウェブサービスを開発した。
Search for SSL Intermediate CA Certificates – SSLDB.info

また、詳しい内容をWAF Tech Blogにまとめたのでご参考まで。

後日、裏話的な小ネタをこちらのブログに書く予定。


Scutum(スキュータム)のサイトでブログ連載開始

業務に関連する話題をWAF Tech Blog | SaaS型 WAFサービス Scutum 【スキュータム】で連載することができるようになりました。ぜひご覧ください。

こちらでは、引き続きAndroid等の業務には直接関連していないネタなどを扱っていこうと思います。


Clojureでデッドロック



0×00. Clojureでデッドロックは起こるのか?


Clojureの並行プログラミングモデルはSTMとagentを中心とした非常に洗練されたものだとされていることがある。Javaでスレッドを生で扱う場合の危険性と対比され、Clojureは開発効率がよく危険性が少ない、と思っているユーザも多いだろう。しかし実際にはClojureのSTMは書き込み競合が発生した場合に非常に遅く、また実際の並行プログラミングのためにはSTMとagentだけではなく他の方法も覚える必要があり、それほどシンプルなものではない。

安全性についてはどうだろうか。Clojureではデッドロックは起こるのだろうか。結論を書いてしまうと、Clojureで書いたアプリケーションもデッドロックを起こす可能性がある。以下にいくつかの例をあげる。


0×01. lockingを使うパターン


ClojureにはJavaのsynchronizeのほぼ代用となるlockingマクロが存在している。lockingの引数として渡したオブジェクトに対するモニターを取得し、ボディ部の処理を行う。デッドロックの王道パターンの1つとして「複数のスレッドが2つ以上のモニターを逆順で取得しながら処理する」というものがあるが、これをClojureで書くと以下のようになり、デッドロックを起こす。

deadlock1.clj

(ns deadlock1)

(def _mutex1 (new Object))
(def _mutex2 (new Object))

(defn fn1[ _ ]
  (while true
    (locking _mutex1
      (println "sleep 1")
      (Thread/sleep (rand-int 10))
      (locking _mutex2
        (println "sleep 2")
        (Thread/sleep (rand-int 10))
      )
    )
  )
)

(defn fn2[ _ ]
  (while true
    (Thread/sleep (rand-int 10))
    (locking _mutex2
      (println "sleep 3")
      (Thread/sleep (rand-int 10))
      (locking _mutex1
        (println "sleep 4")
        (Thread/sleep (rand-int 10))
      )
    )
  )
)

(send-off (agent nil) fn1)
(send-off (agent nil) fn2)

このときのスレッドダンプは以下のようになる。

"pool-2-thread-2" prio=10 tid=0x0000000040e22000 nid=0x42d6 waiting for monitor entry [0x00007f43ba4c0000..0x00007f43ba4c09f0]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at deadlock1$fn2$fn__12.invoke(deadlock1.clj:24)
	- waiting to lock  (a java.lang.Object)
	- locked  (a java.lang.Object)
	at deadlock1$fn2.invoke(deadlock1.clj:22)
	at clojure.lang.AFn.applyToHelper(AFn.java:163)
	at clojure.lang.AFn.applyTo(AFn.java:151)
	at clojure.lang.Agent$Action.doRun(Agent.java:100)
	at clojure.lang.Agent$Action.run(Agent.java:150)
	at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
	at java.lang.Thread.run(Thread.java:619)

"pool-2-thread-1" prio=10 tid=0x0000000040b29800 nid=0x42d5 waiting for monitor entry [0x00007f43ba5c1000..0x00007f43ba5c1a70]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at deadlock1$fn1$fn__7.invoke(deadlock1.clj:10)
	- waiting to lock  (a java.lang.Object)
	- locked  (a java.lang.Object)
	at deadlock1$fn1.invoke(deadlock1.clj:8)
	at clojure.lang.AFn.applyToHelper(AFn.java:163)
	at clojure.lang.AFn.applyTo(AFn.java:151)
	at clojure.lang.Agent$Action.doRun(Agent.java:100)
	at clojure.lang.Agent$Action.run(Agent.java:150)
	at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
	at java.lang.Thread.run(Thread.java:619)

この例はソースコードを見るだけでデッドロックの可能性を発見できるが、実際のケースではこの例ほど単純ではないことが多い。関数呼び出しの階層が深くなる中で、意識せずにロックを取得しながら別の関数を呼び出すことで問題に繋がることが多いだろう(これはClojureだけではなく他のロックを用いる言語でも同様である)。そのため、このパターンのデッドロックは実際に発生することが考えられ、注意が必要となる。lockingを使わずに済めば、それが一番の対策だ。lockingを使う必要がある場合には、取得する順番を決めておく必要がある(この順番を言語で制御できればよいのだが…)。


0×02. awaitを使うパターン


agentの実行を待つためのawaitはブロックする。そのため2つのagentがお互いにawaitするとデッドロックを引き起こすことがある。

deadlock2.clj

(ns deadlock2)

(def _agent1 (agent 1))
(def _agent2 (agent 1))

(defn fn1 [ _ ]
  (Thread/sleep 1000)
  (await _agent2)
  (println 1)
)

(defn fn2 [ _ ]
  (Thread/sleep 500)
  (await _agent1)
  (println 2)
)

(send-off _agent1 fn1)
(send-off _agent2 fn2)

このときのスレッドダンプは以下のようになる。

"pool-2-thread-2" prio=10 tid=0x0000000041bc4c00 nid=0x5b28 waiting on condition [0x00007ffedf571000..0x00007ffedf571a70]
   java.lang.Thread.State: TIMED_WAITING (parking)
	at sun.misc.Unsafe.park(Native Method)
	- parking to wait for   (a java.util.concurrent.SynchronousQueue$TransferStack)
	at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:198)
	at java.util.concurrent.SynchronousQueue$TransferStack.awaitFulfill(SynchronousQueue.java:424)
	at java.util.concurrent.SynchronousQueue$TransferStack.transfer(SynchronousQueue.java:323)
	at java.util.concurrent.SynchronousQueue.poll(SynchronousQueue.java:874)
	at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:945)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:907)
	at java.lang.Thread.run(Thread.java:619)

"pool-2-thread-1" prio=10 tid=0x0000000041bc6400 nid=0x5b27 waiting on condition [0x00007ffedf672000..0x00007ffedf6729f0]
   java.lang.Thread.State: TIMED_WAITING (parking)
	at sun.misc.Unsafe.park(Native Method)
	- parking to wait for   (a java.util.concurrent.SynchronousQueue$TransferStack)
	at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:198)
	at java.util.concurrent.SynchronousQueue$TransferStack.awaitFulfill(SynchronousQueue.java:424)
	at java.util.concurrent.SynchronousQueue$TransferStack.transfer(SynchronousQueue.java:323)
	at java.util.concurrent.SynchronousQueue.poll(SynchronousQueue.java:874)
	at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:945)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:907)
	at java.lang.Thread.run(Thread.java:619)

このようなデッドロックを避けるためにはawait-forを使用しタイムアウトさせればよい。しかしその場合、タイムアウト時のエラー処理を行わなければいけないために、若干シンプルさが欠けてしまうというデメリットが存在する。


0×03. futureを使うパターン


futureのderefはブロックするので、0×02項と同じようにデッドロックを引き起こす可能性がある。

deadlock3.clj

(ns deadlock3)

(def _ref1 (ref 10))
(def _ref2 (ref 20))

(defn fn1 []
  (Thread/sleep 1000)
  (println 1)
  (println (deref (deref _ref2)))
  (println 2)
)

(defn fn2 []
  (Thread/sleep 500)
  (println 3)
  (println (deref (deref _ref1)))
  (println 4)
)

(dosync
  (ref-set _ref1 (future (fn1)))
  (ref-set _ref2 (future (fn2)))
)

このときのスタックトレースは以下のようになる。

"pool-2-thread-2" prio=10 tid=0x00007f09ac38f400 nid=0x7421 waiting on condition [0x00007f09b2c00000..0x00007f09b2c00d70]
   java.lang.Thread.State: WAITING (parking)
	at sun.misc.Unsafe.park(Native Method)
	- parking to wait for   (a java.util.concurrent.FutureTask$Sync)
	at java.util.concurrent.locks.LockSupport.park(LockSupport.java:158)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:747)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(AbstractQueuedSynchronizer.java:905)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1217)
	at java.util.concurrent.FutureTask$Sync.innerGet(FutureTask.java:218)
	at java.util.concurrent.FutureTask.get(FutureTask.java:83)
	at clojure.core$future_call$reify__5494.deref(core.clj:5399)
	at clojure.core$deref.invoke(core.clj:1765)
	at deadlock3$fn2.invoke(deadlock3.clj:16)
	at deadlock3$eval9$fn__10$fn__13.invoke(deadlock3.clj:22)
	at clojure.lang.AFn.call(AFn.java:18)
	at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:303)
	at java.util.concurrent.FutureTask.run(FutureTask.java:138)
	at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
	at java.lang.Thread.run(Thread.java:619)

"pool-2-thread-1" prio=10 tid=0x00007f09ac390800 nid=0x7420 waiting on condition [0x00007f09b2d01000..0x00007f09b2d01cf0]
   java.lang.Thread.State: WAITING (parking)
	at sun.misc.Unsafe.park(Native Method)
	- parking to wait for   (a java.util.concurrent.FutureTask$Sync)
	at java.util.concurrent.locks.LockSupport.park(LockSupport.java:158)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:747)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(AbstractQueuedSynchronizer.java:905)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1217)
	at java.util.concurrent.FutureTask$Sync.innerGet(FutureTask.java:218)
	at java.util.concurrent.FutureTask.get(FutureTask.java:83)
	at clojure.core$future_call$reify__5494.deref(core.clj:5399)
	at clojure.core$deref.invoke(core.clj:1765)
	at deadlock3$fn1.invoke(deadlock3.clj:9)
	at deadlock3$eval9$fn__10$fn__11.invoke(deadlock3.clj:21)
	at clojure.lang.AFn.call(AFn.java:18)
	at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:303)
	at java.util.concurrent.FutureTask.run(FutureTask.java:138)
	at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
	at java.lang.Thread.run(Thread.java:619)

このようなデッドロックを避けるための対策として、futureの処理が終了しているのかどうかを調べるfuture-done?を使うことができる。


0×04. promiseを使うパターン


promiseはデッドロック製造装置かと思われるほどの危険な機能である。以下に例を示す。

deadlock4.clj

(ns deadlock4)

(def _promise (promise))
(println @_promise)

このときのスタックトレースは以下のようになる。

"main" prio=10 tid=0x0000000041442800 nid=0x6282 waiting on condition [0x00007fd6f435b000..0x00007fd6f435beb0]
   java.lang.Thread.State: WAITING (parking)
	at sun.misc.Unsafe.park(Native Method)
	- parking to wait for   (a java.util.concurrent.CountDownLatch$Sync)
	at java.util.concurrent.locks.LockSupport.park(LockSupport.java:158)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:747)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(AbstractQueuedSynchronizer.java:905)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1217)
	at java.util.concurrent.CountDownLatch.await(CountDownLatch.java:207)
	at clojure.core$promise$reify__5536.deref(core.clj:5513)
	at clojure.core$deref.invoke(core.clj:1765)
	at deadlock4$eval7.invoke(deadlock4.clj:4)
	at clojure.lang.Compiler.eval(Compiler.java:5419)
	at clojure.lang.Compiler.load(Compiler.java:5852)
	at clojure.lang.RT.loadResourceScript(RT.java:340)
	at clojure.lang.RT.loadResourceScript(RT.java:327)
	at clojure.lang.RT.loadResourceScript(RT.java:319)
	at clojure.main$load_script.invoke(main.clj:220)
	at clojure.main$script_opt.invoke(main.clj:273)
	at clojure.main$main.doInvoke(main.clj:354)
	at clojure.lang.RestFn.invoke(RestFn.java:409)
	at clojure.lang.Var.invoke(Var.java:365)
	at clojure.lang.AFn.applyToHelper(AFn.java:163)
	at clojure.lang.Var.applyTo(Var.java:482)
	at clojure.main.main(main.java:37)

promiseを正しく使うためのコーディングは難しく、危険性はJavaで生のスレッドAPIを使用するのと変わらない(waitとnotifyの関係に似ている)。そのため、特殊な場合にのみ使うようにするのがよいだろう。例えば並列処理を行うための少し低レベルなライブラリを作るなどの場合に、十分なテストとともに使用するようなケースが考えられる。


0×05. まとめ


このように、Clojureでもデッドロックは起こりえる。他にもパターンがあれば追記していきたいので、コメント欄やtwitter(@kinyuka)で教えていただければありがたい。


異種RDBMS間のレプリケーションを可能にするSymmetric DS


0×00. Symmetric DSとは



Symmetric DSはオープンソースのJava製ソフトウェアで、トリガーベースのRDBMS間レプリケーションを行うソフトウェアである。ライセンスはLGPLだ。先日、Java製の組み込みRDBMSであるH2をレプリケーションする方法がないか探していたところ、H2のサイトからリンクされており見つけた。筆者自身はまだH2/Symmetric DSともに手元で動作確認を行っただけの段階であり、これらがどの程度実用に耐えるクオリティなのかは未知数だが、ドキュメントの整備のされ方やソフトウェアから伝わってくる感触は非常によいものである。


0×01. POSシステムから誕生


RDBMS自身がレプリケーションの機能を持つ場合も多いが、筆者にもっとも馴染みのあるPostgreSQLではレプリケーションのためにさらに別のソフトウェア(pgpool IIなど)を導入するケースがよく見られる。Symmetric DSもそのような「レプリケーション機能を持たないRDBMSでもレプリケーションを可能にする」ものだ。サイトのトップページに書いてあるように、大規模なPOSシステム間でのデータ転送のニーズから開発されたというやや異色なバックグラウンドを持つ。そのため単に2つのRDBMS間のデータを同期するだけではなく、複数の階層にまたがって多数のRDBMS間でのデータ転送も行うことができる。また、遅い回線やたまにオフラインになるような信頼性の低い回線をまたいだデータ転送も考慮されているようだ。


0×02. サポートするRDBMS


ドキュメントによるとMySQL/Oracle/PostgreSQLなどの主要なRDBMSのほか、HSQLDBやDerby,H2などのJava製組み込みRDBMSもサポートしている(ただしSQLiteはサポート外のようだ)。トリガーベースのアプローチを採るため、トリガーをサポートしており、さらにJDBCがサポートされていれば、Symmetric DSが使用できる可能性があるようだ。

非常にユニークなのは、トリガーベースの動作であるため、異なる種類のRDBMS間でのレプリケーションが可能であるということだ。これにより、例えばJavaアプリケーション内で動作しているH2のデータを、ネットワークを通じて、別の場所にある大規模サーバのPostgreSQLに同期しておき、データ解析等の重い処理はサーバ側で行う、ようなことが可能となる。例えば将来、Android端末内のH2をインターネット上のPostgreSQLに同期するようなアプリケーションが出てくるかもしれない。


0×03. 実際にさわった感触


筆者はドキュメントどおりのハンズオンを、以下の2パターンで行ってみた。

  • PostgreSQL – PostgreSQL間
  • PostgreSQL – H2間
  • どちらも特にハマることもなく、20〜50分ほどで動作確認までおこなうことができた。設定に不備がある場合にはエラーが出力されるが、この際わかりやすいエラーメッセージを出してくれることもあれば、Javaの例外のスタックトレースが大量に出て少々まいることもあった。レプリケーション対象とするRDBMSについての経験はもちろん、Javaの経験や、Springに関する知識があれば特に役に立つだろう。また、チュートリアルに取りかかる前に必ずこちらのうち、使うRDBMSの項目を読んでおくことをオススメする。

    H2とPostgreSQLという異なるRDBMS間で、双方向にデータがコピーされるのを確認できたときには、なかなかに新鮮でニヤリとさせられた。異種RDBMS間でもこのようなレプリケーションが可能であるということで、今後のシステム設計の際に柔軟な思考が可能となりそうだ。MongoDBなどもサポートされれば非常に楽しそうなのだが、MongoDBではトリガーがサポートされていないので厳しいかもしれない。


    0×04. Javaでの拡張が可能


    ドキュメントにあるように、Javaを使ってプラグイン的に機能を追加できるような仕組みが用意されている。おそらく実際に現場で使うことを強く想定したものとなっているのだろうと思われる。

    Symmetric DSの内部ではSpringフレームワークが使用されており、また配布アーカイブのlibディレクトリには多くのJDBCコネクタやJetty、JMS、JUnitなどのjarファイルが含まれている。おそらくJavaに非常に詳しい開発チームが存在しているのではないかと思う。


    0×05. おわりに


    個人的な意見として、著者が使い込んでいないソフトウェアの紹介記事はあまり面白くないという考えを持っている。この記事はその趣旨に反するのだが、Symmetric DSは発想からして面白いのにあまり紹介されていなさそうなので、このような記事を書いてみた。

    筆者自身は、RDBMSよりもMongoDBの(柔軟なスキーマ変更を可能とする)方向に向かいたいと考えており、実際の環境でこれからSymmetric DSを使う日が来るかどうかは定かではないが、いつかまたレプリケーションを行う必要があるケースに遭遇した場合には候補として挙げることになるだろう。


    ClojureでPostgreSQLのデータをMongoDBに放り込む

    細かいことはさておき、とりあえずPostgreSQLのデータをMongoDBにぶち込みたい場合に使うためのClojureスクリプトを書いた。

    pg2mongo.clj

    (ns pg2mongo
      (:import
        (java.sql Connection DriverManager)
        (java.util ArrayList)
        (com.mongodb Mongo DBCollection BasicDBObject WriteConcern)
      )
    )
    
    ; PostgreSQL config
    (def _dbmsUser "joe")
    (def _dbmsPass "joe")
    (def _dbmsUrl "jdbc:postgresql:foo")
    
    ; MongoDB config
    (def _mongoDBName "foo")
    (def _mongoHost "127.0.0.1")
    
    (.newInstance (Class/forName "org.postgresql.Driver" ))
    
    (defn _copyData [ _conn _tableName _coll ]
      (let
        [
        _rs (.executeQuery (.prepareStatement _conn (str "select * from " _tableName) ) )
        _md (.getMetaData _rs)
        _columnCount (.getColumnCount _md)
        ]
        (while (true? (.next _rs))
          (let [ _dbObject (new BasicDBObject) ]
            (dotimes [ _i _columnCount ]
              (.put _dbObject (.getColumnName _md (inc _i)) (.getObject _rs (inc _i)))
            )
            (.insert _coll _dbObject (WriteConcern/NORMAL))
          )
        )
      )
    )
    
    (with-open
      [
      _mongo (new Mongo _mongoHost)
      _conn (DriverManager/getConnection _dbmsUrl _dbmsUser _dbmsPass)
      _rs (.executeQuery (.prepareStatement _conn "select tablename from pg_tables where schemaname = 'public'"))
      ]
      (let [ _mongoDB (.getDB _mongo _mongoDBName) ]
        (while (true? (.next _rs))
          (let
            [
            _tableName (.getString _rs 1 )
            _coll (.getCollection _mongoDB _tableName)
            ]
            (_copyData _conn _tableName _coll)
          )
        )
      )
    )
    
    (System/exit 0)
    

    実行には以下のjarファイルが必要

  • Clojure
  • JDBC Driver
  • MongoDB Driver

  • DoormanがEclipse Marketplaceからインストール可能に



    0×00. Eclipse Marketplaceとは


    Eclipse Market Placeとは、Eclipseプラットフォーム上で動作するプラグインや製品などを検索したりダウンロードしたりすることができる仕組みである。ウェブサイトとしてはhttp://marketplace.eclipse.org/となっており、ブラウザからも使用できるほか、Eclipseの3.7以降からは直接メニュー(Help->Eclipse Marketplace)からアクセスできるようになっている。特にEclipseから直接使う場合、ソフトウェアのダウンロードからインストールまでできて便利な仕組みになっている。開発者は自分の開発したソフトウェアを登録することで、より多くのユーザにアピールすることができるようになる。いわばEclipse版AppStoreである。


    0×01. 登録は簡単


    以前のエントリにまとめたように、以前はスタンドアローンのアプリケーションであったDoorman@JUMPERZ.NETを「Doorman Eclipse Plugin」という名前のEclipseプラグインに移植した(というか、プラットフォームをEclipseに変更した)。そこで、せっかくAppStore的な場所があるのならば使ってみようと思い、手続きを行ってみた。自作アプリケーションを公開する場合、今どきの開発者ならばAppleのAppStoreかGoogleのAndroidマーケットあたりが普通だと思うが、筆者は斜め上のEclipse Marketplaceである。

    Doormanのようなオープンソースのプラグインの場合、Eclipse Marketplaceへの登録にはお金は必要ない(商用のものがどうなのかは要調査)。お金どころか特に身分証明も必要ない。ガイダンスにしたがってアカウントを作成し、ソフトウェアの情報を記入すると、数日の審査期間を経て登録が済んだ。現在無事にこちらで公開されている。分類も、自分が選んだとおりに「Tools」「Network」となっている。


    0×02. アップデートサイトも用意した


    以前のエントリに書いたようにDoormanのアップデートサイトは用意しないつもりだったのだが、Eclipse Marketplaceを眺めていて考えが変わった。意外と多くのソフトウェアがきちんとアップデートサイトを用意しているのだ。もしかしたら今どきのEclipseユーザはjarファイルを直接pluginsフォルダに突っ込むなどということはしなくなっているのかもしれない。Eclipseからのアップデートサイトの使い方も、なれてしまえばURLひとつで行けるため楽な気もする。ということでjumperz.net上に/update/というパスを用意し、そちらでEclipseからのリクエストを受け付けるようにした。現在Doormanだけでなく、ExEditもインストールできるようになっている。

    アップデートサイトURL: http://www.jumperz.net/update/

    Marketplaceに登録されたため、Eclipseのメニューから直接Marketplaceを開き、「Doorman」で検索すればインストールができるようになっている。

    これは案外素晴らしいかもしれない。


    0×03. 新機能JSフック

    今回の登録と時を同じくして、DoormanJSHookという新プラグインを追加した。Doorman上でリクエストやレスポンスをフックし、その際JavaScriptで処理を書けるようにするものである。以前からClojureで処理を書くことができるDoormanHookは存在していたが、さすがにClojureで書くやつはあまりいなさそうなので、Java上でデフォルトで使えるRhinoを使うものを作成した。User-Agentの書き換えを行いたい場合などやCookieのコントロールを行いたい場合などに、if文で条件を付けることが簡単にできるので便利である。

    例えば「Googleにアクセスする際にはCookieを削除し、User-AgentはAnonymousという文字列にする」という場合は以下のように書けばよい。

    var host = request.getHeaderValue( 'Host' )
    if( host.indexOf( 'google' ) > -1 )
    	{
    	request.removeHeaderValue( 'Cookie' )
    	request.setHeaderValue( 'User-Agent', 'Anonymous' )
    	}
    

    書き換えを行う場合にはnet.jumperz.net.MHttpRequestクラスなどのメソッドを呼び出すことになる(上記ソースコードにおけるrequestはこのクラスのインスタンスである)。このあたりのAPIなどはドキュメントすら存在していない(なぜなら仕様がコロコロ変わる可能性を秘めているから)ので、詳しくはソースコードを参照いただければと思う。あるいはtwitter上で@kinyukaで質問していただければお答えできる。まぁ、とにかくマニア向けのソフトウェアということである。


    0×04. SSL証明書の表示機能

    また、順番としてはDoormanJSHookよりも前になるのだが、SSL通信時にどのような証明書チェーンが利用されているのかを簡単に確認できるような機能を追加してある。これは筆者が自分で欲しかったので作った機能だ。

    ウェブサーバの管理者ならばご存じかと思うが、SSL証明書を自分でインストールする場合、中間CA証明書というよくわからないものをインストールしたりすることがある。この際やっかいなのが、「IEではエラーにならないのに、Firefoxではエラーになる」のような場合だ。中間CA証明書がIEにはもともと入っているのに、Firefoxには入っていないような場合で、かつサーバ上にインストールし忘れているような場合にこれが起こる。

    SSLを使用しているウェブサイトを訪問中、現在どのような証明書が使われているのかをウェブブラウザから確認する場合、手順が少し面倒くさい。まず証明書のダイアログを開くまでの手間がけっこうかかる場合がある。そしてさらに、証明書のチェーンを表示するダイアログにおいて、中間CA証明書が果たしてサーバ側にあるものなのか、あるいはローカルにインストールされているものなのかが確認できないことが多い。

    Doormanではこの部分の不満を改善するため、以下のようにCertificate ListとCertificate Detailという2つのViewを用意した(クリックで拡大)。

    Certificate Listの一番左のカラムは証明書がサーバ側なのかローカルなのかを示しており、ここでは2つの証明書(ウェブサイトの証明書とそれに対応する中間CA証明書)がサーバ側にインストールされていることがわかる。また、Expカラムでは期限切れまでの日数を表示している。

    ちなみに証明書の正当性について「Validation: OK」という表示をしているが、これはDoormanの独自基準(証明書のチェーンだけ見る。ウェブサイトのFQDNと証明書のコモンネームは見ない。実行中のJREに含まれるルート証明書でチェック)での判定なので、あまり当てにしない方がよい。基本的にはウェブブラウザで直接アクセスして確認するのがベストだ。ブラウザには入っているのにJREには入っていない証明書などもあるため、DoormanでNGとなってもブラウザでは問題ないケースがある。

    この機能を作ったことで、筆者の業務でのSSL関連の運用作業はずいぶん楽になった。


    0×05. 登録してみての感想

    Eclipseプラグインの開発自体がかなりマニアックな存在となっていることもあり、Eclipse MarketplaceもAppStoreやAndroidマーケットと比べると3桁か4桁くらい登録されているソフトウェアが少ない(反面、ソフトウェアの平均的な質は高い)。Doormanを登録した先のジャンルである「Network」にわずか16個しかソフトウェアが存在しないことからも、Marketplaceの寂れっぷりが孤高の存在であることが伝わってくる。

    ぶっちゃけ登録しても全然ダウンロードされないだろうと思っていたのだが、アクセスログを確認してみたところ意外とそうでもなかった。オランダ・ブラジル・中国など世界各国からそこそこダウンロードされているようだ。(ところで、Eclipseが直接ダウンロードしにくるわけだが、その際のUser-AgentがJakarta Commonsになっていた。Eclipseにしておけばいいのに…)

    ぼちぼちでも利用者が増えてくれるとうれしいので、登録して正解だったと考えている。


    0×06. 孤高のプログラマを目指せ

    ということでClojureやEclipseプラグインの開発などを行っているのだが、国内では書店に行ってもどちらも関連書籍が1冊ずつしかないような有様でかなり孤高な感じである(英語書籍ならそこそこ出版されているのだが…)。Clojureは国内でもじわりとユーザを増やしているような気がするので、Eclipseプラグインの方にも頑張ってもらいたいと考えている日々である。


    0×07. 追記

    JSHookとHookはどちらもBreakさせるために使うことも可能となっている。Hookのコードが戻り値として”break”を返す場合にはBreakを設定したのと同じようにリクエストやレスポンスの編集が行えるようになる。

    例えば「リクエストがPOSTで、かつレスポンスにSet-Cookieヘッダが存在する場合にはBreakする」という場合には以下のようなJSHook(TypeとしてResponseを選ぶことに注意)を記述する。

    if( request.getMethod() == 'POST' && response.headerExists( 'Set-Cookie' ) )
    	{
    	'break'
    	}
    

    このように、複雑な条件でのみBreakさせたいという(あるのかどうか極めて微妙な)ニーズに対応した。


    ClojureのSTMは使い物にならない


    0×00. Clojureがいけてる件について


    ここ数ヶ月でClojureをどんどん実戦投入してみているが、その成果は素晴らしいの一言に尽きる。Javaでは考えられなかったほどスマートかつ柔軟にデータ処理が可能であり、「あれ、こんなに短い記述でできちゃうのか!」と驚かされることが多い。そんなわけで、何でもかんでもJavaで片付けてきた筆者はここにきてClojureにかなり惚れ込んでおり、電子書籍やらウェブサイトやらで本格的に情報収集を進めているのだが…


    0×01. Clojureの並列プログラミング


    現時点では、Clojureを実戦投入したのは、ちょっとした処理に使うツール的なものだけである。理由は単に、筆者がまだClojureの初心者だからだ。しかしそろそろメインの仕事であるサーバアプリケーションやウェブアプリケーションでも使いたくてウズウズしてきており、そのような視点からさらに調査を進めている。

    サーバアプリケーションやウェブアプリケーションでは並列プログラミング的なことをよくやるのだが、ClojureではこれはSTMを使って処理するのが王道のようだ。Clojureの解説を行う本などでは、STMは従来のJavaのロック地獄から解放してくれる救世主的な扱いをされており、筆者としては非常に期待していた。Javaでのマルチスレッドプログラミングは確かになかなか難しい部分があり、コードレビューなどで問題点を見つけ出す技術には経験に基づく勘が要求される。単に並列プログラミングを行いたいだけなのにメモリバリアーが〜とか言われるため、ある種バッドノウハウ的な側面がある(筆者は個人的に大好きだが)。

    ClojureのSTMはぱっと見た感じでは拍子抜けするほどシンプルであり、「これで本当に動くなら、今までのJavaの並列プログラミングは何だったんだ…」と思う仕上がりになっている。


    0×02. 遅いらしい


    筆者の見落としである可能性もあるが、Clojureの解説を行う本(Programming ClojureとPractical Clojure。ついでに現在The Joy of Clojureを読んでいるところ)では、STMのパフォーマンスに関する記述はなかったように思う。そのため、普通に読んでいる読者は、間違いなく「並列プログラミングが必要とされる場面ではSTMを使えばよい」と考えるだろう。筆者もそう思っていたのだが、ある日Clojureに的を絞っているわけではない書籍である「Programming Concurrency on the JVM」を読んでいたところ、「STMはReadが多く、Writeが少ない場面だけにしておくべし」的な記述があって驚いた。Clojureでは基本的に並列プログラミングにはSTMしか選択肢がない(もちろんClojureからJavaのスレッドを生成したりすることもできるが、それならばsynchronized等の文法がサポートされている生のJavaを使った方が百倍開発しやすいため、却下)のに、書き込み競合が多い場面ではSTMが使えないのでは、開発が成り立たないのではと思ってしまうが…。この書籍内では実際にSTMが遅くて使えないケースをソースコード付きで掲載している。

    そんなわけで、単純な例でベンチマークを取ってみることにした。


    0×03. ベンチマークの内容


    マップをひとつ生成する。このマップに対して多数のスレッドから書き込み競合が多く発生するようなアクセスを行う。具体的には、各スレッド内でランダムにキー(100種類のうちの1つ)を生成し、そのキーに対応する値を取得する。値は数値とする。この数値をひとつインクリメントし、再びマップにセットする。マップ内にキーが存在していない場合には1をセットする。スレッドは300個生成し、上記のインクリメント動作はそれぞれのスレッドで10万回ずつおこなう。これをJavaとClojureでそれぞれ記述したものが以下である。

    Test2.java

    package test;
    
    import java.util.*;
    
    public class Test2
    {
    public static final int _keyRange = 100;
    public static final int _threadCount = 300;
    public static final int _repeat = 100000;
    //--------------------------------------------------------------------------------
    public static void main( String[] args )
    throws Exception
    {
    final long start = System.currentTimeMillis();
    final Map map = new HashMap();
    
    Runtime.getRuntime().addShutdownHook( new Thread( new Runnable()
      {
      public void run()
        {
        System.out.println( System.currentTimeMillis() - start );
        System.out.println( map );
        }
      } ) );
    
    for( int i = 0; i < _threadCount; ++i )
      {
      new Thread( new Runnable()
        {
        public void run()
          {
          for( int k = 0; k < _repeat; ++k )
            {
            Random random = new Random();
            String key = "key" + random.nextInt( _keyRange );
            
            synchronized( map )
              {
              Integer value = ( Integer )map.get( key );
              if( value != null )
                {
                int oldValue = value.intValue();
                map.put( key, new Integer( oldValue + 1 ) );
                }
              else
                {
                map.put( key, new Integer( 1 ) );
                }
            
              }
            
            }
          }
        } ).start();
      }
    }
    //--------------------------------------------------------------------------------
    }
    

    bench.clj(注:間違いあり。修正版は0×06項目を参照)

    (ns bench)
    (import '(java.util Random))
    
    (def _keyRange 100)
    (def _threadCount 300)
    (def _repeat 100000)
    (def mapref1 (ref {}))
    (def _start (System/currentTimeMillis))
    (def _agents (seq (repeat _threadCount (agent nil))))
    
    (defn fn1 []
      (let
        [
         _random (new Random )
         _key (str "key" (.nextInt _random _keyRange))
        ]
        
        (dosync
          (let [ _oldValue (get @mapref1 _key) ]
            (if (nil? _oldValue)
              (alter mapref1 assoc _key 1)
              (alter mapref1 assoc _key (+ 1 _oldValue))
            )
          )
        )
      )
    )
    
    (defn fn2 [ dummy ]
      (dotimes [ _index _repeat ]
        (fn1)
      )
    )
    
    (doseq [ _agent _agents ]
      (send-off _agent fn2)
    )
    
    (defn fn3 []
      (println (- (System/currentTimeMillis) _start))
      (println @mapref1)
    )
    
    (doseq [ _agent _agents ]
      (await _agent)
    )
    
    (fn3)
    (shutdown-agents)
    (System/exit 0)
    


    0×04. ベンチマーク結果


    この2つのプログラムを実行してみると、Java版は6〜7秒程度で終わるが、Clojure版は60秒程度かかる。Clojure版はざっと10倍程度遅いことになる。(これは書き込み競合が極端に多い例であり、ClojureのSTMが最も苦手とするケースではあることに注意が必要だ。Writeが少なく、Readが多いケースでは、Javaより速い場合もあるかもしれない。)


    0×05. STMは使えないと思う理由


    筆者はこの結果を受けて、「ClojureのSTMは使えない」と判断した。

    「Readが多いケースならば、STMもよいのでは」という意見もあるかもしれない。しかし開発している最中に、いちいち「この部分の競合ではReadが多いか?」などと考えるのはナンセンスだ。

    また、サーバアプリケーションに予想以上のアクセスが集中するなどのケースで、「読み込みが大部分だろう」と思っていた箇所で書き込み競合が多く発生してしまったら悲惨なことになってしまうかもしれない。そして、そもそも書き込み競合が多い場面で使えないのでは、別の技術も勉強する必要があり、無駄である。

    さらにもう一つの理由として、「正しいコードかどうか」の判断を付けることができないという点があげられる。Javaのロックベースのマルチスレッドプログラミングは、確かに落とし穴が多い。しかしFindBugsのようなツールやコードレビューなどによって、デッドロック等の問題が発生する可能性などを見つけることができる。また、実際にデッドロックが起こってしまった場合などにも、問題点がはっきりする。問題点が見つかれば、それを修正していくことで、「正しいコード」に近づく。そしてある時点で、ほぼ確実に正しく動作するコードになった、と自信を持つことができるようになる。そしてそのコードは速い。

    ClojureのSTMでは、実際にアプリケーションを完成させてテストするまで、「性能がでるかどうか」つまり「正しいコードかどうか」を判断することができない。サーバアプリケーションではしばしばテスト時点では想像もできない複雑なパターンのアクセス集中が発生することが予想されるため、いつまでたっても「このコードでいける!」と自信を持つことができなくなるだろう。また、問題があった場合の修正はアプリケーションの作り自体の変更になる可能性があり、Javaでのデッドロックの修正よりもよほど大がかりになる可能性がある。これはかなりの茨の道になるだろう。

    この筆者の判断は現時点でのものなので、数年後にSTMが爆速になっていたりすれば、もちろん使ってみるつもりだ。また、性能が問題にならないようなちょっとした処理であればもちろんSTMは便利に使えるだろう。


    0×06. 追記(コードのミスを修正)


    コメント欄でTakahiro Hozumiさんより非常に有意義なコメントをいただいた(ありがとうございます!)。やはりSTMのパフォーマンスには問題があるらしい。また、上記の筆者のClojureコードにはミスがあり、実際には300スレッドを生成できていないことがわかった。

    修正版を作成したので以下に掲載する。

    bench.fixed.clj

    29行目をコメントアウトすると、確かに300スレッド生成できていることが確認できる(また、ps -eLf等のコマンドでも確認済み)。

    問題のパフォーマンスだが、修正後のコードを使って300スレッド生成してみるとさらに遅くなることがわかり、Javaより10倍どころではなく、恐ろしく遅くなることがわかった。そのため、0×05で示した本稿の結論的なものは変わらない形となる。


    0×07. さらに追記


    Clojureでは(今回のテストのように)単純に多くのスレッドを生成したいだけの場合にはagentの使用は不適切であるとコメント欄で指摘していただいた。このような場合にはagentの使用には大きなオーバーヘッドがあるようだ。The Joy of Clojureの11章がこのあたりに触れた説明となっており、とても参考になる。

    本稿で目的としているテストを正しく行うためのコードはTakahiro Hozumiさんが作成してくれたhttps://gist.github.com/3048d90328d3118583a4の(ref-bench)であり、パフォーマンスはJavaの10倍程度遅いということになるようだ。

    少し話題が反れるが、Programming ClojureやPractical Clojureを読んだ印象はまさに「Clojure=シンプル」であったのだが、The Joy of Clojureの11章の印象は(悪い意味で)かなり違う。現実的にはそれほどシンプルな記述で並行プログラミングを実現できる、というわけではなさそうだ。lockingなどは普通にデッドロックを起こす可能性があるように思えるためClojureらしくないと感じる。


    0×08. またまた追記


    詳しくはコメント欄を参照していただきたいが、最終的には以下のようなことになるようだ。

  • Agentを使ってスレッドを作ること自体のオーバーヘッドはそれほど大きくない
  • (書き込み競合が多い場合の)STMは非常に重く、Javaの10倍どころではない時間を要する

  • Eclipseからexコマンド(いわゆるviのコマンド)を実行するプラグインをリリース


    0×00. exコマンドとは


    Eclipse上で開発する場合、悲しいのはviエディタが使えないことである。過去にviに似せた操作を可能とするプラグインを試したことがあるが、満足行く出来ではなかった。そのため、いつしか筆者は「Eclipse上ではごく普通のエディタ操作を行い、ターミナル上ではviを使う」という使い分けに慣れていった。

    EclipseのJavaエディタ等も、別に慣れてしまえばそれほど問題はない。筆者はThinkpadタイプの赤ぽっち付きキーボード愛用派であることから、カーソル操作についても手のポジションを変更せずに行えるというのが大きいのかもしれない。EclipseのJavaエディタではコード補完等の便利さが際だつため、特にviじゃなくてもいいな…と思いながらいつのまにか時は過ぎていた。

    しかし、このように使い分けることを決めた後でも、いつも「ぬぁ〜、これが使えればナァ(;´Д`)」と不満に思うのが、exコマンドだ。

    「exコマンド」と言われてもぴんと来ないかもしれない。プログラマの中には「三次元は面倒くさい。二次元萌え〜」などと言うモノがいるようだが、これをさらに突き詰めると、一次元萌えとなる。つまり線である。このようなプログラマは通常よく使われる二次元(複数行を面で表示する)のエディタではなく、1次元(1行しか表示しない)のエディタを好む。これがexエディタである。実はあなたのシステムにも普通にexエディタはインストールされているだろう。通常/usr/bin/exにある(Windowsを除く)。

    冗談はよいとして、要するにexはviエディタでコロンに続いてコマンドを実行するやつの実体である。詳しくはWikipediaを参照のこと。例えばviエディタで編集中に、3行目から5行目まで削除したい場合などには

    :3,5d
    

    などとする。これが慣れると病みつきになる便利さである。筆者はviの覚え立ての頃は「viエディタの良さは、カーソルの移動がキーボードでできることだ」と思っていたのだが、慣れてくるとexコマンドが使えることこそがviの良さであるという風に認識が変化した。


    0×01. Eclipseプラグインからexコマンドを呼び出す


    Eclipseのエディタでもexコマンドが使えれば便利になると考え、プラグイン(名前はExEdit)を作成した。閉店間際のスターバックスコーヒー東戸塚店で1時間、さらに帰宅後に1時間で動くモノができた。使用イメージは以下のようになる。

    以下の画像で、Javaエディタ部分に記されているコードには特に意味がない。ここでは3行目からhoge、fuga、gyoeというコメントが書かれていることに注目してほしい。

    まず、3行目を5行目に移動させてみる。以下のように、ExEditという小さなViewのテキスト入力欄にexコマンド(ここでは「3m5」となる)を入力し、Enterキーを押す。すると次の画像のように3行目(hogeと記述されている行)が移動する。

    2番目の例として、3行目から5行目までを削除してみる。exコマンド(ここでは「3,5d」となる)を入力し、Enterキーを押すと、以下のようになる。

    このようにEclipse内部でexコマンドが非常に自然に使えるようになり、とても便利である。


    0×02. ダウンロードとインストール


    ダウンロードは以下のURLから。

    http://www.jumperz.net/tools/net.jumperz.app.ExEdit_0.9.0.201104162218.jar

    このファイルをEclipseのpluginsディレクトリ以下に直接コピーし、Eclipseを再起動することでインストールされる。「General」に分類されているExEditというviewを開けば使えるようになる。

    jarファイル内にはソースコードも含まれている。ライセンスは(とかいうほどのものでもないのだが)GPLとする。


    0×03. 使用上の注意など


    このプラグイン実装は内部でexコマンドを実際に呼び出しているため、exコマンドがインストールされていない環境では動作しない。つまりWindowsではほぼ動かないと思ってもらった方がよい。UbuntuとMacOSでは動作確認済みである。

    Eclipse側でファイルを編集した後に、かならず保存してからExEditを使う必要がある(編集中の内容が保存されていない場合は、エラーダイアログが出るようになっている)。

    ExEditは裏側でexコマンドのプロセスを起動して直接対象のファイルを書き換える。そのため、Eclipseは外部でファイルが書き換えられたことを検知してダイアログ(変更された内容をワークスペース側に反映させるか?Yes/No)を出し、ユーザがyesを選択する(単にEnterキーを押せばいい)、という動作が正常な動作となる。上の例ではシンプルに動作を示すためにこのダイアログが出てくることを省略している。

    ファイルを直接書き換えるため、万が一の事故には注意して頂きたい。当然ながら当方は使用に当たって一切の責任を負わないものとする(Eclipseが普通に履歴を保存してくれるので万が一データが消えても問題ないと思うが…)。

    また、ExEditは非常に小さなスペースで済むので、viewのサイズは最初に開いた際に小さくするのが吉である。


    0×04. その他お願いなど


    なかなか普通に便利なプラグインだと思うので、是非使って頂き、@kinyukaまでフィードバックしてもらえればうれしい。また、アイコンが超手抜き(2秒くらいで作った)なので、作っていただければありがたい。

    それではみなさま、素敵な一次元ライフを。


    Follow

    Get every new post delivered to your Inbox.