SyntaxHighlighter

2023年11月27日月曜日

SQLite3 のトランザクションと Rails

最近は開発よりもマネージメントの仕事が多いのですが、久しぶりに技術的な話で書くネタができたので書いておきます。

SQLite3 のトランザクションは他のデータベースと少し異なり特殊なので、色々とはまりがちですが、その内容についてまとめておきます。
Rails での話も少し書いておきます。

SQLite3 の分離レベル

SQL の分離レベルは、 Wikipedia の記事にあるように、 Read Uncommitted, Read Committed, Repeatable Read, Serializable の4種類があります。

SQLite3 では、 Read Uncommitted になる例外もありますが、基本的には常に Serializable です。
後述するトランザクションのモードとして、 Deferred, Immediate, Exclusive が指定できますが、それとは関係なく、常に Serializable になるようです。
つまり、どのトランザクションのモードを選んでも、 Dirty Read や Phantom Read などは起こらず、トランザクション中は、常に整合性の取れた読み取りができるかエラーになるかのいずれかになります。

SQLite3 のトランザクションのモードと、エラーになるケース

SQLite3 では、トランザクションのモードを BEGIN TRANSACTION の際に指定することができます。

公式のページに書かれていますが、 Deferred, Immediate, Exclusive の3つを指定でき、デフォルトでは Deferred が使われます。
細かな定義については公式のページに記載があるので割愛しますが、それぞれで、「ロックを取得するタイミング」、「取得するロックの種類」が異なります。
これらは、既に実行中のトランザクションが別で存在する場合に、「相手のトランザクションの終了をどのタイミングで待つか」や「何をしたときにエラーにするか」が異なります。

例えば、標準の設定である Deferred を選んだ場合、 BEGIN DEFERRED TRANSACTION を実行したタイミングでは何のロックも取得されず、その後に続く SELECTINSERT などが実行された際に、初めて読み取り用の Shared ロックや書き込み用の Reserved ロックが取得されます。
なお、 Shared ロックと異なり、同時に複数のセッションが Reserved ロックを取得することはできません。
そのため、例えば下記のようなコードをほぼ同時に二つ実行すると、後から実行した方は INSERT 実行のタイミングで Reserved ロックの取得に失敗することになります。

これは、 SELECT 実行の時点で Shared ロックが取得され、 INSERT 実行の時点で Reserved ロックの取得が試みられることによります。
Reserved ロックは同時に一つのセッションしか取得できないので、後から実行した方のコードでは、 Reserved ロックの取得ができません。
このタイミングで、後から実行した方のコードは、トランザクションの実行に失敗します。
勘違いしやすいですが、自動のリトライは走らず Busy Timeout の設定も意味を持ちません。
これは、後から実行した方のコードでも、既に SELECT を発行しているために、このトランザクションの Serializable の分離レベルを保証できなくなるからです。
よって、 INSERT 実行のタイミングで、デッドロックにより失敗します。

一方で、後から実行した方のコードで、 SELECT などの読み取りのみを行うのであれば、トランザクションは最後まで問題なく実行されます。
おそらくですが、 Deferred のトランザクションが最も効果的に動作するのはこのような読み取りの場合で、この場合は Phantom Read などの影響を受けずに読み取れることが保証できます。

なお、 Immediate を指定したトランザクションの場合、 BEGIN IMMEDIATE TRANSACTION を実行したタイミングで即座に Reserved ロックが取得されるので、同時に二つのコードを実行してもエラーは発生しません。
ただし、 SELECT も待たされてしまうので、並列性は下がることになります。
同時に読み込みや書き込みがあってもエラーとなる可能性は下がるので、こちらが適しているケースも多々あるように思います。

Ruby on Rails での挙動

Ruby on Rails では、標準では SQLite3 がデータベースとして使われるように設定されています。

一方で、 SQLite3 独自のトランザクションのモードである Deferred や Immediate は、 Rails の世界からは容易には指定することはできません。
原則として常に Deferred になります。
なお、分離レベルとしては、 Read Uncommitted は指定することができるようになったようです。

いずれにしても、 SQLite3 は基本的には分離レベルは Serializable であり、その上で Deferred, Immediate などのトランザクションの種類を指定することになるので、他のデータベースも考慮して、汎用的な Rails のコードを書こうと思うと、トランザクションのモードを指定するのは難しくなります。

sqlite3 ライブラリでの吸収

こちらについて少し考えた結果、 SQLite3 へアクセスするためのライブラリ sqlite3 で吸収するのがよいのではないかと考えました。

そこで、こちらの Pull Request で、「標準で使うトランザクションのモード」をコンストラクタで指定できるようにしてもらいました。

これにより、トランザクション実行時に、どのようなモードにするかの既定値を指定することができるようになります。
Rails では、何も指定せずにトランザクションが開始されるので、そういった状況で使われるモードを指定できることになります。

Ruby on Rails での設定

以上を踏まえ、 sqlite3 のバージョン 1.6.9以降を使った上で、以下の設定値を config/database.yml に書くと、トランザクションが Immediate モードで実行されるようになります。

2023年4月3日月曜日

PyTorch の TorchScript のデバッグメッセージ (PYTORCH_JIT_LOG_LEVEL によるログ出力)

PyTorch の挙動を調べる必要があった際に、ログ出力の設定を調べたことを書いておこうと思います。

PyTorch の TorchScript のログ出力

PyTorch のモデルを保存する際などに PyTorch 内部で利用される TorchScript には、標準でデバッグ用のログ出力の機能が備わっています。

なお、私は ONNX へのエクスポートの際の挙動のデバッグの際にこのログ出力の機能を用いました。

PYTORCH_JIT_LOG_LEVEL 環境変数の設定

基本的に、 jit_log.h の冒頭のコメント部分に書かれていることに従って環境変数を設定します。

この環境変数には、以下のようにログ出力したいファイルのファイル名を指定します。

  • PyTorch のソースコードを見て、ログを出力したいファイルを探します。
    例えば、 torch/csrc/jit/passes/onnx/shape_type_inference.cpp のログを出力したい場合、 shape_type_inference.cpp が設定すべきファイル名になります。
  • 出力したいログのレベルは現時点では 3段階あり、その値に応じて > をいくつか付与します。
  • 複数のファイルのログを出力したい場合は、 : で区切って連結します。
  • 結果として、 shape_type_inference.cpp:>subgraph_matcher.cpp のような文字列を PYTORCH_JIT_LOG_LEVEL に設定します。

結果として、 Bash であれば export 'PYTORCH_JIT_LOG_LEVEL=shape_type_inference.cpp:>subgraph_matcher.cpp' のように設定します。
Windows 上のコマンドプロンプトを使う場合は、 set "PYTORCH_JIT_LOG_LEVEL=shape_type_inference.cpp:>subgraph_matcher.cpp" のように設定します。

> を含む場合、 '" で囲む必要があるのに注意してください。

ログ出力の例

例として、以下のようなスクリプトを実行する際のログを出力することを考えます。

このスクリプトを実行したときの shape_type_inference.cpp のログをすべて出力する場合、 >>shape_type_inference.cppPYTORCH_JIT_LOG_LEVEL 環境変数に設定します。
この状態で上記のスクリプトを実行すると、以下のようなログが出力されます。

Windows 上でログ出力する際の注意事項

実際には、先の例の通りにやっても、 Windows ではログが出力されません。
これは、 v2.0.0 の時点での (おそらく) バグで、 PYTORCH_JIT_LOG_LEVEL に設定されたファイル名と __FILE__ を比較する際の不具合によるものです。

StripBasename 関数の中でファイルのフルパスからファイル名部分を抽出していますが、この部分はディレクトリの区切り文字として / しか見ていません。
そのため、 Windows で __FILE__C:\actions-runner\_work\pytorch\pytorch\builder\windows\pytorch\torch\csrc\jit\passes\onnx\shape_type_inference.cpp のようなファイル名が設定されている際には、ファイル名部分として shape_type_inference.cpp がうまく抽出されず、 PYTORCH_JIT_LOG_LEVELshape_type_inference.cpp が設定されていてもうまくログが出力されませんでした。

この場合、 StripBasename を修正してビルドし直すか、 site-packages\torch\lib\torch_python.dll をバイナリエディタで直接修正し、 DLL に含まれる C:\actions-runner\_work\pytorch\pytorch\builder\windows\pytorch\torch\csrc\jit\passes\onnx\shape_type_inference.cpp の部分を C:/actions-runner/_work/pytorch/pytorch/builder/windows/pytorch/torch/csrc/jit/passes/onnx/shape_type_inference.cpp に変更することで、ログを出力できるようになります。

なお、このプルリクエストで修正されました!

このログを使ってデバッグした話の詳細については、次回以降に書くかもしれません。

2022年5月5日木曜日

SQLAlchemy を用いた Snowflake へのアクセス

SQLAlchemy を用いた Snowflake へのアクセス

今回は、Snowflake SQLAlchemy ツールキットおよびPythonコネクターの使用にあるような、 SQLAlchemy を用いた Snowflake データベースへのアクセスについてまとめようと思います。

また、 Snowflake の JSON データへアクセスするのを容易にするために開発したライブラリである snowflake-sqlalchemy-json についても触れようと思います。

Snowflake と半構造化データ

