IronRuby-PowerShell (1)

cuzic です。

ちょっと取り組んでみたら意外とあっさりとできたので、IronRuby による PowerShell のコマンドレットを操作できるようにするライブラリについて、書きます。

もうちょっと、頭の中を整理したいというか、練りたいので、 GitHub 等に公開は特にまだしていません。

たとえば、下記のスクリプトが動作します。

  [1, 2, 3, 4].powershell <

これは、PowerShell

 $square = {foreach($x in $input) { $x * $x }
 1, 2, 3, 4 | &$square

と同じ効果を持ちます。

iAsyncResult はあえて内部的に非同期的な呼び出しを使っているので必要になるおまじないです。内部での呼び出しを同期的にすれば解消するのですが、インタフェース的にどうするのかについて、考えているところです。

ほかに、WMI で Win32_Process を使って実現したような現在実行中のプロセスの一覧を出力する処理は、

  powershell("Get-Process") do |iAsyncResult, output|
    output.each do |i|
      puts i
    end
  end

のように簡潔に書けます。

このライブラリを使えば、変数名の最初の $ や 関数呼び出しのときの & など目障りな記号を使わずに手馴れた Ruby スクリプトで、PowerShell に用意されているコマンドレットを数多く利用することができます。

現在の仕様としては、

  • ブロック呼び出しで呼ばれた場市歩は、非同期処理、ブロックがあれば同期処理
  • Object クラスを拡張して、powershell メソッドを実装
  • System.Management.Automation.dll の位置はわりと決め打ち

というようなかんじになっています。

PowerShell の呼び出しに関する部分のコードについては、わずか50行ほどの記述で完結しておりますので、理解することは簡単でしょう。

このコードをベースに JScript.Net に移植することも考えたのですが、JScript.NetGenerics がサポートされていないとのことで、PowerShell との連携は不可能そうです。
# MSDN のあちこちで、「JScript では、ジェネリックな型およびメソッドは使用できません。」との記述が・・・。

smafile = ENV["PROGRAMFILES"] + %q(\Reference Assemblies\Microsoft\WindowsPowerShell\v1.0\System.Management.Automation)
require smafile

SMA = System::Management::Automation

class Object
  def powershell source
    powershell = SMA::PowerShell.create
    if block_given? then
    # ブロック引数があれば同期処理
      input = nil
      case self
      when Array
        # IronRuby では Generics を of メソッドで実現
        input = SMA::PSDataCollection.of(self.first.class).new
        each do |elem|
          input.Add elem
        end
        input.Complete
      else
        input = SMA::PSDataCollection.of(self.class).new
        # トップレベルメソッドとして呼ばれた場合
        unless self.inspect == "main" && self.class == Object then
          input.Add self
        end
        input.Complete
      end
      output = SMA::PSDataCollection.of(System::Object).new
      completed = false
      asyncCallback = proc do |iAsyncResult|
        completed = true if iAsyncResult.IsCompleted
        yield iAsyncResult, output
        powershell.Dispose if completed
      end
      at_exit do
        sleep 1 until completed
      end
      powershell.AddScript source
      powershell.BeginInvoke input, output,
        SMA::PSInvocationSettings.new,
        asyncCallback, nil
    else
    # ブロック引数がなければ同期処理
      begin
        powershell.add_script source
        if self.is_a? Array then
          return powershell.invoke(self)
        else
          return powershell.invoke([self])
        end
      ensure
        powershell.Dispose
      end
    end
  end
end

def test_gps
  powershell("Get-Process") do |iAsyncResult, output|
    output.each do |i|
      puts i
    end
  end
end

def test_gps_html
  powershell("Get-Process | ConvertTo-Html") do |iAsyncResult, output|
    output.each do |i|
      puts i
    end
  end
end

def test_square
  [1, 2, 3, 4].powershell <