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

ぼくがかんがえたさいきょうの PowerShell モジュール仕様 6.0

本記事は PowerShell Advent Calendar 2016 の 5 日目の記事です。
昨日は arachan さんの PowerShellで遠隔操作の準備です。
明日は stknohg さんの PowerShell上でdockerコマンドの自動補完を行うposh-dockerモジュールについてです。

さて、2016 年も終わりが見えてまいりました。
今年は PowerShell にとってはいろいろとあった年でした。
何があったかは、1 日目の牟田口さんの記事 2016年のPowerShellを軽く振り返ってみるをご覧頂くのが良いと思います。

PowerShell も公開から 10 年、バージョン 5.1 が公開されるまでになりました。
長く使っていると、いろいろと不満も溜まってくるものです。
そろそろ、こう、どーんと大きな改革があってもいい頃なんじゃないでしょうか。ねぇ。
というわけで、こんな風になったらいいのにな、という妄想を書き連ねてみたのがこの記事です。
正直、何か改革をするなら、Linux 版が出るこのタイミングは絶好なのではないか、と思います。Linux 版や Mac 版のユーザーには最初から新しい仕様を使ってもらえるわけですし。
しかし、今のところ、そうした改革が行われる気配はありません。なので、この記事は完全に妄想です

なお、C# 等の言語を使ってバイナリ モジュールを作る場合を主に想定しております。

Cmdlet クラスからの継承の廃止

バイナリ モジュールのコマンドレットは、Cmdlet クラスから派生していなければなりません。
まぁ、大部分のモジュールは PSCmdlet から派生する方が多いと思われます。

同一モジュールに含まれるコマンド、特に、名詞(コマンド名の Get- とか Set- とかの後の部分)が共通するコマンドでは、パラメーターや実装に共通部分があることが多いものです。
こうした共通部分の実装には、(PowerShell では)クラスの継承が使われるのが常套手段です。
例えば Get-Content と Set-Content がどうなっているかと言うと、なんと継承階層がこんなに深くなっているのです。

一般論として、C# のような多重継承を許さない言語で、機能を追加するために継承というメカニズムを利用するのは良い設計とは言えません。
代わりに、ICmdlet インターフェイス(というのを新たに用意して)を実装するという形がよいでしょう。

ちなみに、Cmdlet と PSCmdlet の違いですが、PSCmdlet は PowerShell ランタイムにアクセスできるが、Cmdlet はそれができない、という点にあります。
たとえば、PSCmdlet にあって Cmdlet にない要素として、以下のようなものが挙げられます。

InvokeProvider プロパティ
これがないと、「現在のパス」が取得できません。
MyInvocation プロパティ
これがないと、「どのパラメーターが実際に渡されたか」がわかりません。
ParameterSetName プロパティ
これがないと、…いや、これは無くてもいいか。

このあたりについては以前書いた記事も見て頂ければと思います。

tech.blog.aerie.jp

このあたりも、ランタイムにアクセスするためのインターフェイスDependency Injection で注入してもらう形が現代的なのではないかと思います。

実のところ、Cmdlet クラスを継承するのは WriteObject 等の出力系メソッドを使うためではないかと思うのですが、これらは Cmdlet クラス自体が実装するものと言うより、CommandRuntime プロパティにセットされている ICommandRuntime に移譲されています。
原始的な DI チックではあります。

非同期操作のサポート

C# でコマンドレットを書く場合、以下のメソッドをオーバーライドします。

これらのいずれも async メソッドではないために、オーバーライドしたメソッドの中で async/await を使った非同期メソッドは利用できません。
また、実際にやってみるとわかるのですが、WriteObject 等の出力系メソッドは、パイプライン スレッド(BeginProcessing/ProcessRecord/EndProcessing が実行されるスレッド)以外から実行するとエラーになります。
そのため、ワーカースレッドからこれらを実行したい場合、実行要求をパイプライン スレッドに持ってきて実行してやらなければなりません。
こうしたことの面倒を見るのが SynchronizationContext クラスなのですが、PowerShell のパイプライン スレッドは SynchronizationContext を持っていません。

このあたりの解決策については、以下のブログ記事などをご覧ください。

d.sunnyone.org

