この記事は「PowerShell Advent Calendar 2018」の参加記事です。
ちなみに投稿が遅れたのではなく 16 日が埋まっていなかったので穴埋めしています。ここ重要!
Write-Verbose について
PowerShell では Verbose パラメーターを付けることでトレース情報を出力させることができます。モジュールの内部では Write-Verbose を呼び出すことで出力内容を制御します。Script Module の場合、PowerShell のスクリプトからコマンドレットを呼び出すだけなので、特に何も考える必要はないのですが、Binary Module の場合は勝手が違います。なぜなら、Cmdlet.WriteVerbose メソッドは static ではないからです。単純なコマンドレットであれば、ProcessRecord メソッドで全部書けば済むかもしれません。ある程度の規模になってくるとそうもいきません。かといって Cmdlet のインスタンスを持ち回すのも美しいとは思えません。
Trace.WriteLine があるじゃないか
C# には Trace.WriteLine メソッドがあります。こいつを使えばいけるのではと思いましたが、標準では Trace.WriteLine をホストに流し込んでくれるような仕組みにはなっていないようです。*1 仕方がないので TraceLister を自作します。
CmdletTraceListener.cs
public class CmdletTraceListener : TraceListener { private readonly Cmdlet cmdlet; public CmdletTraceListener(Cmdlet cmdlet) { if (cmdlet == null) { throw new ArgumentNullException(nameof(cmdlet)); } this.cmdlet = cmdlet; } public override void Write(string message) { this.cmdlet.WriteVerbose(message); } public override void WriteLine(string message) { this.cmdlet.WriteVerbose(message); } }
TraceableCmdlet.cs
public abstract class TraceableCmdlet : PSCmdlet { private readonly TraceListener traceListener; protected TraceableCmdlet() { this.traceListener = new CmdletTraceListener(this); } protected override void BeginProcessing() { Trace.Listeners.Add(this.traceListener); base.BeginProcessing(); } protected override void EndProcessing() { base.EndProcessing(); Trace.Listeners.Remove(this.traceListener); } }
こうしてあげることで、どこからでも Trace.WriteLine を呼び出すだけで (Verbose パラメーターを指定していれば) ホストにメッセージが出力されますし、疎結合になるのでテストも容易になるのではないかと思います。
(2018/12/20 追記)
Twitter でご指摘いただきました。ありがとうございます。
twitter.comCmdlet の Write 系メソッドは、Cmdlet の寿命の間、かつ、Cmdlet のメインスレッドからしかアクセスできないので、グローバルなところへ持ち出すと、そうした制限を無視して呼び出されてしまって例外が起きないかな…という不安があります。具体的にここが問題とは指摘できませんが。
— コロッケ加速器 (@aetos382) 2018年12月19日
確かに例外が発生すると EndProcessing が呼ばれないので、TraceListener が解除されずにメモリ リークになってしまっているようです。IDisposable を実装してあげるのがよいようですね。
(2019/04/19 追記)
パイプライン使ったときに駄目みたいですねorz
*1:Console.WriteLine は Write-Host と同様に動作します。