2012年1月6日

Android SDKのサンプル「LunarLander」でエラーが出るので修正してみた

Android SDKに含まれている「LunarLandar」というサンプルを試していて、エラーが出る事があるのに気付いた。

Homeキーなどで他のアプリケーションに移ってから LunarLander に戻って来た時に「Thread is already started」というエラーになる。

surfaceDestroyed で破棄されたThreadに対して、surfaceCreated で再び start() を呼んでしまっている事が原因らしい。

それにしても、標準のサンプルがどうしてこんな状態で放置されているんだろう。。。

ターゲットのVersionを1.5, 2.2, 4.03に変えて試してみたけれどもどれも同じ。
同じくSurfaceViewを使っている「JetBoy」というサンプルも試して見た。これも同じだった。

検索しても日本語の情報はあまり見つからなかった。
SurfaceViewとThreadのonPause、onResumeの処理について - shumach217の日記

英語だといくつかもう少し詳しい説明が見つかった。
Fixing the Lunar Lander example: a threaded gameloop for Android at michi's log 

Android - How to pause/unpause thread in games | Robert Green's DIY

Android crash when app is closed and reopened - Stack Overflow


対策方法としては以下の3つの選択肢が考えられそうだ。

  1. surfaceCreated で thread.start() する前にスレッドが無効な状態ではないか確認する。無効な場合はスレッドを新規作成(new)してから start する。
  2. surfaceDestroyed でスレッドを終了させずに、フラグを用いて擬似的に一時停止状態にする。
  3. surfaceDestroyed でスレッドを終了させずに、 object.wait() を用いてスレッドを一時停止状態にする。

まず1の方法でLunarLanderのソースを修正して試して見た。

これだとエラーは出なくなったけれども、復帰後の画面が真っ暗なまま何も表示されない。おそらく他の変数の内容が復元されていない状態になってしまうからだと思うが、きちんと動くようにしようとすると結構面倒そうなので、あきらめた。

次に2の方法。
変更点は以下の通り。
  • LunarThreadクラスに変数 mInBackground を追加。
  • isInBackground() and setInBackground() メソッドを追加。
  • run() メソッドで、isInBackground() をチェックして true なら 100ms スリープする様に変更。
  • surfaceDestroyed() メソッドでスレッドを停止しない様に変更。
  • surfaceCreated() メソッドで isInBackground() をチェックして true ならスレッドを start() するのではなく setInBackground(false) を呼ぶ様に変更。
変更後のLunarViewクラスのソースはこちら。
Fixed LunarView class in Android SDK Sample LunarLander. — Gist

これで一応問題なくバッチリ動く様になった。^^

ただ、100msのスリープを入れて擬似的にスレッドを一時停止状態にしているだけ、というのが、ちょっと気になる。

どうせならJavaの標準として定義されているwait/notifyの仕組みを使った方が良さそうな気がする。多分その方がCPUリソースの消費が少なくなりそうな気がする。多分。。。

ということで3の方法も試して見た。

変更点は、 run() メソッドでスレッドを待機させる時に sleep() ではなく wait() を使う様にした点だ。

変更後のLunarViewのソースはこちら。
Fixed LunarView class in Android sample LunarLander, using object.wait() method. — Gist


実際に2と3でCPUリソースの消費に違いがあるのか、Eclipseのデバッグモードでスレッドの状態を確認して見た。



2の方法の場合、Statusの表示が「timed_wait」になった後、何もしなくても utime, stimeの値が少しずつ増えていく。やはり少しずつCPUリソースを消費している様だ。

それに対して3の方法だと Statusが「wait」になり、utime, stime の値はピタっと止まったまま増えない。

端末のバッテリーにも多分この方が優しいだろうと思う。

ということで、スレッドを待機状態にする場合は、object.wait() と notify()/notifyAll() メソッドを使う方が良いみたいだ。












2012年1月4日

ASP.NETのセキュリティパッチ適用でエラー「Operation is not valid...」が発生する場合の対処

年末にこの記事を読んで、早速WindowsのWebサーバーにパッチを適用しておいた。
ASP.NET Security Update Shipping Thursday, Dec 29th - ScottGu's Blog

対処されている脆弱性の詳細な説明はこちら。
徳丸浩の日記: Webアプリケーションに対する広範なDoS攻撃手法(hashdos)の影響と対策


ところが今日になってある業務アプリケーションで「エラーが起きる」との連絡があった。

エラーの内容は、


Operation is not valid due to the current state of the object
「The URL-encoded form data is not valid.」

というもの。

いろいろテストして見ると、どうやら年末に適用したセキュリティパッチが原因らしい。

ASP.NET MS11-100: how can I change the limit on the maximum number of posted form values? - Stack Overflow

An ASP.NET request that has lots of form keys, files, or JSON payload fails with an exception and returns a 500 HTTP status code

上のページに解決方法が書かれていたので、何とかエラーは解消した。

web.config の appSettings セクション内に次の設定を追加すれば良い。
    <!-- Post Backで処理可能な最大フィールド数(Defaultは1000) -->
    <add key="aspnet:MaxHttpCollectionKeys" value="5000"/>

設定する値が大きくなるほどPostBackで処理出来るフィールド数に余裕が出来る。

でも、増やしすぎるとセキュリティパッチの意味が無くなるのであまり良くないと思う。


そもそも1000以上ものフィールドをPostBackする様な画面を作るのがおかしい。

とは思うけれども、とりあえず以前動いていたものは動く様にしないと行けないので、当面はこれで回避するしかなさそうだ。

ちなみにエラーが起きたページは、GridViewの各行の中にCheckboxやDropdownが複数配置されていて、ボタンが押された時にグリッド内の全行に対して何らかの処理をするという作りになっていた。それで大体250行以上表示している場合に1000フィールドの制限に引っかかっていた模様。

もちろんページングして一度に表示される行数を少なくしておけば問題は起きないのだけれど、「1トランザクションで全データを対象に処理する」という要件があったためにあえてページングしない様になっていたらしい。

ASP.NETだと一つのFormタグの中に全部の画面要素が入るので、何も考えずに一昔前のVBの延長的な感覚で作ってしまうとすぐにこんな事になってしまう、という良くない例だ。

でも、クライアント・サーバー型システムから移植した様な業務アプリケーションでこういうケースって、実際には結構あるんだろうなあ。