2010-06-05 [プログラミング] IronRuby-WMI (1) 
cuzic です。
JavaScript の柔軟な使い方の勉強として、WMI (Windows Management Instrument)のラッパーを、Windows Script Host で実行する環境向けに作ってみようと思ってみました。
PowerShell と似たノリで、
var klass = get_wmiobject("Win32_Process");
var instances = klass.GetInstances();
forEach(instances, function(instance){ // forEach の実装は省略
WScript.Echo(instance.Caption); // 実行中のプロセス名称を表示
});
というようなインタフェースで使えるようにできたらいいな、と思ったのだが、いろいろ調べてみるとなかなか大変であることが分かった。
例を使って説明すると、
var klass = get_wmiobject("StdRegProv");
HKEY_LOCAL_MACHINE = 0x80000002;
var arrValueNames, arrValueTypes = klass.EnumValues(HKEY_LOCAL_MACHINE, "SOFTWARE\ODBC\ODBCINST.INI\ODBC Drivers");
のように、複数の返り値(ActiveX のメソッドでは out パラメータ)がある場合は、このように自然に書きたかったが、この仕様を満たすような実装は JavaScript では実現できない。
実現できない理由は、
- instance.Caption のようなアクセスを許容するには、WMI オブジェクトを生で扱うよりない(JavaScript には C# でいうプロパティの実装手段がないため)
- 一方、この場合の EnumValues を実現するには ActiveX を生で呼び出すのではなく、JavaScript のクラスなどでラップする必要がある
- そもそも、JavaScript には多重代入がない。
からだ。
これらは、Ruby であれば実現できる。というわけで、ふと習作として Ruby で実現したら、どうなるかを考えて、JavaScript 版をどうするかを考え直してみたいと思った。
単純に、Ruby で実装してみても仕方がないので、最近話題になっている IronRuby で書いてみた。
http://github.com/cuzic/ironruby-wmi
require 'System';
require 'System.Management, Version=2.0.0.0,
Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'
class String
def underscore
scan(/[A-Z][a-z]+/).to_a.map(&:downcase).join("_")
end
end
module WMI
class WMIError <StandardError
end
class InvalidQuery <WMIError
end
class Base
def initialize obj
@obj = obj
end
def wmi_delegate_obj
@obj
end
def self.wmi_delegate_obj
wmi_class
end
def self.wmi_class
@wmi_class ||= System::Management::ManagementClass.new(class_name)
end
def self.class_name
@class_name ||= self.name.split("::").last
end
def self.get_instances
wmi_class.get_instances.map do |obj|
self.new(obj)
end
end
def self.get_instances_async &block
searcher = System::Management::ManagementObjectSearcher.new(
System::Management::SelectQuery.new(class_name));
results = System::Management::ManagementOperationObserver.new
m = Module.new
m.module_eval do
define_method :call, block
end
klass = self
results.ObjectReady do |observer, event_args|
instance = klass.new(event_args.NewObject)
instance.extend m
instance.call instance
end
completed = false
results.Completed do |observer, event_args|
completed = true
end
at_exit do
sleep 1 until completed
end
searcher.Get(results)
end
def check_return_value return_value
self.class.check_return_value return_value
end
def self.check_return_value return_value
case return_value
when 0
# do nothing
when 2
raise WMI::WMIError.new("Access Denied");
when 3
raise WMI::WMIError.new("Insufficient Privilege");
when 8
raise WMI::WMIError.new("Unknown failure");
when 9
raise WMI::WMIError.new("Path Not Found");
when 21
raise WMI::WMIError.new("Invalid Parameter");
else
raise WMI::WMIError.new("ReturnValue == " + return_value)
end
end
def invoke_method_1 method_name, *args
return_value = @obj.InvokeMethod method_name, args
self.class.check_return_value return_value
end
def method_missing name, *args
@obj.__send__ name, *args
end
end
def self.const_missing name
klass = Class.new(self::Base)
self.const_set(name, klass)
klass.class_eval do
wmi_class.Properties.each do |prop|
prop_name = prop.Name
if prop.IsArray
define_method prop_name do
@obj.Properties[prop_name].Value.to_a
end
else
define_method prop_name do
@obj.Properties[prop_name].Value
end
end
if prop_name != prop_name.underscore then
alias_method prop_name.underscore.to_sym, prop_name.to_sym
end
end
end
klass.wmi_class.Methods.each do |m|
method_name = m.Name.to_s
param_count = out_count = m.OutParameters.Properties.Count - 1
if m.InParameters
param_count += m.InParameters.Properties.Count
end
mm = %(
def #{method_name} *args
(#{param_count} - args.size).times do
args.push nil
end
array = args.ToArray
return_value = wmi_delegate_obj.InvokeMethod "#{method_name}", array
check_return_value return_value
if #{out_count} == 0 then
return nil
else
return *array.to_a[-#{out_count} .. -1]
end
end
)
klass.instance_eval mm
klass.class_eval mm
if method_name != method_name.underscore then
mmm = %{
class <<self
alias :#{method_name.underscore} :#{method_name}
end
}
klass.instance_eval mmm
klass.class_eval mmm
end
end
code = %(
def method_missing method_name, *args
if method_name == :InvokeMethod then
wmi_method = args.shift.ToString
self.__send__ wmi_method, *args
else
result = wmi_delegate_obj.__send__ method_name, *args
if result.is_a? (System::Management::ManagementObjectCollection) then
return result.map do |obj|
klass.new obj
end
end
return result
end
end
)
klass.instance_eval code
klass.class_eval code
klass
end
end
IronRuby で Ruby の柔軟性をどこまで引き出せるかのテストみたいな状態になってしまっているが、
- const_missing を活用することで
WMI::Win32_Process.get_instances
などのように即座に WMI クラスを利用可能
- define_method も駆使して動的なメソッド定義を行い、WMI のプロパティを自然にアクセスできるように工夫
- get_instances_async で非同期の WMI オブジェクトの取得の実現も可能に
- get_instances_async はすべてのインスタンス取得が完了するまで、スクリプトを終了しないように at_exit で sleep する。
といった点を工夫したつもりである。
このライブラリを使うことで、
WMI::Win32_Process.get_instances_async do |process| puts caption end
のような簡潔なコードで、現在実行中のプロセスの名称を非同期に取得できるようになる。
最初の目標から大きく外れ、IronRuby で Ruby の柔軟な機構をごりごりと使う実験みたいな例になってしまったが、define_method, instance_eval などを含むコードでもちゃんと動いてもらえて、非常に満足である。
なんとなく作ってしまったので、公開してみたが、今後の展開としては、
- IronRuby ベースで PowerShell のラッパーとしても使えるライブラリの作成(この路線を発展)
- IronRuby から、CRuby で動作する RubyCLR ベースに移植
- IronRuby ではなく、JScript.NET に移植。(eval の連打での実装???)
- 最初、本当に作りたかった WMI の JavaScript ラッパーの作成。
といった選択肢のどれかを進めていこうと思っている。
やってみて、一息ついてみると、JavaScript (JScript) で実現する場合での限界などについても理解が深まった。私がやりたいことは JScript.NET でプロパティの部分については実現可能そうだ。高速さは犠牲になるが、JScript.NET で動的なメソッド定義も実現可能だ。JScript.NET コンパイラは、.NET フレームワークがインストールされている環境であればインストールされているようなので、普通の XP パソコンであれば使えて、どこでも動くといいやすい。PowerShell についても JScript.NET であれば、自然に扱えそうである。そのため、JScript.NET への移植などを進めていくかもしれない。(実際、今後どうするかは私の気分で決まるわけだが)