2015年2月27日金曜日

PowerShell の Add-Type と [Reflection.Assembly]

Add-Type の方が、[reflection.assembly]::LoadWithPartialName() よりいいよ!やったね。と書こうと思ったのです。 はじめは。

大体の場合は、 Add-Type はイイ感じに動作します。が、せっかくAdd-Type を作るときに Microsoft はちょっと、それは。と思うような動作も許してしまっています。 なので、結局一概にはいえないな、と思うのでちょっと記事にしておきます。

[Reflection.Assembly]
Windows は、 .Net を利用することで快適に使えます。ただ.Net は膨大な規模なのですけど、PowerShell のデフォルトでは コアになる部分しか読み込まれていません。

PowerShell で デフォルトで参照されていない.Net を利用したい。わけで、.Netアセンブリをメモリに読み込む機会も多々あります。*1

たとえば、.Net 4.5 で追加された HttpClient を利用しようとすると

Unable to find type [System.Net.Http.HttpClient]. Make sure that the assembly that contains this type is loaded.
At line:1 char:1
+ [System.Net.Http.HttpClient]
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (System.Net.Http.HttpClient:TypeName) [], RuntimeException
+ FullyQualifiedErrorId : TypeNotFound
PowerShell 1.0 では、明示的に参照するために [System.Reflection.Assembly] のStaticメソッドで読み込みをしました。

あ、System は省略可能なので、 [Reflection.Assembly] と書けます。

Load メソッド
まずは Load() メソッドでしょうか。え、使わない?ソウデスネ。Fullname を求めるので使ってられません。そんなの覚えてられるか。書きたくない!

[Reflection.Assembly]::Load("System.Net.Http, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")
LoadFrom メソッド
それなら LoadFrom() って? C# のように参照に追加してあとはさくっと名前で参照とかできないのでツラいですね。

[Reflection.Assembly]::LoadFrom("C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETCore\v4.5.1\System.Net.Http.dll")
LoadWithPartialName() メソッド
で、良く出てきているのがこれです。

[Reflection.Assembly]::LoadWithPartialName("System.Net.Http")
気分的には こっちが見慣れた フルネーム ですし、うまく動いていますね。しかし、Stringなのでタブ補完効かないしめんどくさい。

[System.Net.Http.HttpClient]
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False HttpClient System.Net.Http.HttpMessageInvoker
Add-Type
PowerShell 2.0 で、 PowerShell は改善策を打ってきました。それが、Add-Type Cmdlet です。

たとえば、先ほどの

[Reflection.Assembly]::LoadWithPartialName("System.Net.Http")
は、Add-Type でこう書けます。

Add-Type -AssemblyName System.Net.Http
これなら、タブ補完も効くしいい感じですね!やった!だいたいの場合は。

問題は、Partial Name で判別ができない場合です。

複数バージョンのアセンブリがインストールされている場合が問題

しかし、 Add-Type は、これとは別の内部テーブルを使っているようで、 [Reflection.Assembly]::LoadWithPartialName()とは違う結果がもたらされます。

もし 複数のアセンブリが異なるバージョンでインストールされている場合、 Add-Type -AssembryName は判別する方法を持ちません。で、だいたいの場合は古いバージョンが参照されて、新しいバージョンを参照しているスクリプトは失敗します。

Add-Type の失敗例
例えば SQLServer,SMO は、厄介です。

v10.0.0.0 と v11.0.0.0 などがありますが、Add-Type で最新が参照されるでしょーっと気にせずに追加しようとすると....

Add-Type -AssemblyName Microsoft.SqlServer.SMO
PowerShellエンジンは 古いバージョンを読み込むどころか、アセンブリの読み込みに失敗します。v9.0.242.0 って、やだー。ないよそんなのれがしー。

Add-Type : Could not load file or assembly 'Microsoft.SqlServer.Smo, Version=9.0.242.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91' or one of its dependencies. The system cannot find the file specified.
At line:1 char:1
+ Add-Type -AssemblyName Microsoft.SqlServer.SMO
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Add-Type], FileNotFoundException
+ FullyQualifiedErrorId : System.IO.FileNotFoundException,Microsoft.PowerShell.Commands.AddTypeCommand
Add-Type ではフルネームが必要になる場合がある
この場合、バージョンを的確に指定する、つまりフルネームならいけます。

