読者です 読者をやめる 読者になる 読者になる

non vorrei lavorare

ブログ名の通りです。javascript three.js mruby rust OCaml golang julialang blender

macOS Sierraにしたらrustが暴走して、黒い画面には入れなくなった #解決済み

パソコン・インターネット

こんにちは、最近長男は、ワイルドスピードシリーズがお気に入りで、ダンボールで車の模型を作って遊んでいます。kjunichiです。

思わぬ原因不明の落とし穴に落ちた

macOS Sierraにしたらrustが暴走して、Terminal.appが立ち上がるも入力のプロンプトが いつまでたっても(10分くらい待ってたw)表示されなくなりました。

負荷も高い状態がずっと続き、Spotlight経由でEmacsを動かして、M-x shellで中を見ようとするも、 こちらも立ち上がらず、一時、途方にくれましたが、

アクティビティモニタを確認すると、rustcが複数実行され、そろぞれCPUを90%以上使った状態でした。 (ターミナルのタブ数+emacsのrustcが実行されてる感じでした。)

f:id:kjw_junichi:20160922101920p:plain

ここで、ターミナル上でCtrl+Cしたら、rustcが終了して、無事にプロンプトが現れ、コマンドラインが復活しました。

なんとなく、ちょと前に導入された、ターミナルでの作業状態を復活させる機能に絡んで 何か悪さをされている気がしたので、過去のセッション情報を

