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に処理を追加し、後から逐次実行していくイメージです。
実行してみると...
無事に実行できました。この書き方がベストかどうかはわからないのですが、PowerShell Cmdlet から RxなAPIを叩くときに困ったときの助けになれば幸いです。