Snowflake は、いわゆるクラウドでビッグデータを処理するためのデータプラットフォームです。
細かく最適化を考えなくても、それなりによいパフォーマンスでデータの保存/読み出しができ、 SQL でのアクセスもサポートされています。

2022年5月時点では、30日の無料トライアルを受け付けており、どんな雰囲気かは無料で試すことができます。

Snowflake は、一般的なデータ型をサポートしており、他のデータベースを使っていた人であれば、違和感なく使えるように思います。
なお、ビッグデータ向けのデータベースでよくあるように、 Unique 制約などは強制されないので、そこには注意が必要です。

少し特徴的な型として、半構造化データ型というものがあります。
これは、 VARIANT などの型をカラムに指定することで、カラム内に JSON, Avro などの半構造化データを入れておき、 SQL から直接アクセスできるようにするものです。

例えば SELECT column_name:key1 FROM table; のように記述することで、 table テーブルの column_name カラムにある {"key1": "value1", "key2": "value2"} の中の "value1" にアクセスすることができます。
WHERE の条件部分で指定することも可能で、通常のカラムと同様にアクセスすることができます。

SQLAlchemy による SQL の生成

話は変わりますが、 Python のライブラリで SQLAlchemy というものがあります。
このライブラリは ORM として使うこともできますが、生の SQL をラップし Python から扱いやすくするという、 core と呼ばれる使い方もあります。

これは、以下のように Python のコードとして SQL を組み立てられるものです。

SQLite の場合、上記の query は、以下のような SQL に変換されます。

このように SQLAlchemy を用いると、 Python のコードとして SQL を作ることができます。

他にも JOIN や CTE (Common Table Expression) を用いた例を書いておくと以下のようになります。
この例では、テーブルの定義に declarative_base を使っています。この場合は、 Table を使う場合と異なり、 .c の部分が不要になり、補完も効きやすくなるので、書きやすいかもしれません。
なお、例自体には特に意味はありません。

Python で記述することで、 CTE 部分を変数として保持でき、それなりに分かりやすく記述できているように思います。

上記の query からは、下記の SQL が生成されます。

snowflake-sqlalchemy

Snowflake でも、上述した SQLAlchemy を使うことができます。

公式のページで使い方がまとめられているので、こちらのページを参考にするのがよいと思います。

なお、 Snowflake の場合、テーブルを指定する際には DATABASE.SCHEMA.TABLE のように、データベース名とスキーマ名でテーブル名を修飾することで、一意にテーブルを指定することができます。
SQLAlchemy で、このようにデータベース名やスキーマ名も含めて修飾したテーブル名を指定する場合、以下のように quoted_name を用いてテーブル名を記述することで、実現できます。

snowflake-sqlalchemy-json

上述したように、 Snowflake では VARIANT 型のカラムに対しては、 JSON 形式などの値に対して SQL 内から所定の書式でアクセスすることができます。

snowflake-sqlalchemy では、その辺りの処理は 2022年5月時点では実装されていないようだったので、 snowflake-sqlalchemy-json でいくつかを実装してみました。

現時点では読み込みのみしかサポートしておらず、機能も限定的ですが、それなりに使えそうに思うので、このライブラリを使った例を挙げておきます。
ここでは、 Snowflake のアカウント作成時に例として提供されていた SNOWFLAKE_SAMPLE_DATA.WEATHER.DAILY_14_TOTAL テーブルに含まれているデータを読み込む際の例について挙げます。データのサンプルは、末尾に挙げておきます。

VARIANT 型を SQLAlchemy の JSON にマッピングしアクセス

上記の query で、 V カラムに入っているデータの中の city キーで取得できるオブジェクト内の要素にアクセスすることができます。
基本的に、 Python の dict と同様のアクセス方法になり、直感的に書けているのが分かると思います。

lateralflatten を用いたアクセス

Snowflake の VARIANT 型へのアクセスで重要な概念である lateral 結合を用いたアクセスについては、下記のように記述できます。
なお、 lateral 結合については結局 LATERAL とは何者なのか?の記事が分かりやすいように思います。

flatten は、あくまで通常の関数なので、 SQLAlchemy で関数を呼び出す際の形式である func.flatten(xxx) として記載します。
また、 lateral は SQLAlchemy がサポートしているので、公式のチュートリアルの通りに呼び出します。
lateral() で返されるテーブルの value カラムにアクセスすると、 flatten で展開された要素にアクセスすることができます。

なお、上記は FROM 句に FROM DAILY_14_TOTAL, LATERAL FLATTEN(xxx) のように lateral を記述する方法ですが、この書き方では Warning が出るかもしれません。
その場合、以下のように、 JOIN 句として独立させて書くこともできます。
私はこちらの方が分かりやすいように思います。

用いたデータの形式

最後に、 DAILY_14_TOTAL テーブルの V カラムに入っている JSON データの形式を挙げておきます。

以上のようにすることで、 Snowflake の VARIANT 型へのアクセスを SQLAlchemy を用いて記載することができるようになります。

2022年3月7日月曜日

Azure DevOps 向けの拡張機能 PublishMarkdownReports

Azure DevOps 向けの拡張機能 PublishMarkdownReports

Microsoft の統合開発環境である Azure DevOps 向けに、拡張機能 Publish Markdown Reports を作成したので、その紹介です。

拡張機能自体の作り方は、 Web 拡張機能の開発のページに詳しい方法が載っているので、こちらを参照してみてください。

やりたかったこと

ソースコードから自動生成した Markdown の API 仕様書を、 Azure DevOps の CI/CD である Azure Pipelines のページで閲覧できるようにしたい、というのがやりたかったことでした。
Azure Pipeline から Azure DevOps Wiki を更新する方法もあるのですが、こちらの記事のように Wiki 用の Git Repository を自動で更新する方法では、「同じぐらいのタイミングで CI/CD が走ったとき」や「CI/CD タスクが Re-queue され、古いコミットに対するタスクが後で実行されたとき」に、古い内容で上書きしてしまわないような仕組みが必要になります。

今回は、そういったことを考慮しなくてすむように、 Publish Markdown Reports では Azure Pipelines のビルド結果の画面に任意の Markdown ドキュメントを表示できるようにすることにしました。
このようにすることで、別の Wiki 用の Git Repository は不要になり、 CI/CD タスクの実行順序などを考えなくてもよくなります。
ただし、ビルド結果は設定によっては古い結果が削除されるので、正式なドキュメントとして残す場合は別途対応が必要です。

完成した表示画面は下記になります。
screenshot

Publish Markdown Reports について

まずは、 Publish Markdown Reports の概要について説明しようと思います。
なお、ソースコード全体は GitHub に置いてあります。

表示用 Markdown ファイルのアップロード

この拡張機能では、 Markdown として表示するファイルをビルドタスクの中でアップロードします。
下記のような YAML の記述を Azure Pipelines に加えることで、指定されたディレクトリのファイルをアップロードします。

上記の場合、ソースコードのディレクトリの下の markdown ディレクトリの下にあるファイルすべてをアップロードします。
また、既定のページとして main_page.md を指定しています。このファイルは contentPath で指定したディレクトリからの相対パスで指定します。
headingId は、 Markdown の # 見出し のような章のタイトル (<h1> などのタグで囲まれた部分) に対して、どのような ID を付与するかを指定します。
現時点では完全ではないのですが、 Python の Markdown の HTML 変換ライブラリである Python-Markdown や、 Doxygen の結果を Markdown に変換する Doxybook2 の ID のつけ方に対応しています。
なお、 Pydoc-Markdown などのツールでは、 <a> タグでリンク先の場所が定義されているので、 headingId の指定は不要です。

内部的には、これらのファイルはビルド中に Attachment として、ビルド結果に対する添付ファイルの形でアップロードされています。
このファイルは API でしか取得できないもので、ビルド生成物として登録する Artifact とは異なるものです。

Markdown ファイルの表示

この拡張機能では、アップロードされた Markdown ファイルを Marked で HTML 変換して表示しています。

ただし、 Azure DevOps の拡張機能の仕組み上、少し面倒ですが、下記のように表示を実現しています。

  1. 拡張機能に含まれる frame.html をビルド結果のタブ内に表示
  2. frame.html から Azure DevOps の SDK を用いて Attachment として保存されている Markdown を JavaScript で取得
  3. 取得した Markdown ファイルを Marked ライブラリを用いて HTML に変換
    その際に、リンクを処理する際には、 frame.html?page=/link-to-page.md のように、 frame.html に対して page パラメータとして Markdown のファイルのパスを指定するようにします。
    また、画像を表示するために、画像に対するファイル名に対して Attachment を取得し、得られたデータを Blob 変換した上で、 URL.createObjectURL で生成した URL に差し替えます。
  4. 生成された HTML をタブに表示

高速化

上述した方法では、表示されるまでに少しタイムラグが発生します。これは、 Azure DevOps の SDK の性質上、ロード時の初期化に 1~2 秒かかることが多いためです。
これを解消するために、少し高速化を行います。

上述した frame.html をラップする main.html を用意し、 frame.htmlmain.html の中で iframe として表示するようにします。

その上で、それぞれのファイルで以下のような処理を行うことで、 Azure DevOps の SDK の初期化処理がページ遷移のたびに動作しないようにしています。

main.html
  • Azure DevOps の SDK である VSS.SDK.js をロードし、初期化する。
  • onmessage ハンドラで frame.html からのメッセージを待ち受け、メッセージに応じて Attachment をダウンロードし、 frame.html に送る。
