PowerShellでAssemblyのLoad/Unloadを簡単に

Add-TypeでAssemblyをロードしちゃうと、Unloadできないじゃないですか。
あの仕様って、どんな嫌がらせなんですかね?
じゃあってんで、PowerShellプロセスを使い捨てにするのが一般的だと思うけど、そうすると一部は残したい、という場合にまたAdd-Typeしなきゃいけない。
解決方法を考えてみた。


Unloadできるのが一番ですが、、、手があるか調べきれなかったので、History泥棒をすることにしました。
しかも、コマンド実行毎に泥棒する方法がわからず、タイマーで泥臭くやってたりします。
名前付きパイプによるプロセス間通信のサンプルでもあります。



スクリプト

Start-Timer/Start-Taskに依存しています。
Powershellで非同期実行 - じゅんじゅんのきまぐれ

function Open-SandBox() {
	$baseName = 'SandBox'
	$exitCommand = "Close-$baseName"
	$pipeName = "Junjun$baseName"
	Write-Verbose "$baseName Init"
	$base = @"
		`$$pipeName = New-Object PSObject -Property @{
			pipe = New-Object System.IO.Pipes.NamedPipeClientStream '.', '$pipeName', InOut
			id = 0
			timer = `$null
			keep = `$null
		}
		`$$pipeName.pipe.Connect()
		`$$pipeName | Add-Member -MemberType ScriptMethod 'Write' {
			Param(`$msg)
			`$wb = [System.Text.Encoding]::UTF8.GetBytes(`$msg)
			`$wbl = [BitConverter]::GetBytes(`$wb.Length)
			`$$pipeName.pipe.Write(`$wbl, 0, `$wbl.Length)
			`$$pipeName.pipe.Write(`$wb, 0, `$wb.Length)
		}
		`$$pipeName | Add-Member -MemberType ScriptMethod 'SendKeepHistory' {
			if(`$$pipeName.keep -ne `$null) {
				`$$pipeName.keep | %{ `$$pipeName.Write(`$_) }
				`$$pipeName.keep = `$null
			}
		}
		`$$pipeName | Add-Member -MemberType ScriptMethod 'SendHistory' {
			`$h = Get-History -Count 1
			if(`$h.Id -eq `$$pipeName.id + 1) {
				`$$pipeName.Write(`$h.CommandLine)
				`$$pipeName.id = `$h.Id
				`$$pipeName.SendKeepHistory()
			} elseif(`$h.Id -gt `$$pipeName.id) {
				Get-History -Count (`$h.Id - `$$pipeName.id) | %{
					if(`$_.Id -gt `$$pipeName.id) {
						`$$pipeName.Write(`$_.CommandLine)
						`$$pipeName.id = `$_.Id
						`$$pipeName.SendKeepHistory()
					}
				}
			}
		}
		Register-EngineEvent ([System.Management.Automation.PsEngineEvent]::Exiting) -Action {
			Remove-Timer `$$pipeName.timer.Name
			`$$pipeName.SendHistory()
			`$$pipeName.SendKeepHistory()
			`$$pipeName.Write('exit')
			`$$pipeName.pipe.Close()
		} | Out-Null
		`$$pipeName.timer = Start-Timer { `$$pipeName.SendHistory() } 5000
		function $exitCommand() {
			exit
		}
		Write-Host $baseName 'Start'
"@

	while($res.result -eq $null -or $res.result.Count -eq 0 -or $res.result[$res.result.Count - 1] -ne $exitCommand) {
		if($res.result -ne $null) {
			if($res.result.Count -gt 0) { $res.result[0] = '' }
			for($i=1; $i -lt $res.result.Count; $i++) { $res.result[$i] = $res.result[$i] -replace '''', '''''' }
			if($res.result.Count -gt 1) {
				$run = $base + "`n`$$pipeName.id=''`n@(" + ($res.result -join ''',''').Substring(2) + ''')' + @"
					| %{
						Write-Host `$_
						if(!`$$pipeName.id.StartsWith('a')) {
							`$$pipeName.id = (Read-Host 'run?(Y/n/abort/all)')
						}
						if(`$$pipeName.id -eq 'all' -or (`$$pipeName.id -ne 'n' -and `$$pipeName.id -ne 'abort')) {
							iex `$_
							if(`$$pipeName.keep -eq `$null) { `$$pipeName.keep = @() }
							`$$pipeName.keep += `$_
						}
					}
					`$$pipeName.id = 0
"@
			}
		}
		if($run -eq $null) { $run = $base }

		$res = Start-Task {
			$pipe = New-Object System.IO.Pipes.NamedPipeServerStream $args[1], InOut
			$pipe.WaitForConnection()
			$lenb = New-Object byte[] 4
			$loop = $true
			while($loop) {
				$len = $pipe.Read($lenb, 0, $lenb.Length)
				$len = [BitConverter]::ToInt32($lenb, 0)
				$buf = New-Object byte[] $len
				$len = $pipe.Read($buf, 0, $buf.Length)
				$cmd = [System.Text.Encoding]::UTF8.GetString($buf, 0, $len)
				if($cmd -match '^exit') {
					$loop = $false
				} else {
					$cmd
				}
			}
			$pipe.Close()
		} -Silent -MessageData $pipeName

		PowerShell -NoLogo -NoExit -EncodedCommand $([Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($run)))
	}

	Write-Host $baseName 'End'
}

Open-SandBoxすると、子PowerShellを開きます。(同じウインドウ)
そこで、Add-Typeやら何やらする。
環境刷新したくなったら「exit」すると、一旦親に戻るものの、再度子PowerShellが起動します。
この時、前の子PowerShellHistory(もどき)を渡されるので、取捨できます。
無限ループです。
終わりたい場合は、子PowerShellで「Close-SandBox」してください。

解説他

PowerShellは随時Historyを親に送ります。
新しい子PowerShellは、もらったHistoryのそれぞれの実行確認をします。
(「Y/n/all/abort」は「n」で実行しない、「all」で以降実行、「abort」で以降停止、他は実行、です。だいたい)
PowerShellで、Close-SandBoxすると、SandBoxモードが終わります。


コマンド実行毎にHistoryを盗みたかったのですが、方法不明のため、5秒タイマーで盗んでます。
5秒以内に、$MaximumHistoryCountを超える入力があると、ロストします。
(そういう想定の場合は、タイマー間隔を短くするか$MaximumHistoryCountを増やしてください)
子から親へは名前付きパイプで転送しています。
.Net remotingのIPCチャネルは結局名前付きパイプらしいので。
Exitのイベント捕捉は、stuncloudさんのブログで知りました!(ありがとうございます!)


HistoryInfo、シリアライズできないなんて聞いてないよー。
デフォルトコンストラクターがないのです。
Export-Clixml&Import-Clixmlでファイル経由の方がスマートかつ便利なんですけど、、、。
ファイル経由を回避するにはこの方法でした、、、。
みんな、メモリーディスクを確保すると良いよ!