5/4に、ブラウザのタブを前や次の空白タブに移動するFirefox拡張「Go to empty tab」を公開しました。ソースコード一式は、GitHubに公開しています。
本記事では、開発の振り返りとして、公開までの経緯を書いていきます。具体的には、以下のことについて記載します。
私はFirefoxを主に使用しています。
Firefoxはタブを大量に開いても軽いので、タブをつい開きすぎてしまいます。新しいWebページを開くときもタブを閉じるのが面倒なので、とりあえず空白タブを開いて、タブ間に区切りを入れて新しいタブを開いています。
タブ間の区切りとして空白タブを入れている関係上、閲覧のためにも、タブの整理のためにも、いま開いているタブから空白タブに瞬時に切り替えることができれば便利だと思いました。
そのような拡張機能がないか探してみましたが、検索には引っかかりませんでした。そこで、ないなら作るの精神のもと、拡張機能の開発を思い立ちました。
開発を思い立ったのは、2021/2/28のことでした。
しかし、私生活が忙しかったり、他のことを優先していた結果、プロジェクトのディレクトリを作ったのは4/4になりました。
ですので、実質1ヶ月程度で開発を行ったことになります。もっとも、コードの大部分は、最後の数日の間に仕上げました。
拡張機能の開発にはWeb技術、つまりHTML、CSS、JavaScriptを用いることはすでに知っていました。
しかし、それらをある程度書いたのは2015年ごろが最後で、ここ最近はJavaやC++、Pythonばかり書いていました。つまり、Web技術からはすっかり離れていて、浦島太郎状態だったのです。当時は、ECMAScript 2015で導入された機能や構文を使っておらず、「JavaScript: The Good Parts ―『良いパーツ』によるベストプラクティス」などの本を読んでコードを書いていました。
ですので、まずはECMAScript 2015以降のJavaScriptについて調査するところから始めました。
ECMAScript 2015では、糖衣構文ではあるものの、クラスが定義できるようになっています。また、モジュールシステムも整備されています。ただ、モジュールシステムについては、CommonJSなどを使用した、モジュールシステムが言語機能に導入される前の仕組みを解説した情報もあり、少々混乱しました。
また、ECMAScript 2015ではPromiseが、ECMAScript 2017ではasync/await構文が導入され、非同期処理が簡潔に書けるようになっています。以前に書いたコードでは、コールバックからさらにコールバックを呼ぶことでコードの可読性が低下する、コールバック地獄になっていたことを思い出しました。以前、JavaでRxJavaを用いたリアクティブプログラミングを試したこともあったため、RxJSの採用も考えましたが、今回は採用を見送りました。そこまで高度なイベント処理は必要なく、async/await構文でじゅうぶん簡潔に記述できると予想できたためでした。
ビルドツールも充実しています。当時の私は、JavaScriptはブラウザ上でコンパイルせずに動作することもあり、JavaScriptファイルに対してビルドツールを使うという発想はありませんでした。昨今のビルドツールでは、JavaScriptファイルをひとつに結合したり、minifyなどの最適化を施すことができたりなど、大幅に進化しています。ここまで来ると、コンパイラ言語と変わらない部分もあるように思いました。
よく分からない分野については、まずは長い物には巻かれよ、ということで、「State of JavaScript 2020」の利用度を基本に、あとは好みで決めました。
Node.jsのLTS版である14系をインストールしました。何はともあれ、Web技術を用いた開発は、最近はこれがないと始まらない印象を受けました。
拡張機能の開発を行うには、web-extが必要です。このツールで、拡張機能の実行やビルドを実行します。
開発中のタスクを自動化する観点から、ビルドツールを探したところ、webpack(https://webpack.js.org/)を採用しました。複数のファイルをひとつに結合したり、minifyなどの最適化を行うのであれば、個々の作業を行うコマンドを手打ちで実行するよりも、ビルドツールのコマンドひとつで実行できたほうが便利だからです。
また、web-extのプラグインもインストールしました。
小規模な拡張機能なので、JavaScriptを用いて実装しても良いと初めは考えていました。しかし、どうせビルドをかけるなら、より堅牢な代替言語が使用できないか、検討を始めました。問題は後で見付かれば見付かるほど修正に時間がかかるため、ビルドの時点やコードを書いている最中に問題を検出できるなら、それに越したことはありません。
そのため、TypeScriptを採用しました。
書き捨てのスクリプトやごく小規模なツールを除いて、可能な場合はテスト駆動開発を行ってきました。最低限の品質を確保できるという利点もありますが、動作するコードが少しずつできあがっていくので、開発のモチベーションを保ちやすい点が自分にとっては大きいと思います。
そのため、ユニットテストフレームワークとしてJESTを採用しました。
静的コード解析とスタイルチェックを同時にできるのでESLintを採用しました。
規模が小さい拡張機能ですが、実行前にバグを検出できるということ、またJavaScriptらしいコードを書きやすくなるため、採用しました。
スタイルには、「Airbnb JavaScript Style Guide」を採用しました。採用についてこだわりは特になく、ESLintの初期化時に、使用するスタイルガイドとして初めに選択されていたためです。
拡張機能の開発は、次のように進めていきました。
拡張機能の作り方や、タブ関係のAPIについて公式のドキュメントを調査しました。例えば、次のページが役に立ちました。
拡張機能の入門には「Your first extension」を参照しました。実際にサンプルの拡張機能を作りながら、学ぶことができました。
タブ関係のAPIについては「Working with the Tabs API」を参照しました。
TypeScriptやJEST、webpackを使用した拡張機能の例として、「Rikaichamp!」を参考にしました。
また、各種ツールのドキュメントも参照しました。
TypeScriptの学習には、「プログラミングTypeScript――スケールするJavaScriptアプリケーション開発」を読みました。JavaScriptの知識を前提として、TypeScriptに特化した記述が多めになっており、参考になりました。
開発した拡張機能はOSSとして公開するつもりでしたので、今回はMITライセンスを選択しました。
これまではソースファイルのヘッダにライセンスヘッダを入れていましたが、今回はSPDXを使用しました。これで、ファイルを開いた直後の画面がライセンスヘッダに占領されることがなくなり、見通しが良くなったように思います。ただ、SPDX側としては、ライセンスヘッダがライセンスに定義されている場合は、それも入れることを推奨しています。MITライセンスでは、ライセンスヘッダは定義されていないので、SPDX short-form identifiersのみ記載しています。
こうして拡張機能を開発する準備は整いましたが、疑問がひとつありました。
そもそも、空白タブはどのように見付ければ良いのでしょうか。タブに紐付くURLを見れば良さそうですが、Firefox上で新規タブを開いても、アドレスバーには何も表示されません。
そこで拡張機能の開発の練習がてら、タブの情報をブラウザーコンソールに出力する拡張機能を開発しました。その結果、タブに紐付くURLが以下のいずれかの場合に、空白タブだとわかりました。
これで、拡張機能を開発する準備が整いました。
テスト駆動開発を行うため、まずは簡単なテストコードを書きました。エディタの補完機能を利かせるために、メインコード側は中身が空の関数や型を定義し、テストコードを書きながらそのインタフェースを調整していきました。
ここで問題になるのは、Firefoxが提供するAPIをテストコード上で扱う方法です。例えば、タブ関連のAPIや、DOMがそれに当たります。
今回は、その部分はテストコードの対象外としました。もちろん、モックオブジェクトなどの方法でそれらのコードをシミュレートすることはできます。しかし、テストコードを書くほど時間的コストをかける必要はないと判断しました。その部分と、切り替え先の空白タブを見付ける処理は明確に分離することができ、またFirefoxが提供するAPIとの接合部分はごくシンプルなものだったためです。
結果、切り替え先の空白タブを見付ける処理についてのみテストコードを書くことにしました。今後、Firefoxが提供するAPIとの接合部分が複雑になれば、今回は対象外としたその部分についてもテストコードを書くかもしれません。
テストコードを書き足しながら、拡張機能のメインコードも書き足していきました。
JavaScriptは久しぶりで、TypeScriptは初めてだったため、簡単なロジックミスをテストコードが検出することも何度かありました。小さい拡張機能ながら、これだけでも十分にテストコードの効果があったと思います。デバッグも、修正ができたか確認するのも、テストコードを実行するコマンド一発でできました。これでテストコードがなければ、Firefox上で毎回拡張機能を操作しながら、デバッグを繰り返すことになっていたでしょう。テストコードのおかげで、時間が節約できたと思います。
拡張機能の開発者は私一人ですが、次にいつこの拡張機能を編集するかわかりません。将来の自分のためにも、モジュールからexportされる型や関数には、ドキュメントコメントを記載しました。形式は、TSDocに従いました。
ところで、拡張機能の中では、enumが適していそうな部分があります。しかし、TypeScriptのenumには問題が指摘されているため、Union Typesを用いて定義しました。
これで拡張機能は一通りできあがりました。公開するうえで、デフォルトアイコンのままでは他の拡張機能と区別が付けづらくなるため、アイコンを作成することにしました。
アイコンの形式には、SVGを選びました。複雑なアイコンを作るつもりはなかったことと、解像度に合わせてアイコンの画像ファイルを用意するのが面倒に思ったためです。
アイコンは、Google Fontsで提供されているタブのアイコンを加工して作成しました。
アイコンのライセンス確認すると、 Apache License Version 2.0で、帰属の表示は必須ではないとのことでした。
初めはDraw.ioを使用してアイコンを加工しようと思いました。しかし、画像サイズをピクセル単位で指定できませんでした。そのため、Inkscapeを使用しました。
これで一通り拡張機能の公開の準備が整ったので、AMOへ登録を行いました。なお、AMOとは、アドオンが配布される公式サイトaddons.mozilla.orgのことです。
AMOでは、以下の2種類から配布方法を選択できます。
なお、審査のために、ソースコード一式のアップロードが必要な場合があります。minifyなどの最適化や、webpackなどを使用してファイルを結合した場合は、必要となります。また、アップロードする場合、READMEファイルか、別途表示されるページに、拡張機能をビルドする方法やテストする方法を記載する必要があります。自主配布では不要かと思いましたが、実際には登録を求める内容が表示されました。
今回はwebpackを使用してファイルの結合とminifyを行っているため、ソースコード一式のアップロードを行いました。
まずは、自主配布の方法を試しました。バージョン0.0.1は、このために登録しました。これで実際に、AMO上からアドオンをインストールして正常に動作するか確認できました。
5/4の17時頃に、公開のためにバージョン0.0.2をアップロードしました。審査待ちの状態となり、57個中57個と、拡張機能の審査を待機する列の長さが表示されました。
そして、21時頃に審査に合格し、拡張機能が公開されました。
]]>もともと、パケットキャプチャツールに対してはどこか近寄りがたいイメージがありました。ネットワークに流れる他人のパケットをキャプチャして解析というのは、いかにもクラッカーがすること、のようなイメージを抱いていたのです。
しかし最近、身の回りでネットワークのトラブルを見るにつけ、パケット解析の必要性を感じるようになりました。特定のホストだけインターネットに繋がらないといった、事象の切り分けが容易な場合は、ホストからpingを打ってみる、ホストの設定を見直すなど、ホストの操作だけで事足ります。しかし、全ホストが繋がらない、ネットワークが異様に遅いなど、事象の切り分けが難しい場合は、まず何が原因なのか、調査を行う必要があります。しかし、あちこちのマシンやルータの設定を見直すのでは、時間がかかります。ここでもし、ネットワーク上に流れているパケットの様子を見ることができれば、原因の絞り込みが容易となり、何の設定を見直せば良いのか、見当がつくようになります。
そこで、パケットキャプチャツールを用いたパケット解析の知識を仕入れようと、「実践パケット解析 第2版」を読みました。パケット解析とは何か、どのように行うのかといった問いに答えてくれそうでしたので、この本を選びました。
パケット解析のことをよく知らない自分にとっては、パケットキャプチャツールをネットワークに仕掛ける方法が印象的でした。仕掛ける、といっても様々な方法があり、状況に応じて異なります。基本的には、ネットワーク機器に直接触れることができるか、設定を変更することができるかというところが重要なポイントとなります。もし可能であれば、ポートミラーリングやネットワークタップ、リピーターハブを利用して、パケットキャプチャツールを仕掛けることができます。不可能であれば、ARPキャッシュポイゾニングを用いて、ホストと、スイッチングハブやルータとの間に割り込んでパケットキャプチャツールを仕掛けることになります。
本書の中心はWiresharkの使い方ですが、スクリーンショット付きで説明されており、わかりやすく書かれています。Wiresharkはオープンソース、マルチプラットフォームのパケットキャプチャツールで、パケットのプロトコルを解析し、ヘッダを分かりやすく表示するだけでなく、フィルタリング機能や統計表示機能など、パケットを解析する支援機能も充実しています。特にフィルタリング機能では、独自の構文を用いて検索条件を作成することができ、ヘッダの各要素レベルで条件を作成可能です。ネットワーク上では大量のパケットが流れるため、検索条件の善し悪しが解析作業の効率を左右すると感じました。
使用事例も記載されているため、初めてパケット解析を行う際には心強い味方となりそうです。ネットワークに繋がらないといったネットワークトラブルから、不正な侵入を受けた場合の解析など多岐にわたった事例が紹介されています(ただし、不正な侵入を受けた場合の解析は、それだけで奥深いテーマのため、本書では触り程度に触れられています)。ネットワークトラブルの場合は、まず異常な状態と正常な状態を区別するため、ベースラインとなる状態を定めておくべきである、という主張が繰り返しなされていたのが印象的でした。確かに、トラブルの有無にかかわらず、ネットワーク上にはパケットが流れ続けているわけですから、ベースラインとなる状態を定めておかないと、何と比較して異常な状態と判断するのか、基準がなくなってしまいます。
このように、基本から丁寧に説明されているため、パケット解析のノウハウが一通り得られたように思えました。実際にパケット解析を行う場面となった際には、本書を片手に頑張ってみようと思います。もっとも、企業でパケットキャプチャツールを使用する場合は、企業のセキュリティポリシー上、インストールや使用が禁止されている場合があるため、関連部署との調整が必要不可欠ではありますが……。
]]>現在最新バージョンのEMMA(2.0.5312)では、HTMLレポートを生成する際に、ソースコードの文字エンコーディングを指定することができません。プラットフォームのデフォルト文字エンコーディングがソースコードの文字エンコーディングとして使用されるため、両者が一致していない場合はHTMLレポートが文字化けしてしまいます。例えば日本語版Windowsのデフォルト文字エンコーディングはShift JISのため、それ以外の文字エンコーディングで書かれたソースコードからHTMLレポートを生成すると、文字化けしてしまいます。
そこで、ソースコードの文字エンコーディングを指定できるようにしたパッチを作りました。本パッチを適用したEMMAでは、以下のいずれかの方法でソースコードの文字エンコーディングを指定することができます。
本パッチは、本家に投稿済みです。また、Android SDKに含まれているEMMAについても本パッチを適用し、結果をGitHubにアップロードしました。EMMAプロジェクトは現在休止中であるため、マージが待てない方は本パッチをご自由に(ただし自己責任で)お使いください。
]]>そこで調べてみると、g++-multilibパッケージをインストールすると32ビット用JDKを64ビット版Linuxにインストールできることがわかりました。g++-mutilibとは、g++に標準で備わっているアーキテクチャのサポートをさらに追加するためのパッケージで、このパッケージをインストールすることで64ビット版Linuxでも32ビット向けプログラムをビルドできるようになります。
実際にg++-multilibをインストールし、再びJDK 1.4のインストーラを起動したところ、無事にJDK 1.4をインストールできました。前述のライブラリのビルドにも成功しました。
そこで、指定したログがlogcatから出力されるまでスクリプトを待機させるPythonモジュールを作りました。以下のように使うと、Activityが起動するまで待機できます。
import logmatcher logmatcher.start() # ... (Activityの起動) logmatcher.wait( 'START {act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER]')
以下の環境でスクリプトは動作します。
GitHubでスクリプトを公開しています。「Downloads」からアーカイブをダウンロードしてください。ライセンスはApache License, Version 2.0です。
特定の文字列がログに現れるのを待機するほかに、特定の正規表現パターンにマッチする文字列がログに現れるのを待機することもできます。詳しくは、READMEをご覧ください。
]]>/* package */ void method() { ... }
Eclipseの一部やAndroidに標準搭載されているブラウザアプリケーションや設定アプリケーションなどがそのようなコーディングルールで記述されています。
Javaで書かれたソースコードがコーディング規約に従っているかどうかをチェックする際には、Checkstyleがよく使用されます。しかし、本記事の執筆時点の最新バージョンであるCheckstyle 5.5では、パッケージスコープを表すコメントの有無をチェックできません。
そこで今回、パッケージスコープを表すコメントの有無をチェックするCheckstyleプラグインを作ってみました。GitHubで公開しています。ライセンスはCheckstyleと同じLGPLです。Checkstyle 5.5以上に対応しています。
本プラグインでは、アクセス修飾子のないクラスなどの定義にパッケージスコープを表すコメントが書かれていることをチェックし、書かれていなければ警告します。加えて、アクセス修飾子が書かれているにもかかわらず以下のようにパッケージスコープを表すコメントがある場合に警告を行います。
/* package */ public void method() { ... }
使い方は次の通りです。まず、Checkstyleの設定ファイルに本プラグインのモジュールを追加します。本プラグインのモジュールは、TreeWalkerモジュールのサブモジュールです。例えば、次のようになります。
<module name="TreeWalker"> <module name="CommentedPackageVisibilityCheck" /> </module>
クラスパスに本プラグインのJARファイルを追加して、Checkstyleを実行してください。
JARファイルは、「commented-package-visibility-check-0.1.zip」をダウンロードして展開すると手に入ります。
CommentedPackageVisibilityCheckモジュールには、以下のプロパティを用意しています。必要に応じて設定してください。
プロパティ名 | 説明 | 省略時の値 |
---|---|---|
format | パッケージスコープを表すコメントにマッチする正規表現。 | /\* package \*/ |
requireLatterWhiteSpace | パッケージスコープを表すコメントの直後に、少なくとも1文字以上の空白が必要か。 | true |
Jenkinsを使ってAndroidプロジェクトで継続インテグレーションを行おうとしたのですが、JUnitのXML形式でテスト結果を出力する標準的な方法が見当たりませんでした。テキスト形式では出力できます。しかしXML形式でテスト結果が出力できれば、テストが何件成功して何件失敗したのかが一目でわかるほか、その件数がビルドを経るに従ってどのように推移したのかがグラフでわかりテスト結果が見やすくなります。
調査したなかでは、android.test.InstrumentationTestRunnerを拡張してXML形式を出力するようにすれば解決はできるようでした。実際、「Android JUnit Report Test Runner」というプロジェクトでそのようなtest runnerを含むJARライブラリが提供されています。しかしテストのために新しいライブラリを導入するのは気が引けました。元のソースコードには手を入れずにXML形式のテスト結果を出力する方法が欲しかったのです。
そこでAndroi SDKに含まれているddmlib.jarを使用して、テストを起動し、おのおののテストメソッドの結果を受け取り、XML形式にテスト結果を出力するJythonスクリプトを作成しました。スクリプトの形にすることで、ソースコードを変更しなくてもテスト結果をXML形式で出力することができるようになりました。
本スクリプトの実行にはAndroid SDKとJython 2.5系が必要です。実行前には、CLASSPATH環境変数にddmlib.jarのパスを設定してください。ddmlib.jarのパスは<Android SDKのパス>/tools/lib/ddmlib.jarとなるはずです。
Androidデバイスもしくはエミュレータにテスト対象のアプリケーションとテストアプリケーションをインストール後、以下を実行してください。android.test.InstrumentationTestRunnerを実行し、結果をXMLで標準出力に出力します。なお以下の実行例はbash上のものです。
./run_tests_for_xml.py -a <adbのパス> <テストアプリケーションのパッケージ名>
「<adbのパス>」はAndroid SDKに含まれているadbのパスに置き換えてください。<Android SDKのパス>/platform-tools/adbとなるはずです。「<テストアプリケーションのパッケージ名>」は、テストプロジェクトのAndroidManifest.xmlにある、manifest要素のpackage属性の値に置き換えてください。
特定のtest runnerを実行したい場合には次のようにスクリプトを実行します。
./run_tests_for_xml.py -a <adbのパス> <テストアプリケーションのパッケージ名> <test runnerの名前>
「<test runnerの名前>」は、テストプロジェクトのAndroidManifest.xmlにある、instrumentation要素のandroid:name属性の値に置き換えてください。現在のスクリプトでは実行するtest runnerはひとつだけ指定できます。
特定のデバイスやエミュレータ上で実行したい場合には次のようにスクリプトを実行します。
./run_tests_for_xml.py -a <adbのパス> -s <デバイスもしくはエミュレータのシリアルナンバー> <テストアプリケーションのパッケージ名>
「<デバイスもしくはエミュレータのシリアルナンバー>」には「adb devices」で出力されるシリアルナンバーを指定します。Jenkins上でAndroid Emulator Pluginを使用している場合は次のようにしてエミュレータを指定します。
jython25 run_tests_for_xml.py -a <adbのパス> -s $ANDROID_AVD_DEVICE <テストアプリケーションのパッケージ名>
GitHub上に置いてあります。ライセンスはApache License 2.0です。
Android SDKが生成するbuild.xmlへ本スクリプトを使用するターゲットを追加するには、GitHub上にあるrun-tests.xmlの-test-xmlターゲットや-test-coverage-xmlターゲットを参照してください。
]]>本targetを使用する手順は以下の通りです。
<taskdef name="findbugs" classname="edu.umd.cs.findbugs.anttask.FindBugsTask" /> <target name="findbugs" depends="compile" description="Run FindBugs."> <findbugs home="${findbugs.home}" output="${findbugs.output}" outputFile="${findbugs.output.file}" excludeFilter="${findbugs.exclude.filter}"> <sourcePath path="${source.absolute.dir}" /> <sourcePath path="${gen.absolute.dir}" /> <auxClasspath path="${android.jar}" /> <class location="${out.classes.absolute.dir}" /> </findbugs> </target>
target中の以下のプロパティは適切なものを設定するか、置き換えください。findbugsタスクの詳細は、「Using the FindBugs™ Ant task」を参照してください。
自動生成されるR.javaの内部クラスの名前は小文字で始まるため、NM_CLASS_NAMING_CONVENTIONが指摘されます。そこで以下のXMLファイルでレポートの対象外とします。
<?xml version="1.0" encoding="UTF-8"?> <FindBugsFilter> <Match> <Class name="~.*\.R\$.*" /> <Bug code="Nm" /> </Match> </FindBugsFilter>
これで、「ant findbugs」と実行するとFindBugsをかけることができます。
レポートが文字化けする場合は、以下のいずれかの対策を取ります。
これは、前者がGestureDetector.OnGestureListenerに定義されているメソッドで、後者がGestureDetector.OnDoubleTapListenerで定義されており、GestureDetector.SimpleOnGestureListenerはその両者を実装しているためです。GestureDetector.OnGestureListenerは、ダブルタップを考慮しません。
そのため、ダブルタップとシングルタップとを区別したい場合は、GestureDetector.SimpleOnGestureListener#onSingleTapUp(MotionEvent)ではなくGestureDetector.SimpleOnGestureListener#onSingleTapConfirmed(MotionEvent)を使用すべきです。
なお、前者はMotionEvent.ACTION_UPの動作を通知し、後者はMotionEvent.ACTION_DOWNの動作を通知することにも注意が必要です。
]]>両者に違いはあるのでしょうか。気になったので調べてみました。結論としては、両者の動作に違いはありません。
android.os.Process.setThreadPriority()は、android.os.Processに定義されているANDROID_PRIORITY_*定数をnice値としてsetpriorityシステムコールを呼び出します。処理の流れは以下の通りです。
java.lang.Thread.setPriority()は、java.lang.Threadに定義されているスレッド優先度をnice値に変換してsetpriorityシステムコールを呼び出します。処理の流れは以下の通りです。
結果、両者の効果は同じです。ただし、android.os.Processではスレッドの優先度が用途ごとに定数として定義されているため、まずandroid.os.Process.setThreadPriority()の使用を検討すべきでしょう。
]]>