frame.html
  • 表示すべき Markdown ファイルを main.html に要望する。
  • main.html から送り返されたデータを処理し、 Marked ライブラリで HTML に変換する。

このようにすることで、画面が切り替わる際は frame.html だけがロードされ、 main.html 側は破棄されないため、 VSS.SDK.js による初期化処理は動作しないことになります。
そのため、 Markdown ファイルの切り替えによる画面遷移時には、 Markdown ファイルや画像のロードのみで動作が完結し、高速な動作が可能となります。

このようにして、それなりに実用に耐えうる拡張機能ができました。

2022年2月7日月曜日

Bash 向けの tcsh 同様な history-search

Bash 向けの tcsh 同様な history-search

Bash のシェルスクリプトの勉強がてら、 tcsh 同様な history-search-backward のようなものを実装してみました。

Bash の history-search-backward

Bash には、元々 history-search-backward というコマンドが用意されています。
しかし、このコマンドは tcsh のコマンドとは異なり、カーソル位置が行の末尾に移動しません。
私は Solaris の tcsh で Unix 系の環境に初めて触れたこともあり、 tcsh のようにカーソルが末尾に移動するという挙動の方が馴染みがあるので、そのようなコマンドを作ってみようと思いました。

なお、あくまで私が Bash のシェルスクリプトの練習をするというのも重要な目的の一つなので、例えば zsh をなど他の解は、今回は気にしていません。

ポイント

今回は、 Bash のシェルスクリプトの勉強を兼ねていたので、以下を守ることにしました。

  • Bash 以外での動作は気にしない。
  • Bash の組み込みコマンド以外は使わない。
  • Windows 版の Git に含まれている Bash でも動作させる。

コードの概要

コードは GitHub に置いてあります。

おおまかな流れとしては、以下のようになっています。

  1. fc コマンドでコマンド履歴の一覧を取ってくる。
  2. 一行ずつ read コマンドで読み込む。
  3. 特定の文字列で始まるコマンドを抜き出し、それを READLINE_LINE, READLINE_POINT に設定することで、ターミナルの文字列を置換する。

coproc の使い方

fc コマンドを実行し、その結果を read コマンドで処理する際に、 coproc コマンドを使うことにしました。
Bash の while でパイプから読み込んだテキストを一行ずつ処理をする場合、 Qiita の記事にあるように、 Process Substitution などを使えばよいようですが、 Windows 版の Git に含まれている Bash などでは実行することができません。
そのため、今回はこの方法は諦め、 coproc を使って読み込むことにしました。

概要

coproc に関連する部分をシンプルにして抜き出すと以下のようになっています。

coprocfc を実行するプロセスを作り、その出力を whileread で一行ずつ処理しています。

ちなみに、よく言われるように下記では while 内がサブシェルで実行されるので、 HISTORY_ARRAY に期待した通りに値が保存されません。

また、上述したように、下記のように Process Substitution を使う方法では、私の手元の Windows の bash.exe では動作しませんでした。

ジョブ情報の出力の抑制

そのまま使ってしまうと、[1] 1859[1]+ Done のような、ジョブの情報が出力されてしまうので、それらを抑制する必要がありました。

開始時は以下のようにします。
coproc の標準エラー出力を捨てればよいのですが、波括弧で囲ってリダイレクトすると指定しやすいです。

終了時の出力は、 coproc 呼び出し後に以下の disown を呼んでおくことで、抑制することができます。

coproc からの読み込み

coproc で返されるファイルディスクリプタから read コマンドでテキストを読み込むには、 -u オプションを使うことで実現できます。
この辺りは Web 上で情報がたくさん見つかるので省略します。

一方で、ただ coproc FC_FD { fc -lnr -${HISTSIZE} ; } として実行した coproc を read -u ${FC_FD[0]} のように読むだけでは、場合によってはうまくいきません。

Stack Overflow に書かれているように、 Bash の coproc では、 coproc で実行したプロセスが先に完了してしまうと、プロセスからの出力をすべて処理する前に出力が読めなくなります。

そのため、 coproc のプロセスの出力を読み終わるまで、プロセスが終了しないように工夫する必要があります。
また、 coproc のプロセスの出力をバッファリングせずに出力しきるために、以下のようにします。

このように、 coproc の中で read -s で入力を待ち、 exec 1>&- で出力のファイルディスクリプタを閉じることで出力を確実に処理させ、 coproc での出力をすべて処理できるようにします。

というような感じで、 Bash の組み込みコマンドのみで tcsh のような history-search-backward を実装することができました。
私はこれまでにほとんどシェルスクリプトを書く機会がなかったのですが、 Bash 4 以降は coproc や連想配列もあり、それなりにいろいろなことができるようです。

2022年1月28日金曜日

API による Microsoft Teams のステータスメッセージの取得/設定

API による Microsoft Teams のステータスメッセージの取得/設定

Microsoft Teams では、ステータスメッセージを設定することができます。

今回は、これをスクリプト経由で設定する方法について記載しようと思います。

Graph API でできる範囲

2022年1月現在、 Teams の Microsoft Graph REST API では、 ユーザーのプレゼンス情報の取得/設定ができます。
この API では、 availability, activity の取得/設定がサポートされており、 Python スクリプトなどで自由に設定することができます。

しかし、 beta 版も含め、ステータスメッセージの取得/設定は現状サポートされていないようです。

Teams アプリ、Teams Web アプリと同様の方法でのアクセス

そこで、公開されていない API を用いてアクセスすることになります。

結論としては、下記の情報で OAuth2 のアクセストークンを取得し、後述する URL に POST, PUT することで、ステータスメッセージの取得/設定することができます。
これは、原則として、 Teams アプリや Teams Web アプリが行っている方法と同様と思われます。

OAuth2 情報

OAuth2 のアクセストークンは、下記の情報で取得を行います。

Client ID
1fec8e78-bce4-4aaf-ab1b-5451cc387264
Client Secret
Public Client なので不要
Redirect URI
https://login.microsoftonline.com/common/oauth2/nativeclient
Scope
https://api.spaces.skype.com//.default

なお、上記の Client ID は、 Teams のデスクトップアプリの ID になっています。
また、 Client Secret, Redirect URI は、いずれも Public Client の設定になっています。
Scope の .default の前にスラッシュが二つありますが、 Microsoft のページによると、これで正しいようです。
(ただ、スラッシュが一つでも問題なく動いてはいるようでした。)

ステータスメッセージの取得

ステータスメッセージの取得は、下記の URL に JSON データを POST することで実現できます。

送信するデータの中の "mri" に設定している GUID は、 Graph API で取得できるユーザー情報の中の id のことです。
8:orgidPublicDirectoryParticipantPrefix と呼ばれるもののようで、固定値でよいようです。

URL
https://presence.teams.microsoft.com/v1/presence/getpresence/
HTTP Method
POST
Body として送信する JSON データ

上記を送ると、指定したユーザー分のステータスメッセージを含むユーザー情報が返ってきます。

ステータスメッセージの設定

自分のステータスメッセージの設定は、下記の URL に JSON データを PUT することで実現できます。

メッセージと有効期限を指定し、設定することができます。

URL
https://presence.teams.microsoft.com/v1/me/publishnote
HTTP Method
PUT
Body として送信する JSON データ

なお、 "message" の末尾に <pinnednote></pinnednote> を追加すると、「他のユーザーが自分にメッセージを送るときに表示する」にチェックした状態になるようです。

Python での実装例

最後に、 Python を用いてステータスメッセージを設定する例を書いておこうと思います。

Windows 上で簡易的に OAuth2 を扱えるようにした msal-interactive-token-acquirer と、 HTTP アクセスで広く使われている requests を用いた場合、以下のようになります。

なお、これは個人の Microsoft アカウントではなく、組織の Microsoft アカウントで行う必要があります。
個人で試す場合は、 Microsoft 365 開発者プログラムなどを活用するとよいと思います。

2021年8月16日月曜日

PyTorch における Deformable Convolution の ONNX 変換

PyTorch における Deformable Convolution の ONNX 変換

PyTorch では、何年か前から Deformable Convolution が DeformConv2d としてサポートされていました。

しかし、 ONNX 変換はサポートされておらず、 DeformConv2d を含むネットワークを ONNX にしようとするとエラーになっていました。

今回は、 DeformConv2d を PyTorch で ONNX に変換できるようにするモジュール deform_conv2d_onnx_exporter を作ったので、その説明です。

が、諸事情により、一旦ライブラリの公開は取りやめました。すみません。 → 再度公開しました。 PyPI のページからダウンロードできます。

Deformable Convolution の説明自体はここでは触れません。

ONNX 変換に必要なこと

DeformConv2d を ONNX に変換できないのは、内部で使われている deform_conv2d 関数を ONNX に変換する方法が定義されていないためです。

最終的に呼ばれる torch.ops.torchvision.deform_conv2d は、 CPU, CUDA 向けなどに C++ で実装されています。
そのため、 ONNX に変換するためには専用の変換関数を用意してやる必要があるのですが、現時点では実装されていません。

似たようなものとしては、 torch.ops.torchvision.roi_pool があるのですが、こちらは torchvision/ops/_register_onnx_ops.py の中で、以下のように ONNX 変換用の関数が登録されています。