例えば私の環境*2の場合は、v11.0.0.0 を指定すればokです。

Add-Type -AssemblyName "Microsoft.SqlServer.SMO, Version=11.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91"
しかし、もう一回言いますがフルネームはない。

[Reflection.Assembly]::LoadWithPartialName() なら大丈夫

Microsoft.SqlServer.Smo だけで読み込んで欲しいじゃないですか。で、これならok。

[Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.Smo")
はい、ok。

GAC Version Location
--- ------- --------
True v2.0.50727 C:\Windows\assembly\GAC_MSIL\Microsoft.SqlServer.SMO\11.0.0.0__89845dcd8080cc91\Microsoft.SqlServer.SMO.dll
ただし、この場合は どのバージョンが読み込まれるかは保証できません、が、とりあえず読んでくれる。やった。Add-Typeどの〜。

PowerShell の Type Conversion
この問題には、 PowerShell の型変換システムが絡んでいます。

[Reflection.Assembly]::LoadWithPartialName() メソッドは裏でうまくアセンブリを判別し、またPowerShellの型変換アルゴリズムを使ってくれています。素晴らしい。*3

PowerShell の10ある型変換アルゴリズムはここを見てください。

Understanding PowerShell's Type Conversion Magic
Direct assignment. If your input is directly assignable, simply cast your input to that type.
Language-based conversion. These language-based conversions are done when the target type is void, Boolean, String, Array, Hashtable, PSReference (i.e.: [ref]), XmlDocument (i.e.: [xml]). Delegate (to support ScriptBlock to Delegate conversions), and Enum.
Parse conversion. If the target type defines a Parse() method that takes that input, use that.
Static Create conversion. If the target type defines a static ::Create() method that takes that input, use that.
Constructor conversion. If the target type defines a constructor that takes your input, use that.
Cast conversion. If the target type defines a implicit or explicit cast operator from the source type, use that. If the source type defines an implicit or explicit cast operator to the target type, use that.
IConvertible conversion. If the source type defines an IConvertible implementation that knows how to convert to the target type, use that.
IDictionary conversion. If the source type is an IDictionary (i.e.: Hashtable), try to create an instance of the destination type using its default constructor, and then use the names and values in the IDictionary to set properties on the source object.
PSObject property conversion. If the source type is a PSObject, try to create an instance of the destination type using its default constructor, and then use the property names and values in the PSObject to set properties on the source object. . If a name maps to a method instead of a property, invoke that method with the value as its argument.
TypeConverter conversion. If there is a registered TypeConverter or PSTypeConverter that can handle the conversion, do that. You can register a TypeConverter through a types.ps1xml file (see: $pshome\Types.ps1xml), or through Update-TypeData.
しかし、Add-Type は この型変換を使わず、内部に変換テーブルを持っているようです。で、失敗すると。

しかも、Add-Type があるためか、 [Reflection.Assembly]::LoadWithPartialName() は、deprecated 予定とか PowerShell Team も言ってますし、ほげー。*4

結論
もし、 Add-Type でアセンブリが読める + 読まれたバージョンで問題がないなら

Add-Type -AssemblyName System.Net.Http
もし、Add-Type の内部テーブルにアセンブリがなく読み込めない、バージョンがわからないけどとりあえずどれが読まれてもいいなら、

[Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.Smo")
もし、アセンブリのバージョンを指定したい、または将来 上の2つが使えなくなった場合は、

Add-Type -AssemblyName "Microsoft.SqlServer.SMO, Version=11.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91"
がいいでしょう。

まとめ
Add-Type かわいいよ。もっと賢くなるともっと好き。

*1:PowerShell 3.0 以上で明示的に読む必要がいくつかでなくなっており便利になっています。

*2:Windows 8.1 Pro / Visual Studio 2013

*3:他のCmdletでもPowerShellの型変換はこれに従います

*4:Assembly.LoadWithPartialName メソッド (String)

0 件のコメント:

コメントを投稿