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

銀の光と碧い空

クラウドなインフラとC#なアプリ開発の狭間にいるエンジニアの日々

PowerShell Cmdlet の中でRxを使いたい

PowerShell Advent Calendarの4日目の記事です。

PowerShell Advent Calendar 2014 : ATND

去年もPowerShell Advent Calendarなのに C# な話だったんですが、今年も性懲りもなくC# コード満載のCmdletネタになります。

PowerShell を C# から実行する - 銀の光と碧い空

最近、マルチプラットフォーム対応のポータブルクラスライブラリが話題ですが、PowerShell Cmdlet として実装するC# プロジェクトでも中身のロジックをポータブルにしておくといろいろ便利です。といっても、AndroidとかiOSに対応するところまでいかなくとも、ロジックのクラスをクラスライブラリとして分離して Cmdlet のクラスから参照してPowerShell から呼び出すほかに、たとえばそのクラスライブラリをWPFツールから参照してGUIから実行する、といったことができます。

さて、そんな状況で、わりと長い(数分くらい)の処理を実行する Cmdlet とその中身のロジックを作ることになりました。また、この処理はWPFのツールからも実行したいので、上に書いたようにクラスライブラリとして分離させることにしました。当然数分かかるため、進捗状況を知りたいので、処理の途中経過でメッセージで表示することにしました。単純に処理の結果だけ取得するなら、Task な返り値でいいのですが、進捗状況を知りたいとなると。。。と悩んだ結果、Rxを使って IObservable で進捗状況を返すことにしました(Tは独自定義の進捗状況を表すクラス)。で、IObservable なAPIをCmdletクラスで呼びだそうとして、問題はここで起きたのです。

問題を再現するためのコードを紹介します。まず、IObservable なクラスライブラリのメソッドはこんなものです*1

で、これを Cmdlet で呼ぶために、まずこんな書き方をしてみました。

Select メソッド内で飛んできた進捗を Write-Object しつつ、処理の完了を待つために、 FirstAsync したものを Wait しています。 これを実行するとこんなことになります。

Invoke-BadHelloRx : WriteObject メソッドと WriteError メソッドは、BeginProcessing メソッド、ProcessRecord メソッド、
および EndProcessing メソッドの上書きの外側から呼び出すことはできず、同じスレッド内からだけ呼び出すことができます。
コマンドレットで呼び出しが正しく作成されていることを確認するか、または Microsoft カスタマー サポート サービスにお問い合わせください。
発生場所 行:3 文字:1
+ Invoke-BadHelloRx
+ ~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [Invoke-BadHelloRx]、PSInvalidOperationException
    + FullyQualifiedErrorId : InvalidOperation,ObservableCmdletSample.BadRxCmdlet

ぐぬぬ... Write-Object は呼びだされたスレッドの中から実行しないといけないのですね...((ちなみに、async void にして、Waitメソッドの代わりにawaitすると、PowerShellが強制終了し、ほとんど情報のないイベントログがイベントID:1000 として記録され、さらに訳が分からなくなります)) では、ということで、SynchronizationContext.Current を取得して、 ObserveOn してみましょう。

実行すると...

Invoke-BadHelloRx : 値を Null にすることはできません。
パラメーター名:context
発生場所 行:3 文字:1
+ Invoke-BadHelloRx
+ ~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Invoke-BadHelloRx], ArgumentNullException
    + FullyQualifiedErrorId : System.ArgumentNullException,ObservableCmdletSample.BadRxCmdlet

おお、ぬるぽ!!!111 Cmdletの中は SynchronizationContext.Current が null でした。

対処法を検索したところ*2*3、このCmdletが呼ばれる環境(コマンドプロンプトの中なのか、WPFの中なのか...など)がわかれば、それに対応した SynchronizationContext を作ればいいようなんですが、そもそもいろんな環境で再利用したいので、そういうこともしたくない...

と悩んでいたところ、CTO に教えてもらったのがこの方法です。

BlockingCollection<Action> を使って、RxのSubscribeの中でQueueに処理を追加し、後から逐次実行していくイメージです。

実行してみると...

f:id:tanaka733:20141201115100p:plain

無事に実行できました。この書き方がベストかどうかはわからないのですが、PowerShell Cmdlet から RxなAPIを叩くときに困ったときの助けになれば幸いです。

*1:この程度なら、Subject使わずに Observable クラスのメソッドチェーンで書けそうですが、実際は様々な非同期処理をawaitしている処理になっています。

*2:c# - Powershell custom cmdlet Calling WriteVerbose outside of implementation of standard methods - Stack Overflow

*3:powershell-for-developers/WPFJob.cs at 437aab5821e8de698168f245413d3ad725b40f7f · dfinke/powershell-for-developers · GitHub