こちらにあるように、 Graph object g を用いて、 ONNX の Operator である MaxRoiPool を使って ONNX のグラフを構築していくことになります。

ONNX Operator

ONNX の Operator 一覧は GitHub にあります。

今回やる必要があるのは、ここに定義されている Operator の一覧の中から適切なものを選び、組み合わせることで、 deform_conv2d の挙動を構築することになります。

なお、 ONNX の Operator は通常の手続き的な書き方ができるわけではなく、基本的には Tensor に対する処理の組み合わせで書いていくことになります。

deform_conv2d の ONNX Operator での実装

やることは、 Deformable ConvNets v2: More Deformable, Better Results の論文に書かれた内容を愚直に実装することになります。
注意点としては、 deform_conv2d の引数として渡されてくる Tensor のデータの格納順序が一部不明瞭なところがあるので、それを実動作ベースで確認しながら進めることになります。

大まかな流れとしては、下記になります。

  1. 各 Convolution の対象となる画素の位置を求める。
  2. その画素の位置座標が、画像外を指さないように丸める。
  3. 位置座標は小数になっているので、 Bilinear 補完により、周囲 4画素の値から重み付け平均を取り、画素の値とする。
    なお、このとき、画素の値は GatherND Operator を用いて取得をする。
  4. 得られた画素の値を基に、必要に応じて mask をかけ、 Convolution を行う。
  5. 必要に応じて bias を足す。

「愚直に実装する」と書きましたが、実際にはここはかなり苦労しました。
上記の各処理を ONNX の Operator のみで記述していくのは、 deform_conv2d の内部実装ドキュメントの少なさも相俟って、なかなか大変なものがありました。

気を付けたこと

Transpose を多用しない

上記の流れを実装する際に、 (論文に沿った愚直な方法では) GatherND を使って画素値を集めてくることになるかと思います。
似たような Operator の GatherElements でも実現はできますが、入力画像のチャネル数が大きくなった場合に、 (おそらく) とても非効率になります。

一方で、現時点の GatherND Operator の仕様上、複数のチャネルにまたがってデータを一括して集めてくる場合、そのチャネルがテンソルのランクの末尾の方に位置している必要があります。
つまり、 Tensor[batch, height, width, channel] のように、座標として指定される height, width よりも末尾に channel が位置している必要があります。

そのため、 Transpose Operator (PyTorch の Tensor の permute) で軸の順序を入れかえることになりますが、この処理は状況によっては負荷がかかるようです。

当初は Transpose を多用するコードになっていたのですが、なぜか速度がまったく出ず、 ONNX の profiling をすることで、無駄な Transpose が遅い要因の一つであることが分かりました。
そのため、必要最小限な部分に絞って Transpose を呼ぶようにすることで、効率化を図りました。

ただ、この辺りは ONNX のランタイムの実装にも強く依存する話なので、環境が変わると結果も異なるかもしれません。
不要な Operator を呼ばないようにするのはよいと思いますが。

GatherND で範囲外の座標値にアクセスしない

当然ですが、 GatherND では範囲外の座標値にアクセスしてはいけません。

当初は、これを防ぐために、範囲外なのかどうかを Greater, Less Operator を使って判定した mask を作り、これを活用することで範囲外へのアクセスを防いでいました。

しかし、この方法ではそれなりに時間がかかることから、 Clip Operator を使った方法に変更しました。

これは、まず入力画像の周囲に必ず 0 クリアされた Padding 領域が入るようにしておくことで、 Clip された座標値が Padding 領域を指すようにしておき、 Bilinear 補完の計算に入らないようにすることで実現しました。

これはそれなりに効率化がされたようで、処理速度も速くなりました。

これらを気を付けることで、 (遅いことは遅いのですが) それなりに使える範囲での実行速度を実現することができました。

よさそうなら PyTorch 本体にも Pull Request を出してみようかと思います。

2020年8月3日月曜日

nginx と Flask を使った Git LFS サーバー

nginx と Flask を使った Git LFS サーバー

Git LFS サーバーを自前で用意する必要が生じたのですが、手頃なものが見つからなかったので、手作りで作ってみることにしました。
Git LFS サーバーの仕様自体はシンプルで、とりあえず動作させるぐらいであればすぐに作れたので、今後のために記録を残しておこうと思います。

また、今さらですが Docker にも慣れておきたかったので、 Docker のイメージにしてみました。本来は docker-compose を使うようなケースのような気もしますが、とりあえず単一の Docker コンテナにしてあります。

なお、作成したサーバーのソースコードなどは、 gitlfs_with_nginx として GitHub 上に置いてあります。

Git LFS サーバー

Git LFS は、 Git で大きめのバイナリファイルを管理するために GitHub が中心となって策定した Git の拡張機能です。

概要としては、以下のようになるかと思います。

  • あらかじめ指定したファイルを、 Git リポジトリではなく、別に指定した Git LFS サーバーに保存する。
    ファイルは、ファイル固有の oid (現状ではファイルの SHA256 値) と紐付けて、 Git LFS サーバー内で Key-Value ストアのように保存される。
  • 元の Git リポジトリには、実体となるファイルの oid などが書かれたポインターファイルのみを保存しておき、 checkout などの際に、適宜 Git LFS サーバーからファイルの実体をダウンロードしてくる。
  • これらのファイルの保存やダウンロードの動作は、 Git のフックなどで実現されており、ユーザーはほとんど意識することなく、通常の Git リポジトリで作業する感覚で作業することができる。

ちなみに、私は Git LFS を最初に使ったとき、「Git LFS サーバーで管理されているファイルだけ、 Subversion のようなタイミングでダウンロードされるんだな」と感じました。

それはともかくとして、 Git LFS サーバーは比較的シンプルな仕様の HTTP サーバーです。
Batch APIBasic transfer をサポートするだけであれば、(どこまで異常系を考えるかによりますが) それほど悩まずに実装することができると思います。

Flask と nginx による Git LFS サーバー

そこで、 Git LFS サーバーの理解を深めるのを兼ねて、 Python の Flask フレームワークを使って実装してみました。
また、 nginx をリバースプロキシとして使うことで、パフォーマンスを向上させています。
以下に概要のみ、記します。 これらの処理は lfs_server.py に書かれています。

batch エンドポイント (batch メソッド)

Batch API の仕様に従って、 download, uploadoperation 要求に対し、指定された oid のファイルの実体をダウンロードもしくはアップロードするための URL を返します。

このとき、ファイルの実体をダウンロード/アップロードする際の GET/PUT リクエスト時に送信してもらうヘッダを同時に返します。
このヘッダには認証用のデータなどを含めることができます。
今回の実装でも、リポジトリ名や認証期限などを blake2b でハッシュをとったものをヘッダに追加しています。

ダウンロード用エンドポイント (download_file メソッド)

ダウンロードは、基本的に nginx に処理を任せます。
Flask 側では認証処理をしたあと、 X-Accel-Redirect ヘッダを指定し、 nginx 側で静的なファイルを配信するように構成することで、 Flask 側でファイル配信しなくてすむようにしています。

具体的には、 X-Accel-Redirect ヘッダに /repos/repository_name/oid のようなパスを指定することで、 nginx から静的なファイルを配信できるようにしています。

アップロード用エンドポイント (upload_file メソッド)

アップロードも、基本的に nginx に処理を任せます。
nginx 側で client_body_temp_path, client_body_in_file_only などを使うことで、クライアント側から PUT されてきたファイルを、 nginx がファイルに保存するようにします。
そして、そのファイル名を Flask 側に渡すことで、 Flask 側では渡されたファイルを oid で一意に決まる所定の場所に移動させます。
Flask 側はファイルの移動を行うだけなので、処理の負荷が少なくてすみます。

Docker での実装

上記のような仕組みの Flask サーバーの実装や nginx の設定を行い、 Docker イメージとして構築してみました。

env.list としていくつかのパラメータは変更できるようになっており、社内利用ぐらいであれば、 Basic 認証を追加するぐらいで使えなくはないのではないかと思います。

2019年12月31日火曜日

Windows 上の Python で OpenVINO を動かす

Windows 上の Python で OpenVINO を動かす

今回は、 Windows 上の Python で、 Intel からリリースされている AI 推論エンジン OpenVINO を動作させます。
といっても、OpenVINO ツールキットそのもののインストールは不要です。
Python の pip ライブラリをインストールするだけで試すことができます。

Intel OpenVINO

OpenVINO は Intel によって開発されている、エッジ端末上で AI 推論を行うためのライブラリです。
あくまで推論に特化しており、学習は別のツールで行うことが前提です。
また、推論エンジンは Intel CPU, GPU, GNA, VPU, FPGA などに対して高度に最適化されています。

事前準備

Python のインストール

Windows 上で、 Python 3.5, 3.6, 3.7 のいずれかをインストールしておきます。
64bit 版をインストールしておく必要があります。
現時点では、 Python 3.8 には対応していません。

OpenVINO の pip ライブラリのインストール

元々の OpenVINO は、現時点では pip ライブラリになっていないようです
そのため、私が Windows 向けに非公式な pip ライブラリ (wheel パッケージ) を作っておきました。
今回はこのライブラリをインストールします。
なお、後に公式ライブラリが公開される場合にややこしくなるので、公式の PyPI には登録していません。

PyPI に登録されていないので、下記のようにインストールします。

なお、 openvino-python をインストールすると、 cv2 モジュールも使えるようになります。そのため、別途 opencv-python などをインストールする必要はありません。