rm ~/.bash_sessions/*

で消すも、状態変わらず、

bash -l

しても、同様にrustcが実行されてしまう状態のまま。

原因不明だが、Rustの再インストールで解決した模様

コマンドラインが使えない状態は早急に解決したかったので、

curl -sSf https://static.rust-lang.org/rustup.sh | sh -s -- --uninstall

して、一旦rustを削除して、terminal.appを再度立ち上げ、正常にログインできていることを 確認して、rustを再びインストールしたところ、これで治ったようです。

cordovaのプラグインを作ってkindle fireでmrubyを動かしてみた

おはようございます。次男のトイレトレーニングが順調に進んで、保育園でもパンツで過ごしています。kjunichiです。

4千円しないAndroidタブレットを活用しないのはもったいない

Amazonプライムデーで4千円ださずに購入したkindle file。長男が操作をいつのまにか覚え、車のゲームを あれこれDLして遊んでるところを奥さんに見つかり、大目玉を喰らってます。

今回はそんなkindleにmrubyを動かそうとした記録です。

準備編

Androidの開発はプライベートでは既に奥さんの仕事向けにWebViewをつかったやつを大昔のEclipseで 作成したことがあるくらいなので、ぼんやりとは把握してるつもり。そして、イマドキはEclipseではなく、 Android Studioで行うのが一般的らしいというのは予習済み。

Android Studioの環境作成

OSXで主に今回の作業を進めた。Windows 10でも一部やったが、OracleJDKを入れて、 Android Studioをインストールすれば、すぐに動く感じ。 (Android SDK、NDKのDLや仮想マシンのDLでかなり時間は掛かけど。)

SDKやNDKもAndroid Studioからインストール出来、これを使って作業をしている。

Android Studioを離れての作業でも、これらのパスを押さえておけば、大丈夫そうだった。

mrubyの組み込み

素のmrubyをAndroid向けにビルドするには簡単。

mrubyをAndroid Studioのエミュレーターで動かすためのlibmruby.aを作る - Qiita」に書いたようにlibmruby.aを作って、 これをJNIな.soファイルにリンクして、呼び出せばOK。

挫折編

素のmrubyが簡単に組み込めたので、mruby-cliでのクロスコンパイルに対応してあるmruby-webcamを 組み込めば、

cam = Webcam.new
cam.capture {|img|
  # img : JPEG format
  puts img.length
}
cam.snap

と言った具合に簡単にkindle fireのカメラにアクセス出来ると思い、作業を始めた。

  • NDKって出来ること思ったより少なかった
  • mruby-webcamAndroid向けにビルド可能にしたが、kindleのカメラ使えない問題

現行のFireOSですら、カメラのアクセスはJavaを経由しないと難しい模様。 Nでは許可を得るのにJava必須な模様なので、FireOSがアップデートされたら、こちらもJavaが必須となりそう。

Cordova(Android)編

当初はNDKを使えば、C言語で頑張れば、色々出来るから、それらを上手いこと mrbgemにすれば楽しそう!と考えていましたが、基本Javaを介さないとAndoridのサービス?等を 利用することができなそうなことが分かってきたので、方針転換

  • C->Javaしなきゃ行けないなら、Java->JavaScriptも使ってしまえ! -> Cordovaがあるのか!

putsとgetsの実装

RubyPicoが目標だが

puts,getsが実装できれば、最低限mrubyを動かして、プログラムの実験が出来るのではと思い始めた 。すでにこの段階で、mrubyには2.7系ではあるが、opencvがリンクしてあり、mruby-webcamを 少し改造すれば、カメラで撮影した画像を顔検出位まではmrubyで出来そうな見込みであり、 早々にputs,getsを実装したいことろだった。

先人であるRubyPicoのコードが大いに参考になった。 Rubyはクラスの書換えも自由自在なので、puts、getsを書き換えることが簡単にでき、

  • putsではDOMにdivタグで囲まれたテキストを追加する実装に変更
  • getsではalertをwindow.promptで入力を取得する実装に変更

といった実装で進めた。

躓きポイント(puts実装時)

JavaからJavaScriptを呼ぶには

事前にAndroidでは昔からWebView#loadUrlはあまり望ましくないとの情報を 得ており、WebView#evaluateJavascriptがKitKat以降は使えるとの事だったが、

CordovaのAndroidでは簡単には使えない模様。loadUrlは使えたので、こちらでとりあえず実装を進めた。

といった具合にputsですら苦戦して、getsなどは、スレッド絡んでホントに大変だった。

躓きポイント(gets実装時)

WebView#evaluateJavascriptが使えれば、window.promptへの入力後に呼んでもらうコールバックを設定できて 苦労がなかったはずだが、それが使えなかったので、プラグインのコードでwindow.promptの入力を待つのに苦戦

CordovaのプラグインはUIスレッドで動いているのを理解しておらず、 Thread.sleepして固まった。 これに対応する為に、別スレッドで動かし、window.prompt入力後に Js側のコールバックを実行するように非同期処理にして、getsが出来た。

final CallbackContext cb = callbackContext;
        if (action.equals("mrbLoadString")) {
          final String script = args.getString(0);
            Thread th = new Thread(new Runnable(){
              @Override
              public void run() {
                String jniString = MrubyJni.mrbLoadString(script);
                Mruby.setResult(jniString);
                String message = ". JNI says: " + _jniString;
                Mruby.mrbLoadString(message, cb);
              }
            });
            th.start();

            return true;
}

public static String gets() {
      Log.d("Mruby", "gets");
      waitInput = true;
      inputString = "dummy";
      final String js = "console.log(mruby.__jsGets())";
      stdout.getView().post(new Runnable() {
        public void run(){
          //stdout.sendJavascript(js);
          Log.d("Mruby", "gets/exec js = " + js);
          stdout.loadUrl("javascript:" + js);
          Log.d("Mruby", "gets/exec js done");
        }
      });
      Log.d("Mruby", "gets/wait");
      while (waitInput) {
        try {
          Thread.sleep(200);
        } catch (InterruptedException e) {
          Thread.currentThread().interrupt();
        }
      }
      Log.d("Mruby", "gets/rtn = "+inputString);
      return inputString;
}

成果物

役に立ったこと

デバッグ文代わりのLogとCのマクロが役に立った。 コマンドラインでcordova runコマンドで動かして、AndroidStudioで適当なプロジェクト開いておくと、 出力したログが勝手に表示されて便利だった。

import android.util.Log;

Log.d("Mruby", "gets/wait");
__android_log_print(ANDROID_LOG_DEBUG,"Init", "Java_com_hatenablog_abrakatabura_mruby_MrubyJni_initialize");

IDE環境でのCordovaのプラグイン作成方法が不明なので、improt文含めコピペ。

まとめ

去年書いた記事の「外部のライブラリに依存するmrbgemを使ってもmruby-cliでワンバイナリを作成できるようにした」の作業を通して、クロスビルドの経験が出来ているので、あまり臆せずNDK絡んでの開発が行えた気がする。経験大事。

また、HT-03Aの頃と違い、NDK関連も検索すれば、やりたいことに近い事例や、エラーメッセージへの 対応等サクサク出てて来る。ただ、kindle fireだとそもそもAndroidのバージョンいくつと同等なのか とか、それなりに取り組まないと把握が難しいと感じた。やり出せば、楽。ちなみに現時点では5.1のlollipop相当らしい。

ndk-buildコマンド or Android Studioプラグイン

コマンドラインで生活してる場合、ndk-buildコマンドが楽な知見。

プラグインの設定が面倒。過去になんどか書き方が変更になっているので、検索に注意が必要。

Cordovaの感想

Cordova使ってみて、さくっと子供たちのゲーム教育用アプリを作ってkindleAndroid端末で動かせるので、 お父さんはIT系の仕事やってる感を前面に押し出せそうで、心強いw。

ドキュメントがググると日本語で見つかるが、ザ機械翻訳なので、Androidが人造人間と訳されているなど、 読みづらい。とはいえ、無いよりずっと良い。

また、頻繁にAPIも変更なっているようで、既に出回っているプラグインのソースを呼んで 使い方を理解する等が出来ないと辛いが、それが出来れば、良い感じで楽できそうだった。

今後の課題

現状ではmrubyをEmscriptenでJsに変換して動かすWebrubyと比べて、なんらメリットがなさそうなので、 当初のカメラを使って簡単な画像処理あたりまでの実装。

参考資料

関連記事


あと、CordovaでiOSも試した。去年から無料で実機にデプロイ出来るようになったので、 実機で動かせた。iOS10も出たばかりで、こちらの新しい気のも試したい感はある。

mrubyでHTTP/1.1のKeep-Aliveで複数回リクエストを出してみた

おはようございます。先週は次男が、熱を出し、今週は長男が熱を出しました。長男の熱は、寝不足とkindleの車のゲームのやり過ぎが原因のようですが。。kjunichiです。

サンプルが無い問題

そもそも、HTTPクライアントの例がHTTP/1.0だったり、あってもHTTP/1.1でConnection: close指定という パターンに世の中は溢れていた。

KeepAliveにするも、2回目のリクエストの応答が来ない

s = TCPSocket.open("127.0.0.1", 8080)
s.write("GET / HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n")
puts s.read
puts "<----"
s.write("GET / HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n")
puts s.read
s.close

としたが、2回目のリクエストが貰えない。 当初、発行出来たのかも分からなかった。

そこで、自分が割とつかえるNode.jsでHTTPサーバーをサンプルコード 切り貼りで作成した。

//server.js
const http = require('http'); //httpモジュールのインポート
const server = http.createServer(); 

server.on('request', function(req, res) {
  console.log("onRequest");
  const myhtml = "<html><body><a href=\"/\">link</a></body></html>"
  res.writeHead(200, {'Content-Type': 'text/html',/*'Content-Length': myhtml.length,*/'Connection':'keep-alive'}); 
  res.write(myhtml);// resの中身を出力
  res.end();
})
server.on('connection', function(socket) {
  console.log("A new connection was made by a client.");
  socket.setTimeout(30 * 1000);
  // 30 second timeout. Change this as you see fit.
})
server.listen(8080,"0.0.0.0")

