外部アプリケーションから呼び出されたPowerShellスクリプトがモジュールインポートに失敗する

これはみんなにとっては自明のことかもしれないけど、僕にとっては昨日3時間苦闘した出来事だ。64bit OS環境でも、32bitアプリケーションから`powershell.exe`を呼び出すと32bitで実行される。モジュール検索パス`$env:PSModulePath`もそれに合わせて書き変わるから、手作業で配置したモジュールを見つけてくれないということも起きる。

# PowerShellスクリプトを書いた。動作もした。

先日、僕はPowerShellと[VMware.PowerCLI](https://code.vmware.com/web/dp/tool/vmware-powercli/)モジュールを使って、vSphere環境を停止させるスクリプトを作った。実際には仮想マシンをゲストシャットダウン→失敗時はパワーオフ→失敗時は仮想マシンのプロセスkillする（そうしてでも仮想マシンを止めないとSANを解放できなくてESXiがシャットダウン途中で固まりかねない）とか、ESXiをメンテナンスモード移行を待ってからシャットダウンするけど指定時間内に移行しなかった時はシャットダウンを強行するとか、僕が適度だと思うソフトさとハードさを備え調整用のスイッチやオプションを持たせたコマンドレットを書いてモジュール化した。

コマンドレットにはそのハードさを要件に合わせて調整できるスイッチやオプションを持たせ、今回なにより重要な点として、`-WindowsEventLog`というログをWindowsのイベントログに出力するスイッチまで付けた。こんなWindowsでしか動かなくなるような動作要件を与えられてなかったら、コマンドレットやモジュール化しないでベタ書きのスクリプトにしてただろう。この実装は再利用性の低さに気持ち悪さを覚える僕としての落とし所だった。そうしてから、今回のリクエストに合わせたオプション指定で自作コマンドレットを順に呼び出していくだけの簡単なスクリプトを書いた。

スクリプト（とモジュール）は僕の想定通りに動いた。`$WhatIfPreference`を`$true`にすれば動作のシミュレーションができたし、指定しなければ全部を綺麗に止めてみせた。イベントログの出力にも満足がいった。僕はスクリプトの実行方法などを利用者になる運用管理ソフトの担当者に連絡し、この仕事を終えたつもりになった。もちろん、実際にはそうはいかなかったから、こんなメモを書いている。

# 他アプリケーションから呼出すと動作しなかった。

今日、運用管理ソフトの担当者から動作時間をしたいと言われ、立ち会った。見事にスクリプトが動作しない。最初は次のような呼び出し方を伝えていた。これはコマンドプロンプト（PowerShellではなくcmdの方）で実行できることを確認してあった。

    powershell -File "c:\Program Files\(略)\MyShutdown.ps1"

担当者曰く、この運用管理ソフトでは`-File`ではなく`-Command`オプションで呼び出したことしかないとのことだった。`-Command`オプションに置き換えてみると、`Program`と`Files`の間の空白位置で分けて個別の引数として処理しようとしてるようだ。分かる気はするし適切なエスケープ方法があるのだろうけど、試験時間が限られていたのでスクリプトを移動することにした。こうなった。

    powershell -Command "c:\MyTools\MyShutdown.ps1"

これを運用管理ソフトのスクリプト呼出設定に指定した。しかし相変わらず動かない。イベントログにもなにも記録されない。生憎なことに、この運用管理ソフトでは呼出の成否も、exitコードも知ることができないらしい。ましてや標準入出力や標準エラー出力が記録されることもない。

# モジュールを読み込めてなかった...なぜ？

まずスクリプト先頭に、スクリプト実行開始をイベントログに記録する処理を入れた。これでスクリプトが呼び出されてることはわかった。でも自作したコマンドレットの一つ目、vCenterへの接続開始を示すイベントログが記録されない。その間にあるのは、`Import-Module`によるVMware.PowerCLIと自作モジュールの読込みだけだ。そんなものが失敗するなんてあるだろうか？

まさかと思ったけど、でも確認する必要はある。いろいろ悩んだけど、最終的な確認方法は、運用管理システムへのスクリプト呼出設定を以下に変更することだった。

    powershell -Command "c:\MyTools\MyShutdown.ps1" > "c:\MyTools\MyShutdown.log" 2>&1

これで標準エラー出力は標準出力にリダイレクトされ、標準出力は指定した.logファイルにリダイレクトされるから、全てが記録される。実行してからこのログファイルを見てみると、モジュールが見つからないというエラーで、`Import-Module`が失敗している。ありえないと思ったけど、そうだったのだ。

それにしても、なぜだろう？よくある他アプリケーションからの呼出でのトラブルは、こんなところだ。

* 実行ユーザーが違う
* 実行ユーザーによって権限が違う
* 実行ユーザーによってホームディレクトリが違う
* 実行ユーザーによってその他の環境変数が違う

ユーザーやホームディレクトリが違うことは想定して、モジュールは全ユーザー用にモジュールをインストールする時の配置先ディレクトリ、つまり`c:\Program Files\WindowsPowerShell\Modules`に配置した。そういうディレクトリだから、権限問題のはずはない。環境変数の問題のはずもない。なにせ全ユーザー用のモジュール配置先なんだから。

# モジュール検索パスが変わっていた。

でも今だって「起こるはずがない」と思ってた自体に悩まされている。原因も「そんなはずはない」と思っていたところにあるかもしれない。そこでスクリプト先頭に`$env:PSModulePath`とだけ書いた一行を追加した。これはモジュール検索対象になるパスが設定された環境変数だ。そしてスクリプトを実行させ、ログファイルを覗いてみた。

`$env:PSModulePath`に`c:\Program Files\WindowsPowerShell\Modules`は含まれていなかった。代わりに`c:\Program Files (x86)\WindowsPowerShell\Modules`があった。それは32bitアプリケーションのためのパスではないのか？この実行環境はWindows Sever 2012、64bitのOS環境だ。そしてPowerShellのプロンプトから実行した時は`c:\Program Files\WindowsPowerShell\Modules`以下を見に行っていた。コマンドプロンプトからpowershellコマンドで実行した時もそうだ。PowerShellも64bitで動くはずじゃないのか？

訳がわからなかったけど、僕はVMware.PowerCLIと自作モジュールを`c:\Program Files (x86)\WindowsPowerShell\Modules`に複製した。実際のところ、スクリプトはそれで動作した。`$WhatIfPreference`を`$true`にすれば動作のシミュレーションができたし、指定しなければ全部を綺麗に止めてみせた。想定通りだ…動作に限って言えば。

# まとめというか、幕引き

多分、こういうことだったのだろう。

* `powershell`コマンドを呼び出した運用管理ソフトは、32bitアプリケーションだった。このことは確認済みだ。
* 32bitアプリケーションから呼び出すと、32bit版のPowerShellが実行される。これについては、別の問題に直面した人のだけど「[64bitなWindows上で32bitアプリを実行し、そのアプリからPowerShellのスクリプトをコールする際の注意点 - 技術的ひとりごと(備忘録)](http://hoge7743.hatenablog.com/entry/2014/10/26/040244)」というブログエントリがあった。
* 32bit版PowerShellは、64bit用の`c:\Program Files`ではなく32bit用の`c:\Program Files (x86)`以下を見に行っていた。
* 全ユーザーで利用できるようにと`c:\Program Files\WindowsPowerShell\Modules`に配置したモジュールを、32bit版のPowerShellは見つけられなかった。

一つ一つは妥当な感じがするけど、それらを総合すると、なんてことだろうね。こんな問題になった。

そのうち、こういう理解で正しいのか、そして全ユーザー向けのモジュールを手動で配置するならどうすべきなのか、調べないといけないな、と思ってる。でも後が詰まってるので、いまはひとまずここまでにして、他の仕事に移ろうと思う。もしご存知の方、特にこれを仕様だと示せるような公式情報に心当たりがある方、おいででしたらお教えいただけるとすごく助かります。