また、 numpy も必要なのでインストールしておきます。

パスの設定

openvino-python を動作させる際に必要な DLL は、 pip によるインストールの際に所定のディレクトリにダウンロードされています。
venv 環境の場合、 venv/Library/bin の下にダウンロードされます。
そのため、このディレクトリを PATH の先頭に追加しておきます。

venv の場合、 venv/Scripts/activate.bat の末尾を以下のように変更しておくとよいと思います。

動作確認

上記のように事前準備ができ、パスが通った状態であれば、下記のスクリプトがエラーなく実行できるはずです。

これで、 IENetwork など、種々の OpenVINO のクラスを使うことができます。

次回は、実際に Pre-trained なモデルを使い、簡単な動作サンプルを作ってみようと思います。

2018年11月24日土曜日

1バイナリの Python と exepy

1バイナリの Python と exepy

1バイナリにした Stackless Python の更新もそろそろ終わりなので、その中に含まれている exepy ライブラリについてまとめておこうと思います。

1バイナリの Python の仕組み

この Python は、 外部 DLL や *.py ファイルが不要になっています。
DLL については、ビルド時に tcl/tk, OpenSSL などを含め、静的リンクすることで解決しています。
*.py ファイルについては、 embeddedimport というモジュールを新規に開発することで対応しています。

これらについては Single Binary 版の Stackless Python 3.6.4Single Binary StacklessPython 3.6.4 L8 リリースに簡単にまとめてあります。

exepy の仕組み

2.0.0 以降の exepy では、以下のようにして exe を作ることができます。

上記で作られた exe は、一時フォルダにファイルを展開したりすることなく、単一の exe ファイルだけで input.py の処理を実行することができます。

さて、このとき、 sample.exe は python.exe とほぼ同じバイナリになっています。
唯一の違いは、 input.py に相当する内容が sample.exe のリソースとして組み込まれているところです。

embeddedimport は、実行している exe ファイル (sample.exe) のリソースに特定の ID のリソースが含まれている場合、その内容から *.py のデータをデコードしロードします。
つまり、 import input が実行された場合、 embeddedimport は以下の順で *.py を探します。

  1. 現在実行中の exe ファイルのリソース内
  2. exe 内に静的に持っている *.py のデータ (具体的には embeddedimport_data.c の内容)
そのため、元々の python.exe にリソースを埋め込むだけで、 *.py を含んだ exe ファイルを作ることができます。

exepy のサンプル

いくつかサンプルを示しておきます。

複数ファイルからなるサンプル

main.py と sub.py があり、 main.py が sub.py を import しているとします。

上記のファイルを下記のように exe 化します。

上記で作られた sample.exe の実行は、実質的に下のコマンドを実行したときとほぼ同等の動きになります。

画像ファイルなどの抱き込み

Single Binary StacklessPython 3.6.6 L16 に含まれる exepy 2.1.0 では、画像ファイルなどの抱き込みができるようになっています。
例えば、 picture.png を tkinter で表示する例を考えます。

以下のような main.py を用意します。

こちらを以下のコマンドで exe 化します。

これで picture.png も組み込まれた exe ができあがります。

__loader__.get_data(...) で、通常のファイルのようにデータを取得することができます。
あとはこれを用いて tkinter の Label, Widget などで表示すればよいです。

今回の変更で embeddedimporter が get_data をサポートしました。
これにより、任意のバイナリデータを組み込めるようになっています。

あとは、アイコンの変更機能ぐらいを追加して開発も終了です。

2018年6月26日火曜日

Single Binary StacklessPython 3.6.4 L8 リリース

Single Binary StacklessPython 3.6.4 L8 リリース

業務の効率化や Python インタプリタの勉強も兼ねて、 StacklessPython を 1 バイナリ化していましたが、バージョン L8 まできました。

GitHub のリリースページからダウンロードできます。

今回は、主に以下の変更をしました。

  • tkinter のサポート
  • EXE 化の実験的サポート
  • Windows Subsystem 版のサポート

tkinter のサポート

これまでに Python の標準ライブラリは、ほぼすべてサポートしていましたが、 tkinter はいくつかの理由でサポートしていませんでした。

tkinter をサポートし、1 バイナリで実行するには、 tcl/tk のライブラリを静的にリンクする必要があります。
また、 tcl/tk は、いくつかの *.tcl ファイルを必要とするので、こちらについても対応する必要があります。

静的リンクについては、 nmake -f makefile.vc core OPTS=static のように OPTS として static を指定すれば、静的リンク版をビルドできるので、こちらを使うことになります。

一方で、*.tcl ファイルの抱き込みについては、やや面倒です。

*.tcl ファイルの抱き込み

tcl では、Tcl_Filesystem という構造体で、複数のファイルシステムを持つことができます。
これは一言で言うと Python の path_hooks に相当するものです。
今回は embeddedFilesystem というファイルシステムを作成し、 embeddedfs:/tcl8.6/init.tcl のように embeddedfs:/ で始まる仮想的なパスが指定された場合に、バイナリの中に保持しているファイルの中身を返すようにしています。

詳しくは tclEmbeddedFilesystem.c を参照してください。

EXE 化の実験的サポート

せっかくなので、おまけの機能として EXE 化の機能を付けてみました。
なお、元々、標準ライブラリ外の機能は考慮していないので、py2exe や PyInstaller の代わりには使えません。あくまで、標準ライブラリだけを使って書けるスクリプトの EXE 化ができます。

例えば下記のように、 tk でウィンドウを作成し、クリックでカウントアップするだけのプログラムを EXE 化してみます。

今回の Single Binary StacklessPython では、上記のように tkinter を使うような場合も、実行することができます。

これを、単一の EXE にするには、下記のようにします。
ただし、これはまだ実験的な機能なので、将来は仕様を変更するかもしれません。

上記を実行すると sample.exe が生成されます。

サイズは 14MB ぐらいになりますが、テンポラリディレクトリにファイルを展開したりはしないので、かなり高速に実行されます。

Windows Subsystem 版のサポート

今回から pythonw.exe のように、コンソール画面が表示されないバイナリを作っておきました。

こちらは、主に GUI を使うスクリプトを EXE 化する際に使うと便利です。

Python, tcl/tk の実装の勉強にもなり、 AppVeyor の利用の勉強にもなりましたが、すべての標準ライブラリを 1 バイナリに収めたので、そろそろ終了にする予定です。

2018年6月13日水曜日

Single Binary 版の Stackless Python 3.6.4

Single Binary 版の Stackless Python 3.6.4

Stackless Python が 3.6.4 対応されていたので、 1 バイナリ化した Stackless Python も対応してみました。

GitHub のページから、ダウンロードしてご利用ください。

1 バイナリ化した Stackless Python

以前に Stackless Python 2.7 を 1 バイナリ化しましたがその当時の変更を Python 3.6 に対応させることになります。
今回は、その際の対応と、そもそも 1 バイナリ化した Stackless Python はどういうものかについて、まとめようと思います。

1 バイナリ化した Stackless Python 作成の目的

業務の都合上、「種々の PC 実機上で簡単に Python を実行したい」という動機がありました。
それに加えて、 pausable unittest を実行したかったので、通常の Python ではなく Stackless Python を使うことにしました。

また、極力、通常の Python と比べて、実行速度が遅くならないようにしたいということも考えていました。
前に Ruby を OCRA で固めたときは、実行前の展開にやや時間がかかるのがストレスだったので、今回はそういったことがないようにしたいと思っていました。

1 バイナリ化した Stackless Python の変更点

デフォルトの Stackless Python から以下の変更を加えました。

  • プログラム引数の変更
  • embeddedimport の実装
  • 3rd party ライブラリの同梱
  • その他の小変更

プログラム引数の変更

1 バイナリ化した Stackless Python は、引数の意味を二つだけ変更してあります。

  • -B: この引数は意味が逆になっており、-B が存在しないときに、 *.pyc ファイルを作成しないようになっています。
  • -E: この引数も意味が逆になっており、-E が存在しないときに、環境変数を参照しないようになっています。

これらの変更は、いずれも未知の環境で動作しやすいようにしておくためです。
時刻が正しくない複数の実機で実行した場合や、他に Python の処理系がインストールされていた場合にも、正しく動作させるためです。

embeddedimport の実装

通常、 Python の import 文は、 sys.path に登録されたパスを順に探し、その中から外部モジュールをロードします。
また、 sys.path には python.zipも 登録されており、 ZIP ファイルの中もサーチの対象になっています。
この ZIP ファイルからのロードは、 zipimport というモジュールが担っています。

今回は、「一つのバイナリにしたい」という目標があったので、 zipimport を参考にして、 embeddedimport を作り、実装しています。

挙動の概要としては、以下のようになります。

  1. ビルド時に、 Lib 以下の *.py ファイルをすべて C 言語の文字列に変換して、 python のバイナリに組み込んでおきます。
  2. 実行時には、 sys.pathC:/path_to/python.exe のように python.exe のフルパスを登録しておきます。また、 sys.path_hooksembeddedimporter モジュールを登録しておきます。
  3. これにより、 import pickle などが処理される際に embeddedimporter によってモジュールのインポートが試みられます。
  4. embeddedimporter では、自身が保持している *.py ファイルの中から指定されたファイルを探し、それを実行します。

3rd party ライブラリの同梱