node.jsで作成したサーバー側を念のため、Firefoxでアクセスして、サーバー側はそれなりに動いていることを 確認

Socket#recv

一晩寝たら、CのAPIのrecvを思いだし、Socket#readの代わりにSocket#recvを使えば、行けそうなことが分かった。 Socket#readは接続がクローズされるまでブロックされるから、駄目だったのだ。

Transfer-Encodeing: chunked

node.jsで標準のモジュールでhttpサーバーを作ると、Connection: keep-aliveだと 応答のTransfer-Encodingがchunked指定で返されることが判明。

Content-Length方式なら、楽勝だったのに、まぁ、node.jsならこっちで返す方が、node.jsらしいけど。

chunkedな応答への対応

Transfer-Encodingがchunkedの場合、Body部が以下の形式で返される。

16進数で長さ\r\n
指定された長さの文字列\r\n

0\r\nの有無だけで駄目

いちいち、長さを見て云々より、最終行は0\r\nだから、これの有無を見つければ、良いかというと、 以下のパターンで破綻する。

1\r\n
0\r\n
10\r\n
0123456789abcdef\r\n
0\r\n

というパターンもあり、結局Body部の先頭から地道にパースするしかなさそう。

mrubyで16進数を10進数に変換

Integer("0xff")

でCRubyと同様に10進数に変換出来た。