ちなみに本ブログでも最終的にそのあたりまで行こうとして書いていた記事が以下のカテゴリーなのですが、PowerShell 編に入る前に中断してしまっています。

tech.blog.aerie.jp

自分でも何度かコードを書いていますが、あまり実になってはいません。

github.com

Microsoft による実装はさらに気合が入っています。

github.com

まぁ、こういう面倒なことをしなくてもいいように、とっとと標準でサポートしやがれ、ということです。

PSModule クラスの導入

PowerShell バイナリ モジュールは、PSModule クラス(というのを新たに用意するとして)から派生したクラスを一つエクスポートすること、というルールを設けたらどうか、と思います。
このインスタンスにはモジュール内のどこからでもアクセスできるようにしておきます。

バイナリモジュールでは Cmdlet 派生クラスですべてを賄わなければならないため、そのスコープを超えることをやりづらいという悩みがあります。そうしたことを行うのが PSModule クラスです。
例えば、コマンド間で状態を共有したり、保存しておいて次回実行時に読み込んだり。
また、複数のコマンドにまたがる初期化処理(例えば前述の DI の設定など)も、ここでやることになるのではないかと思います。

こうしたことを、各コマンドの BeginProcessing でやるというのは不格好ですし、共通化するためにはまた基底クラスを作って…というのも不格好。
やはり、処理にはしかるべき場所というのがあるものです。

StopProcessing メソッドの廃止

BeginProcessing/ProcessRecord/EndProcessing はパイプライン スレッドから順次呼び出されます。Cmdlet 派生クラスが IDisposable も実装している場合、Dispose もパイプライン スレッドから呼ばれます。
一方 StopProcessing は異なるスレッドから呼ばれます。
そしてなんと、StopProcessing の実行が終了する前に Dispose が呼ばれてしまうのです。

この設計は、端的に言ってバグと呼んでよいのではないかと思っています。
通常、Dispose で排他制御はしません。
なぜなら、オブジェクトがどのスレッドでも使われていない状態で Dispose を呼ぶのが、呼び出し側の責務だと考えられるからです。

また、コマンドの処理が終了する場合というのはいろいろとあります。
いくつか挙げれば

  • 正常に終了した場合
  • 途中で例外が発生した場合
  • Ctrl+C が押された場合
  • パイプラインの上位ないしは下位のコマンドで中断された場合

等があります。
これらのうち、StopProcessing が呼ばれるのは Ctrl+C だけですが、Dispose であればすべての状況で呼ばれます。
加えて StopProcessing の途中で Dispose が実行されてしまう可能性もありますので、StopProcessing でメンバー変数に触るのも安全ではありません。
ついでに言えば、StopProcessing はパイプライン スレッド外で呼ばれますから、WriteXXX 系のメソッドも呼べません。

一体、StopProcessing で何ができるというのでしょう? いっそのこと無くしてしまった方が良いのではないでしょうか。

ちなみに、上記の終了するケースのうち、特に最後の「パイプラインの上位のコマンドで中断された場合」というのは、スクリプト モジュールではどうやっても検知できません。
そのため、コマンドのクリーンアップ処理が必要な場合には、スクリプト モジュールではなくバイナリ モジュールにして Dispose を実装するしかありません。

パラメーターセットの再考

PowerShell コマンドではオーバーロードができない代わりに、パラメーターセットという仕組みがあります。が、これが実に使いづらい。
特に、直交する複数の組み合わせがある場合です。

たとえば、パラメーター A と B のどちらかしか使えないのであれば、

  • A
  • B

というパラメーターセットがあればよいでしょう。

さらに、C と D のどちらか…となると、

  • A と C
  • A と D
  • B と C
  • B と D

という組み合わせが必要になります。

パラメーターが1つ増えるごとに、パラメーターセットは2つ以上増えるわけです。

という不満はやはりあるようで、PowerShell RFC にも提案されていますが、期限切れになってしまっていますね。

github.com

まぁ正直、ここに関しては、どうしたらいいのか代替案はないのですが。

マルチ モジュール パッケージ

…以降、次回!

あとがき

そういえば PowerShell RFC という仕組みで、PowerShell の仕様をコミュニティ ベースで決めていくことになったようですね。
こういう妄想も、提案したら実現するんでしょうか?

…でも英語出来ないんだよなあ。