快適に使えるように、いくつかのライブラリを同梱してあります。

  • comtypes
  • pycodestyle
  • pyflakes
  • pyreadline
  • PyYAML with libyaml

PyYAML に関しては、 libyaml をビルドしておき、 C 言語のサポートがあった方がずっと速いので、そのようにしてみました。

一点、注意としては、 libyaml とのブリッジライブラリである _yaml は、 Cython 形式になっており、 C 言語としてビルドするには前処理が必要でした。
Stackless Python の場合、 Fast Calll 時の関数呼び出しの処理が変更されているので、 CYTHON_FAST_PYCALL を 0 に設定し、 _yaml から Fast Call させないようにすることが必要でした。

その他の小変更

プログラムの引数を変更した関係で、いくつかのテストを変更しました。 -E を使っているテストは非常に多かったので、その辺りを中心に変更しました。

また、 C 言語で書かれた拡張ライブラリ *.pyd を、静的に Python にリンクするために、ビルド方法や config.c でのビルトインモジュールの定義部分を変更しました。

私の業務の都合上、 XML の属性をソートしたくなかったので、 xml.etree.ElementTree の出力部分に少し手を加えました。
具体的には ElementTree.writesort_attrib=True というキーワード引数を追加し、ソートを制御できるようにしておきました。

これらの対応を行い、テストがほぼ通ったので、ついにリリースになりました。
Windpws PE 上での作業や、多数のテスト実機上で Python を走らせる必要がある場合などに、ご利用いただければと思います。

2018年3月8日木曜日

Windows 上で一時的に Python を使って作業する際に使っているバッチファイル

Windows 上で一時的に Python を使って作業する際に使っているバッチファイル

私は、仕事でクリーンな (OS 展開直後の) Windows 実機や Windows PE 上で、Python を用いて作業をすることがしばしばあります。
今回は、その際によく使っているスクリプトを備忘録としてまとめておきます。

いずれも、 Python の -x オプションを使って、バッチファイル先頭行の goto 文をスキップしています。

1バイナリの Python

前提として、1バイナリ化した Python を使います。

以前に書いたように、ほぼすべての標準ライブラリを含んだ StacklessPython を、単一の EXE ファイルをコピーするだけで動作するようにしてあります。
こういった用途のために作ったので、インストールが不要かつコピーするだけで使えるようにしました。

管理者権限で実行する Python スクリプト

下記のようなバッチファイルを用意し、print("hoge") 部分を好きな Python スクリプトに変更すれば、ダブルクリックするだけで、管理者権限の Python スクリプトとして実行することができます。
なお、同じディレクトリに python.exe が必要です。

readline を有効にした状態で Python インタプリタを立ち上げるスクリプト

こちらも、下記のようなバッチファイルを用意し、ダブルクリックすれば、 Python のインタプリタが立ち上がります。
pyreadline を使っているので、 Ctrl-f などのキーバインドや、補完、ヒストリ保存もできます。

作業の内容によっては使えると思いますので、置いておきます。

2017年12月16日土曜日

Outlook のキーワード強調表示用のフォントを作成する

Outlook のキーワード強調表示用のフォントを作成する

LigatureYourName

前回の Outlook でキーワードハイライトを実現する方法の続きです。

LigatureYourName というサイトでフォントを作成することができます。
せっかくなので、 Heroku で WEB サービスとして作成してみました。

フォントの情報

ベースフォント
ベースとなるフォントは、源真ゴシックです。
ライセンス
源真ゴシックに準じ、 SIL Open Font License 1.1 のもとに使用することができます。

その他

  • アルファベットなど、日本語以外の文字ではうまく強調表示されないことがあるようです。
    アプリケーションの描画方法に依存するようですが、理由をご存知でしたら教えてください。
  • 種類は、デフォルトで OpenType (PostScript) です。
    これは、 Windows10 の Creators Update より前では、TrueType 形式のフォントは汚く表示されることがあるためです。
    TrueType 形式が必要な場合、「詳細」欄で選択することができます。
  • このフォントの利用により生じたいかなる損害についても、一切責任を負いません。
    ご自身の責任の範囲でお使いください。

次回は、 LigatureYourName のサイトを作った際の技術的な内容について、まとめる予定です。

Outlook で本文中の固定キーワードを強調表示する方法

Outlook で本文中の固定キーワードを強調表示する方法

はじめに

Outlook は広く使われているメーラーの一つではないかと思いますが、なぜかキーワードハイライトができないようです。
例えば、自分の名前を本文中で常にハイライトしたいのですが、私が探した限り、そういった方法は見つかりませんでした。

以前に使ったことがある Becky!Thunderbird では標準機能やプラグインでサポートされていますので、不便に感じていました。

今回は Outlook で固定キーワードを強調表示する方法についてまとめます。

キーワードハイライトの仕組み

専用フォントによるハイライト

Outlook 用に、専用のフォントを入れ、そのフォントで表示することで実現します。
そのフォントでは、あらかじめ強調したいキーワードの文字だけを太字にしておくなどすることで、疑似的にキーワードハイライトすることができます。
例えば、「ハイライト」というキーワードを白黒反転で強調するような場合、以下のようになります。

このように、あらかじめ指定した「ハイライト」が指定した白黒反転で強調表示されています。
サンプルページで実際の挙動を確認できます。

リガチャ (合字) によるキーワード指定

前掲した例を見ると分かりますが、「ライト」は強調表示されていません。
これは、フォントの「リガチャ (合字)」機能を使って実現しているためです。

Wikipedia の合字ページを見ると分かりますが、リガチャとは欧文などで特定の並びの文字に専用のフォントを指定できるようにする機能です。
よく目にする例では、ff の並びに対し、二つの f がいい具合に合わさったものがあります。

このフォントの機能を活用し、日本語でも特定の並びの文字に対してリガチャを作成することで、疑似的なキーワードハイライトを実現することができます。
今回の例では、「ハイライト」を白黒反転したものを一つのグリフ (字形) として作成し、それを「ハイライト」の文字の並びに対して使うように、リガチャ指定しています。
こういった仕組みのため、部分文字列の「ライト」が強調表示されることはありません。

制限事項

この仕組みでキーワードハイライトを実現する場合、以下のような制限事項があります。

強調するキーワードを動的に変更できない
強調するキーワードを変更する場合、フォントを生成しなおす必要があるので、動的にキーワードを変更することはできません。
リガチャをサポートしている Outlook でのみ使える
具体的には Outlook 2010 以降が必要です。
(Outlook で使う場合) HTML メールの強調には使えない
テキストメールの読み書きに対してしか使えません。
改行がはさまるとリガチャにならない
改行が挟まった場合は、通常の文字として表示されます。

次回は、実際にフォントを作成する方法についてまとめます。

2017年10月27日金曜日

Nyagos の拡張 (主に Subversion 用 Lua スクリプト)

Nyagos の拡張 (主に Subversion 用 Lua スクリプト)

前回は Nyagos を tcsh 風に使うようにするための変更についてまとめました。
今回は、それに加えてもう少し Subversion まわりについて拡張してみました。

できること

作成した vcs_info.lua で、以下のことができます。

  • svn ls https://github.com/masamitsu-murase/nyagos.git/trunk/[Tab] のように、 svn で始まる場合に URL のパスに対しても補完することができます。
  • VcsInfo.box_svn_revisions(path) のように呼ぶことで、 C-x C-g の git のリビジョン選択のように、 Subversion のリビジョンを選択することができます。
  • VcsInfo.check_vcs_branch() で、現在のパスが svn, git, hg のいずれかを判別し、返してくれます。
    また、 git, hg の場合はブランチ名を、 svn の場合は親ディレクトリの名前を、二つ目の戻り値として返してくれます。

必要なファイルは nyagos.d/catalog/vcs_info.lua として GitHub に置いてあります。
このファイルを nyagos.d/catalog 以下に置き、 .nyagos でいくつか設定をします。

Subversion の URL の補完

この機能を使うには、 vcs_info.lua.nyagos で use するだけです。
.nyagos の例を見てください。

このスクリプトを使うと、svn で始まるときに限り、後続の URL を補完してくれます。

svn ls コマンドで、リポジトリのディレクトリを見ながら補完するので、環境によってはやや遅いですが、私としてはそれなりに快適に使えています。

また、デフォルトでは直前の補完結果を 10秒間はキャッシュしていますので、連続で Tab を押したときは軽快に動くようになっています。

Subversion のリビジョンの選択

コマンドラインでマージをする際に、マージ対象のリビジョンを手で入力するのは面倒です。
git の場合は、 C-x C-g でログを表示し、ハッシュ値を入力することができますが、これと同じようなことができることを目指しました。

この機能を使う場合、 box.lua のコードを参考に、以下のようなコードを .nyagos に追加する必要があります。

上記のように、 VcsInfo.find_svn_path で現在のコマンドライン入力の中から URL らしきものを探し、 VcsInfo.box_svn_revisions でリビジョンを選択することができます。

git, svn, hg のブランチ名の取得

VcsInfo.check_vcs_branch を呼ぶことで、現在のディレクトリのブランチ名を取得することができます。

以下のようなルールでブランチ名を取得しています。