h2oで確認

h2oの場合、静的コンテンツはContent-Length方式で応答が返された。もしかすると、 ファイルサイズによってはchunked方式で返されるのかもしてないが。。。

また、hhvmを動かし試したら、こちらはchunkedでnode.jsの時は0xffの形式だったが、 phpinfoで試したこともあり、0xffff形式で、返ってくることが分かった。

mrubyにHTTP::Parser

mrubyにHTTP::Parserがあることに気付くが、 \r\n\r\nまで取得できた状態で、パースすると、メソッドがGETに設定されて 返ってきて、結局自前である程度パースして、サーバーからの応答を取得後 こちらを使って、細かなヘッダー部のパースをお願いする使い方が吉なのか?

成果物

def readSomeChunkedData(s,data,body)
  chunks = data.split("\r\n")
  if chunks[0] == "0"
    return true
  else
    for idx in 0..chunks.size-1
      next if idx % 2 == 1
      return if chunks[idx] == "0"

      hex = "0x" + chunks[idx]
      readSize = Integer(hex)-chunks[idx+1].length
      chunks[idx+1] << s.recv(readSize+2) if readSize > 0
      body[0] << chunks[idx+1]
    end
  end
  return false
end

def readChunkedData(s,data)
  body=[""]
  until readSomeChunkedData(s,data,body)
    data = s.recv(1024)
  end
  body[0]
end

def readBody(s,data,hdr)
  if hdr[:isChunked]
    body = readChunkedData(s, data)
  else
    # Content-Length
    readSize = hdr[:contentLength] - data.length
    data << s.recv(readSize) if readSize >0
    body = data
  end
  body
end

def analyzeHeader(header)
  headers = header.split("\r\n")

  for h in headers do
    h.downcase!
    if h.index("content-length:") == 0
      contentLength = h.split(":")[1].to_i
    end
    if h.index("transfer-encoding:" ) == 0
      if h.index("chunked") >0
        isChunked = true
      end
    end
  end
  {:isChunked => isChunked, :contentLength => contentLength}
end

def readHeader(s, data)
  buf = s.recv(1024)
  data << buf
  if buf.index("\r\n\r\n") == nil
    readHeader(s,data)
  end
  data
end

def parse(s,data)

  if data.index("\r\n\r\n") == nil
    data = readHeader(s, data)
  end

  header = data.split("\r\n\r\n")
  p header
  rtn=analyzeHeader(header[0])
  if header.size  == 1
    body = readBody(s, s.recv(1024),rtn)
  else
    body = readBody(s,header[1],rtn)
  end
  body
end

s = TCPSocket.open("127.0.0.1", 8080)
s.write("POST /hhvm/test.php HTTP/1.1\r\nHost: localhost\r\nContent-Length: 3\r\nContent-Type: application/x-www-form-urlencoded\r\n\r\nA=b")
s.write("GET / HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n")
puts parse(s,s.recv(256))
puts "<----"
s.write("GET /hhvm/test.php HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n")
puts parse(s,s.recv(2))
s.close

まとめ

mrubyというか、mruby-socketでHTTP/1.1のKeep-aliveでのコネクションを維持して複数回リクエストをサーバに 投げることができた。

Keep-aliveだとサーバからの応答をそれなりにパースする必要があり、世の中にサンプルが見つからなかったのも、 まぁ、やってみてなんとなく分かった気がする。

Socket#gets的なものを実装した方が、良かった気もするが、それだと、recvの呼び出し回数が増えそうだったので、 やめた。 (\r\nがあるか。recvで確認して、無ければ、サイズ増やして、再度確認、見つかったら、そこまでrecvするといった 具合で、recvの呼び出し回数が増えるのではと)

もしかして、速すぎる最適ってやつかもw。

関連記事