PowerShell と ZipPackage

本記事を試した環境。

Windows
Windows 7 x64/Windows Server 2008 x64/Windows XP x86
PowerShell
PowerShell 2.0
.NET Framework
3.5 SP1以降がインストールされている
Visual Studio
Visual Studio 2005

PowerShell の対話型インターフェイスの AppDomain は情報が不完全なようで*1、そのために一部処理が失敗することがある。その一つが ZipPackage 関連クラスのストリーム処理。下に成功するサンプルと失敗するサンプルを書く。

まずは成功するもの。なお、OPC準拠のファイルを作るためにある ZipPackage を、下のように単なるzip書庫を作るために使うのは NG だと思うが、単純なサンプルということで、目をつぶって欲しい。

[VOID][System.Reflection.Assembly]::Load("WindowsBase, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");

$s = "実験用文字列";

$package = [System.IO.Packaging.ZipPackage]::Open("test01.zip", [System.IO.FileMode]::Create, [System.IO.FileAccess]::ReadWrite);

$packagePart = $package.CreatePart("/hoge.txt", [System.Net.Mime.MediaTypeNames+Text]::Plain);
$stream = $packagePart.GetStream();
$writer = new-object System.IO.StreamWriter($stream, [System.Text.Encoding]::Unicode);
$writer.WriteLine($s);
$writer.Close();
$stream.Close();

$package.Flush();
$package.Close();

下は失敗する。サンプルとしては、成功したら上よりもかなり大きなファイルが作られるもの。成功する環境もあるかもしれないけど、ZipPackagePart オブジェクト($packagePart)のストリームに流し込むデータを増やせば失敗すると思う。

[VOID][System.Reflection.Assembly]::Load("WindowsBase, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");

$s = "実験用文字列";

$package = [System.IO.Packaging.ZipPackage]::Open("test02.zip", [System.IO.FileMode]::Create, [System.IO.FileAccess]::ReadWrite);

$packagePart = $package.CreatePart("/hoge.txt", [System.Net.Mime.MediaTypeNames+Text]::Plain);
$stream = $packagePart.GetStream();
$writer = new-object System.IO.StreamWriter($stream, [System.Text.Encoding]::Unicode);
for($i = 0; $i -lt 1000000; $i++) {
	try {
		$writer.WriteLine($s);
	} catch {
		# $_                          : System.Management.Automation.ErrorRecord
		# $_.Exception                : System.Management.Automation.MethodInvocationException
		# $_.Exception.InnerException : System.IO.IsolatedStorage.IsolatedStorageException
		write-error ("{0}, {1}, {2}" -f $_.Exception.GetType().ToString(), $_.Exception.InnerException.GetType().ToString(), $_.Exception.InnerException.ToString());
		break;
	}
}
$writer.Close();
$stream.Close();

$package.Flush();
$package.Close();

IsolatedStorageException なんてのが投げられて失敗する。分離ストレージを内部で使っているということなんだろうけど、詳細はわからない。
とりあえず、AppDomainの情報がそろっていれば上のスクリプトは成功する。
たとえば、C#なんかで、System.Management.Automation.RunspaceInvoke.Invoke() を使って、スクリプトを実行するコマンドとか作る。コマンドの AppDomain には ZipPackage が必要とする分離ストレージの操作において必要な情報がそろっていて、スクリプトの実行に成功する。非常にベタなサンプルを下に書く。

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using System.Management.Automation;

namespace TestInvoke
{
    class Program
    {
        static void Main(string[] args)
        {
            string script = @"
[VOID][System.Reflection.Assembly]::Load(""WindowsBase, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"");

$s = ""実験用文字列"";

$package = [System.IO.Packaging.ZipPackage]::Open(""test02.zip"", [System.IO.FileMode]::Create, [System.IO.FileAccess]::ReadWrite);

$packagePart = $package.CreatePart(""/hoge.txt"", [System.Net.Mime.MediaTypeNames+Text]::Plain);
$stream = $packagePart.GetStream();
$writer = new-object System.IO.StreamWriter($stream, [System.Text.Encoding]::Unicode);
for($i = 0; $i -lt 1000000; $i++) {
	try {
		$writer.WriteLine($s);
	} catch {
		# $_                          : System.Management.Automation.ErrorRecord
		# $_.Exception                : System.Management.Automation.MethodInvocationException
		# $_.Exception.InnerException : System.IO.IsolatedStorage.IsolatedStorageException
		write-error (""{0}, {1}, {2}"" -f $_.Exception.GetType().ToString(), $_.Exception.InnerException.GetType().ToString(), $_.Exception.InnerException.ToString());
		break;
	}
}
$writer.Close();
$stream.Close();

$package.Flush();
$package.Close();
";

            //script = "write 1; write-error 2; write 3; write-error 4;"; // RunspaceInvoke.Invoke() の返値がどういうものになるかを簡単に確認する。

            RunspaceInvoke invoke = new RunspaceInvoke();
            System.Collections.IList errors;
            Collection results = invoke.Invoke(script, null, out errors);
            Console.WriteLine(">Results");
            foreach (PSObject r in results)
            {
                Console.WriteLine(r);
            }
            // スクリプトが write-error に書き込んだオブジェクトを出力する。
            Console.WriteLine(">Errors");
            foreach (PSObject e in errors)
            {
                Console.Error.WriteLine(e);
            }
        }
    }
}

なお、RunspaceInvoke.Invoke() メソッドでスクリプトを実行する場合、プロファイル("Microsoft.PowerShell_profile.ps1"とか)を事前に読み込まない。プロファイルのなかで定義している関数やエイリアスを利用する場合は、実行するスクリプト内でプロファイルを読み込む処理を書いておく必要がある。

参考にさせてもらったページ

//ufcpp.net/study/powershell/interop.html" title="C# 上で PowerShell スクリプトを実行 (Windows PowerShell)" target="_blank">[C# 上で PowerShell スクリプトを実行 (Windows PowerShell)]:Visual Studio 2008 でのサンプルを紹介している。LINQなど2008のC# 3.0じゃないとダメなものを使っているサンプルを除けば(var なんかは単純なものなら object に変えてしまえる)、修正すれば2005でもだいたい通るのじゃないだろうか? 自分に必要なものだけ試したから、どれぐらい2005で使えるかはわからないけど。
//csharper.blog57.fc2.com/blog-entry-55.html" title="C#と諸々 コマンドレットの作成方法" target="_blank">[C#と諸々 コマンドレットの作成方法]:"System.Management.Automation.dll"をVisual Studioから参照する方法。

*1:AppDomain の詳細をあまりわかっていないため、煮え切らない書き方をしている箇所がたくさんあるかと思う。