git, hg
git rev-parse --abbrev-ref HEADhg branch でブランチ名を取得しています。
svn
現在のディレクトリを含むチェックアウトツリーのルートディレクトリを探し、その一つ上のディレクトリ名を取得します。
例えば、http://example.com/trunk をチェックアウトしたディレクトリ以下なら、trunk を返します。
同様に、http://example.com/branches/feature_a をチェックアウトしたディレクトリ以下なら、 feature_a を返します。

私は、この機能を用いて以下のようにプロンプトを表示しています。

このスクリプトだと、以下のようにプロンプトが表示されます。

もちろん、 Subversion のディレクトリ名が日本語を含む場合でも、適切に動作します。

Nyagos は Lua で拡張できるおかげで、割と楽に拡張することができました。

2017年10月1日日曜日

NYAGOS を Windows で tcsh 風に使う

NYAGOS を Windows で tcsh 風に使う

私はこれまで Windows 上で tcsh をシェルとして使っていました。しかし、 RS2 にアップデートしたあたりから、なぜかクラッシュすることが多くなってきたので、代替のシェルを探していました。
パスを Windows 風にも使えることと、慣れてしまった tcsh 風のキーバインドをサポートしていることを条件に、いろいろなシェルを探してみました。

その結果、 tcsh から NYAGOS に変更してみることにしました。
ここでは、 tcsh に似せるために標準の NYAGOS に追加した、いくつかの変更についてまとめます。

NYAGOS とは

NYAGOS は zetamatta さんによって Windows 向けに開発されているシェルです。
本体は Golang で書かれており、 Lua スクリプトで拡張できるように設計されています。
また、日本語にもしっかりと対応されています。

素晴らしいシェルを公開していただき、ありがとうございます。

Windows で使えるシェルとしては、NYAGOS 以外にもたくさんのものがありますが、先に挙げた条件に加え、「Cygwin などの、 Windows ネイティブな環境でないものは使いたくない」、「日本語にきちんと対応しているものがよい」、「Golang を勉強したい」という理由により、 NYAGOS を使おうと思いました。

使い方

必要なファイル類は、 GitHub の私の Fork の tcsh_style のブランチにあります。

公式のリリースに比べ、上記のブランチでは nyagos.d/catalog の下の以下のファイルが追加されています。

case_sensitive_completion.lua

大文字と小文字を区別して補完するためのスクリプトです。

dollar_env.lua

環境変数を $TEMP 形式で (ある程度) 使えるようにするためのスクリプトです。
また、 echo $TEM[Tab] のような場合に、$TEMP と補完もしてくれます。
ただし、 $TEMP/[Tab] で Temp ディレクトリ以下のファイルを補完してくれたりはしません。

emphasize_completion.lua

補完候補の最初の一文字を強調表示するスクリプトです。
例えば、フォルダに fileA.txt, fileB.txt, fileC.txt がある場合、 ls file[Tab] と入力すると、 fileA.txt, fileB.txt, fileC.txt のように、次に入力するべき文字が強調されて表示されます。

tcsh_like_keybind.lua

tcsh の下記のキーバインドを実現します。

Alt+/
現在入力中の単語を、過去の入力データをもとに補完します。
Alt+p, Alt+n
現在入力中のコマンドを、過去の入力コマンドをもとに補完します。
Alt+BackSpace
カーソルより左の単語を一つ消去します。
Alt+d
カーソルより右の単語を一つ消去します。

なお、完全に tcsh と同一な動きをするわけではありませんし、それを目指しているわけでもありません。
日本語には対応しているつもりです。

.nyagos ファイルへの変更

上記の変更を有効にするために、 .nyagos ファイルに下記のような変更をします。

上記のように、末尾で use により先ほどのファイルを読み込みます。これで、それぞれの機能が有効になります。
なお、 dollar_env に関しては、 completion_hook の対応がやや古い状態なので、他のファイルより先に読み込んでおくとよいです。
また、 tcsh_like_keybind に関しては、内部で nyagos.prompt の書き換えを行っているので、 nyagos.prompt よりも後で読み込む必要があります。

これらの変更により、快適に Windows 上で tcsh ライクなシェル操作を実現できました。

2017年8月16日水曜日

Microsoft Edge の Native Messaging を使った拡張機能 その1 (UWP を用いた Native Messaging 拡張機能の概要)

Microsoft Edge の Native Messaging を使った拡張機能 その1 (UWP を用いた Native Messaging 拡張機能の概要)

Firefox, Chrome に続いて、 "Open TortoiseSVN" を Microsoft Edge にも移植してみたので、そのまとめです。

もう少し記事を分けるかもしれませんが、以下のような順番で書いていこうと思います。

  • その1: UWP を用いた Native Messaging 拡張機能の概要
  • その2: パッケージングの方法と、 Win32 アプリケーションの起動

なお、最終的なソースコードは GitHubの Open TortoiseSVN for Microsoft Edge にあります。

Edge の拡張機能のサポート

元々 Google Chrome の拡張機能で導入された拡張機能の形式ですが、 Mozilla Firefox にも WebExtensions として導入され、 Edge でも同様の機能がサポートされ始めていました。

具体的には、2016年の Anniversary update 以降、 Microsoft Edge も Chrome や Firefox とほぼ同様の拡張機能をサポートしています。
そして、 2017年の Creators update で、 Native Messaging のサポートも開始されました。

詳しい内容は Native messaging in Microsoft Edge のページに載っているので、基本的な作成方法などはこちらを参照するのがよいでしょう。

この記事では実際に上記のページに従って拡張機能を作成した際に、気になったところを中心に書こうと思います。

また、 Microsoft によるサンプルは SecureInput として GitHub で公開されています。

Firefox, Chrome との違い

Edge での Native Messaging サポートは、 Firefox, Chrome とは大きく違います。

Native messaging in Microsoft Edge のページに書かれているように、下記が特徴的です。

  • JavaScript で書かれた Extension は、 UWP 形式のアプリケーションとのみ通信できる。 Win32 形式のアプリケーションとは通信できない。
    Firefox, Chrome の場合は、 (Windows であれば) Win32 形式のアプリケーションと通信できる。
  • UWP アプリケーションとは、 AppService の仕組みを用いて通信を行う。
    Firefox, Chrome の場合は、標準入出力で JSON 形式のデータを所定のフォーマットでやり取りすることで通信を行う。

つまり、外部アプリケーションと通信する拡張機能の JavaScript 側のインターフェースは Firefox, Chrome と (ほぼ) 同じですが、それを受ける側であるアプリケーションの仕様は、まったく異なることになります。

なお、 Edge の Native Messaging 拡張機能の特徴として二つ書きましたが、実際には「外部のアプリケーションとは AppService の仕組みで通信を行う」という部分が重要です。
これは、 AppService は UWP アプリでしか作成できないので、必然的に UWP 形式のアプリケーションが必要条件となるためです。

UWP アプリとの通信

作成する AppService は、通常のものとまったく同じものです。なので、 UWP アプリで AppService を作成する方法については、他のページを探してみてください。
ここでは、 Edge との通信部分について書きます。

例として、仮に、 com.example.uwp という名前の AppService を作成するとします。

JavaScript 側のコード

その場合、 JavaScript 側から UWP アプリを呼び出すのは以下のようになります。

このように、 browser.runtime.sendNativeMessage もしくは browser.runtime.connectNative の第一引数で指定する application の名前は、 AppService の名前そのものになります。
それ以外は、 Firefox, Chrome の場合と何も違いはありません。

UWP 側のコード

AppService を実装する方法としては、 in-process な実装と、 out-of-process な実装の二種類があります。
詳しくは Microsoft の説明などを見るとよいでしょう。

Open TortoiseSVN では in-process な方法で実装したので、下記を実装する必要があります。

OnBackgroundActivated
他のアプリケーション (今回の場合は Edge) から AppService への接続があった場合、この関数が呼ばれます。
通常の AppService と同じように、この中で Connection に対してイベントハンドラを登録します。
OnAppServiceRequestReceived
他のアプリケーション (今回の場合は Edge) からメッセージが送られてきた際にそのメッセージを処理するためのイベントハンドラです。
Native Messaging のメッセージ処理の手順に合わせて実装します。

OnBackgroundActivated

OnBackgroundActivated の概要としては下記のような実装になりました。

こちらに関しては、特に通常の AppService と違いはないはずです。

OnAppServiceRequestReceived

この関数では、 Edge からのメッセージを受け取り、それに応じて処理を行います。

概要としては、以下のようなことをすればよいです。
ここでは、上で書いた例のように JavaScript 側から {"action": "action1", "args": ["arg1", "arg2", "arg3"]} という引数で呼ぶことを考えてみます。

いくつか重要な点を挙げておきます。

Edge からのメッセージは ValueSet の一つ目の Value として渡される。

ValueSet は辞書のように Key, Value の組を保存できるものですが、 AppService ではこの ValueSet を用いてデータをやり取りすることになっています。そのため、 Edge の Native Messaging 拡張機能も、 ValueSet でデータをやり取りすることになります。

Edge から送られてきたデータは、 JSON 形式の文字列となって、渡された ValueSet オブジェクトの最初の (唯一の) 要素の Value に格納されてきます。

Key は "Message" になっているようですが、これを仮定しないのがよいと思います。

UWP アプリからのメッセージも同様に ValueSet として返す。

逆方向もまったく同じです。
Key は適当な名前でよいので、 Value に JSON 文字列を指定し、 SendResponseAsync でメッセージを送ります。

JSON 形式にシリアライズするためには JavaScriptSerializer と匿名型を用いるのが楽でよいと思います。

なお、本筋とは関係ないですが、やや複雑な JSON 文字列をパースする場合、以下のようにすればよいです。
例えば、 {"action": "action1", "args": ["arg1", "arg2", "arg3"]}args をパースする場合は、例えば以下のようにすればよいです。

このようにすることで、 "args" の中身を配列で取り出すことができます。

あとは UWP アプリ内で好きな処理をすればよいわけです。

注意点

Microsoft EdgeHTML 15.15063 の時点では、 UWP アプリからの戻り値が (私の予想と) やや異なった形式で返ってきます。

具体的には、下記のようになります。

このように、 Firefox, Chrome と違ってわざわざ JSON.parse する必要があります。

私はこの挙動はバグのように思いましたので、 issue として登録しましたが、まだ有効な回答はない状態です。

続いて、作成した UWP アプリを JavaScript のコードとまとめてパッケージングし、 Edge の拡張機能として登録することになります。

次回以降は、下記について書く予定です。

  • Appx パッケージの作成
  • Win32 アプリケーションの起動
  • browser.runtime.sendNativeMessagebrowser.runtime.connectNative における注意点

2017年4月23日日曜日

Pausable Unittest その2 (開発の経緯)

Pausable Unittest その2 (開発の経緯)

概要

今回は、なぜ Pausable Unittest を Stackless Python のライブラリとして作成したかについて書いておこうと思います。

なお、ここで言及している Python は、基本的には Python 2.7 での話です。
最新の Python 3.5, 3.6 系列ではやや異なっているかもしれません。

Pausable Unittest 実現に必要な機能

Pausable Unittest では、以下のようなスクリプトを書くことを目的としています。

この中で重要なのは、(*1) の部分で Python インタプリタを終了し、システムを再起動した後で、続きを実行できるところです。

これを実現するためには、「関数の実行の中断と、その状態の保存/復元」が必要になります。
self.reboot() では、実行中の関数 test_check_reg を中断し、そのローカル変数の状態やどこまで実行したかの状態を含めて保存し、再起動後に復元する必要があります。

generator, fiber, coroutine, continuation

そこで、この機能を実現するために必要な要素技術は何か、について、検討をしました。
その結果、ジェネレータファイバーコルーチン継続などを、ファイルに保存し、あとで復元できればよいという考えに至りました。

ジェネレータ、ファイバー、コルーチン、継続は、いずれも「関数のようなものを途中で中断し、他に制御を移すことができる」という機能を持っています。

例えば、 JavaScript で書いたジェネレータでは、下記のようになります。

上記の例では、1, 2, 3, ..., 5 の順で数字が出力されます。
つまり、g.next() でジェネレータ gen に制御を移し、 yield で制御を戻す、といった流れになります。

これは、関数のような genyield によって中断できていることになります。

なお、 JavaScript ではジェネレータを定義する際には通常の関数と異なり function* を用いますが、 Python では「関数の中に yield があればジェネレータ」となっています。

参考までに Ruby のファイバーは下記のようになります。

こちらも同様に、1, 2, 3, ..., 5 の順で数字が出力されます。

このように、ジェネレータやファイバー (やコルーチン) には、「処理を中断し、処理を他に移すことができる」という特徴があります。

あとは、途中まで実行したジェネレータやファイバーを、ファイルに保存できればよいことになります。
先に挙げた Ruby の例では、下記のようなことができるとよいわけです。

しかし、これは Marshal.dump に失敗し、例外が送出され、うまく動作しません。

通常の Python でジェネレータを使って同様なことを書くと、やはりジェネレータを pickle できず、同様にうまく動作しません。

Stackless Python を選んだ経緯

通常の Python や Ruby で実現できない理由

通常の Python や Ruby で実現できないのは、もちろん「ジェネレータやファイバーをファイルに保存できないから」です。
これは、ただ未実装なわけではなく、技術的に実装が困難だからです。

Python や Ruby でのメソッドの呼び出され方

通常の Python や Ruby では、メソッドの呼び出し時にスタックが消費されます。
例えば、下記のような Python のスクリプトを考えます。

この関数 foo は、バイトコンパイルの結果、以下のようなバイトコードに変換されます。

Python は、与えられたソースコードを上記のようなバイトコードの変換した後、実際に処理を行います。
このとき、各行の命令を実行していくわけですが、ここで関数呼び出しの OpCode である CALL_FUNCTION がどのように処理されるかに注目してみます。

Python での CALL_FUNCTION の処理

現在の Python や Ruby では、ソースコードをバイトコードにコンパイルした後で、それを仮想マシン上で実行するようになっています。
Python の場合、上記のようなバイトコードに変換した後で、実行していきます。

「関数の呼び出し」の場合、通常は CALL_FUNCTION という OpCode に変換されます。
その後、実行時に ceval.cPyEval_EvalFrameEx 関数の中の switch 文で解釈されて実行されます。
Python 2.7.10 のコードを一部だけ抜き出すと、下記のようになっています。

このように、関数の実行そのものは call_function に委ねられ、処理が続けられます。

call_function では、いくつかの条件によって処理が分岐していきますが、間接的に PyEval_EvalFrameEx 関数が呼ばれます。
たとえば、ある条件下では fast_function が呼ばれますが、その中を追っていくと、条件に応じて PyEval_EvalFrameEx が呼ばれるのが分かります。

つまり、 Python インタプリタでは、 Python の関数呼び出しを Python 仮想マシンが実行する際に、 PyEval_EvalFrameEx の再帰呼び出しが発生し、その結果、スタックが消費されていることが分かります。

スタックが消費されることによる影響

スタックが消費される実装になっている場合、関数の状態を復帰させるのが非常に困難になります。

C 言語でのスタックの使い方は完全にコンパイラに依存しています。
例えば、スタック上には種々のローカル変数が配置されますが、それらをダンプしたり、復旧させたりする手段は、 C 言語には提供されていません。
もちろん、環境や OS 依存な方法を使ってスタックをダンプすることはできますが、ダンプしたスタックをそのまま復旧しても、状態が正しく復元されるわけではありません。
スタック上には種々の変数のアドレスなどがある場合もありますが、スタックがダンプされたときと復旧させた時で同じアドレスが使われるとは限らないからです。

そのため、スタックがある程度消費された状態を復元するのは、困難なものになります。

Stackless Python での CALL_FUNCTION の処理

一方で、 Stackless Python では、以下のようにやや処理が異なっています。

このように、 STACKLESS マクロに応じて、goto によるジャンプなどがはさまっています。

Stackless Python では、 call_function の中では、 Python の関数呼び出しは行わず、すべての処理が PyEval_EvalFrameEx のループに戻ってから行われるように工夫されています。
俗に「トランポリン呼び出し」と呼ばれるような方法に近い実装がされています。

その結果、 Stackless Python では、(一部の例外はありますが) Python の関数呼び出し時に、原則として再帰呼び出しが行われません。

こうした実装上の工夫や、 prickelpit.c などで追加されている pickle に関する処理により、 Stackless Python ではジェネレータを pickle することが可能となっているようです。

Stackless Python を選択する

主に、これらの技術的な理由により、個人的に好みの Ruby ではなく、 Stackless Python を選ぶことにしました。
また、 PyPy でも、同様にジェネレータを保存できるので、 Python を選んでおけば、仮に将来的に Stackless Python の開発が止まっても、ある程度の代替案が確保できるだろうという目論見がありました。

おそらく、 Scheme や Lua などでも同様のことができるのかもしれませんが、こういった理由により、 Pausable Unittest を実装する言語として、 Python を選択しました。

2017年1月21日土曜日

Ruby で net/http を使ってファイルなどを multipart で POST する

Ruby で net/http を使ってファイルなどを multipart で POST する

今回は、自分用の覚え書きです。

net/http を用いた multipart の POST

Ruby で HTTP クライアントを書いていると、しばしばファイルなどを POST したいことがあります。
この場合、 multipart なデータを POST することになります。

この機能は、標準ライブラリである net/http のみを用いて、割と簡単に実現できます。

よくネットで見つけられる set_form_data ではなく、set_form を使うのがポイントです。

set_form(params, enctype="application/x-www-form-urlencoded", formopt={}) の引数

params

set_form の一つ目の引数である params には、配列の配列を渡します。
それぞれの配列は、以下の形式になります。

[ (パラメータ名), (値 or ファイルオブジェクト), (オプションのハッシュ) ]

上の使用例を見れば簡単に理解できると思いますが、オプションのハッシュを指定することで、ファイル名や種類を指定することができます。

これらの値の組を配列として指定することで、送信するデータを決められます。

enctype

二つ目の引数である enctype には、 "multipart/form-data" を指定します。

デフォルトは "application/x-www-form-urlencoded" なので、ファイル送信するときは、必ず指定する必要があります。

formopt

三つ目の引数である formopt には、 :charset:boundary を指定することができます。

:boundary は、指定しなければランダムで URL-safe な文字列が生成されて使われます。
そのため、通常は特に指定する必要はないでしょう。

以上のような引数を指定し、 set_form を使うことで、簡単にファイルなどを multipart で送信することができます。

なぜか日本語のリファレンスから漏れており、(そのせいか) 日本語の記事があまりないようなので、メモとして書いておきます。
暇があったらリファレンスに載せてもらうようにお願いする予定です。