2012年12月24日月曜日

about_SessionState - Things That Are Precious To a PowerShell Librarian -

PowerShell Advent Calendar 2012、24 日目です♪

今年ももうすぐ終わり・・・...( = =)、PowerShell Advent Calendar 2012 も残りわずかとなりましたね。
昨日は @MasayaSawada さんの『Windows Server 2012 Essentials の Powershell』でした。


さて街は「クリスマス」一色で煌びやかな 24 日。こちらも負けじと、PowerShell の華(と私が勝手に思っている)、セッションのお話をさせていただければと。

セッション。それは、ヘルプ トピック上、いたるところで登場し、構成要素を追加されたり、検索されたり、削除されたり、インポートされたり、エクスポートされたりする領域。個人的には、PowerShell の超中心的概念だと思うのですが、驚くべきことに、これを説明するヘルプ トピックはありません。代わりに、スコープやモジュール、PSSession、関数、変数のトピックを見て回ることにより、それが PowerShell が持つ環境そのものであり、なるべく問題領域を局所化し、他の部分へ影響を与えにくくするための仕組みであることが、朧げながら見えてくるように思います。

まあ、イマドキのプログラミング言語であれば、このような仕組みを何かしら持っているはずで、目新しいことは無いのですが・・・。ただ、PowerShell に限らず、IDE 側のサポートが少なくなりがちな動的型付け言語では、このような言語側の仕組みに頼るだけでなく、なるべく扱いやすい規模でファイルを分割したり、状態を制御できるように保ったり、といったことを、人間側で、ある程度お膳立てしないといけないのだなー、と感じる今日この頃。先日の記事に登場するライブラリ、PSAnonym も、この辺りの考え方を盛り込み、今後なるべく機能の追加や拡張をしやすい形に整備しました。

そんな試行錯誤から、今回は、about_SessionState と銘打ち、セッションが何なのかということを、自分の理解の整理も兼ねて、できるだけ俯瞰的に解説できればと思います。Tips のような、これは便利!というものではないのですが、PowerShell で、これからある程度まとまったものを作ろうと考えられている方へ参考になれば幸いです。

それではぼちぼち始めましょう~。

※今回の記事で扱うのは、PowerShell v2 の情報となります。PowerShell v3 での情報は、また別の機会に検証させていただければと思います <(_ _)>


こちらの情報を参考にさせていただきました。いつもお世話になっております! (`・ω・ )ゝ
Best Practices for Windows PowerShell
PowerShellでプロトタイプベースのオブジェクト指向を記述する方法 - 趣味の無い人生は虚しい
Parallel LINQ (PLINQ)
Writing your own PowerShell Hosting App (Part 1. Introduction) PowerShell Station
How to Host PowerShell in a WPF Application
snoopshell The marriage of Snoop WPF and PowerShell





目次

PS1 means Pathologically Suppleness no.1!?
「PowerShell でまとまったものなんて作らないよ」・・・(ToT)
いきなりそんなことおっしゃらずに・・・いやいや、奥さん、ちょっとこんなデータがあるんですよ。

まとまったものを作る時にプログラマの誰しもが考えるのが、書くコードの一貫性、すなわちコーディングスタイル。
いくつかのプログラミング言語の状況を比べてみますと、2012/12/24 現在、Bing での検索結果は以下のようなものになりました:

  "JavaScript Coding Style" で検索: 約 1,460,000 件 / "JavaScript" で検索: 約 819,000,000 件 ⇒ 約 0.2 %
  "Scala Coding Style" で検索: 約 67,300 件 / "Scala" で検索: 約 14,200,000 件 ⇒ 約 0.5 %
  "Basic Coding Style" で検索: 約 2,490,000 件 / "Basic" で検索: 約 314,000,000 件 ⇒ 約 0.8 %
  "PHP Coding Style" で検索: 約 2,150,000 件 / "PHP" で検索: 約 282,000,000 件 ⇒ 約 0.8 %
  "Ruby Coding Style" で検索: 約 626,000 件 / "Ruby" で検索: 約 71,700,000 件 ⇒ 約 0.9 %
  "Java Coding Style" で検索: 約 1,460,000 件 / "Java" で検索: 約 94,000,000 件 ⇒ 約 1.6 %
  "C++ Coding Style" で検索: 約 1,020,000 件 / "C++" で検索: 約 22,200,000 件 ⇒ 約 4.6 %
  "C# Coding Style" で検索: 約 860,000 件 / "C#" で検索: 約 18,600,000 件 ⇒ 約 4.6 %
  "Perl Coding Style" で検索: 約 776,000 件 / "Perl" で検索: 約 11,800,000 件 ⇒ 約 6.6 %
  "F# Coding Style" で検索: 約 75,600 件 / "F#" で検索: 約 735,000 件 ⇒ 約 10.3 %

  "PowerShell Coding Style" で検索: 約 99,900 件 / "PowerShell" で検索: 約 496,000 件約 20.1 %

ちょっとスゴいことになっていますね。この割合を鵜呑みにするならば、PowerShell をやられている方の 5 人に 1 人がコーディングスタイルに悩まれていることになります。
個人的には、柔軟な書き方ができる言語(1 つのことをするのに何通りもやり方がある言語)ほど、このような議論が発生しやすいと思っているのですが、あの C++ や Perl、他の Microsoft 製ごった煮言語、C#/F# を抑えての堂々の 1 位とは・・・。また、これだけ高い割合で、コードの書き方に悩まれる方がいらっしゃる、ということは PowerShell で何かしら作ろうとされている方が多いことの表れとも考えられるのではないでしょうか。・・・えっ、無理やり過ぎるですって?そこは発信者特権ということで (^_^;)

この結果を信じるかどうかはさておき、コーディングスタイルで悩むことの 1 つに、「1 ファイルの行数は何行?」「1 メソッドの行数は何行?」といったものがあるかと思います。近頃は関数型なエッセンスを取り込んでいる言語も少なくなく、1 行の裏側で色んなことが行われていますので、上記のような制限を掛けるまでもなくどんどん簡素化される印象があります。
しかしながら、特に低レイヤな仕組みを構築するに当たり、処理が増えていった場合にどのようにして分割すると問題が起こりにくくなるかをあらかじめ検討しておくことは、後々気を病まずに済むことになるでしょう。
PowerShell において、このような分割を検討するに当たり、覚えておく必要があるのが、セッションということになります。




about_Scopes
そのものずばりなヘルプ トピックは首記の通り無いのですが、比較的俯瞰された説明があるのが about_Scopes になります。セッションに触れられるのはちょこっとなのですが、PowerShell のセッションに似た概念であるスコープは 1 つトピックが切られており、それに類似する概念も解説されています。
ざっと書き出してみましょう:

  ・スコープは以下のルールにより、変更してはいけない項目を誤って変更することを防止する:
    ・スコープ内に作成できる項目は、変数、エイリアス、関数、スクリプトまたは PowerShell ドライブである。
    ・スコープ内に作成された項目は、明示的にプライベートとして宣言した場合を除き、項目が作成された
     スコープ内およびその子スコープ内でのみ可視になる。
    ・スコープ内に作成した項目は、他のスコープを明示的に指定した場合を除き、項目が作成されたスコープ内でのみ変更できる。
  ・スコープには以下の種類がある:
    ・グローバル
    ・ローカル
    ・スクリプト
    ・プライベート
    ・番号付きスコープ
  ・スコープは以下のタイミングで生成される:
    ・スクリプトまたは関数を実行したとき
    ・PSSession を作成したとき
    ・PowerShell の新しいインスタンスを起動したとき
  ・スコープに類似した概念に、以下のような自己完結型の環境がある:
    ・PSSession
    ・モジュール
    ・入れ子になったプロンプト

ここにセッションを絡めてみます。文字ばかりだとややこしいですので、図でまとめてみるとこんな感じになるかと思います。


・・・ややこしいのは相変わらずですが、少しは俯瞰しやすくなったでしょうか (^_^;)

ちょっと補足しますと、PowerShell のヘルプ トピックでセッションと言った場合、「.NET におけるアプリケーションのドメイン(AppDomain)」を同時に指している場合や、「PowerShell の新しいインスタンスを起動すると自動的に開始される環境」を単に指している場合、「コンピューター毎に作成できる PSSession」を同時に指している場合・・・というパターンがあるようなのです。

一括りにすると混乱すると感じましたので図でも分けてありますが、この記事では、単に「セッション」と言った場合、基本的には 2 番目のものを指すものとしたいと思います。PowerShell のヘルプ トピック上で単に「セッション」とあっても、最初のものを指していると思われる場合は、AppDomain + セッションとし、3 番目を指していると思われる場合は、PSSession + セッションとしています。

また、モジュールがセッションを持っていることにしています。これは about_Modules トピックには記載はないのですが、PowerShell の振る舞いから便宜上そうしているものです。スコープについても、基本的にセッション + スコープという形で動作しています。

あとは、about_Scopes トピックの関連項目にちらっと記載があるだけの環境変数や作業フォルダなどの OS の環境ですが、変数としては特別に扱われるため、注意が必要です。

さて、外側の領域から、OS の環境/PSSession + セッション、AppDomain + セッション、セッション + スコープと、コマンドを叩きながら代表的な構成要素の動きを見ていきたいと思います。




OS の環境/PSSession + セッション
OS の環境である環境変数や作業フォルダですが、PSSession では新しいものが 1 つ切られ、プロンプトでは起動元のプロンプトのものが引き継がれます。
PSSession はコンピューター毎に作成されるものですし、環境変数などが起動元のプロンプトのものが引き継がれるのは標準のコマンド プロンプトと同じ動きと同じですので、これは問題無さそうですね。
C:\>:: 標準のコマンド プロンプトで環境変数を変更。
C:\>set EnvironmentVariable=Environment Variable

C:\>echo %EnvironmentVariable%
Environment Variable

C:\>:: PowerShell プロンプトを実行。環境変数が引き継がれる。
C:\>powershell -nol -nop -c "& { $Env:EnvironmentVariable }"
Environment Variable

C:\>:: 入れ子になったPowerShell プロンプトを実行。環境変数が引き継がれる。
C:\>powershell -nol -nop -c "& { powershell -nol -nop -c '& { $Env:EnvironmentVariable }' }"
Environment Variable

C:\>:: PSSession では新しい OS の環境が用意される。
C:\>powershell -nol -nop -c "& { icm { iex '$Env:EnvironmentVariable' } -session (nsn localhost) }"

C:\>
 

PSSession って実際は何なの?と思って調べてみると、以下のような結果に。WinRM サービスから起動される、PowerShell.exe とは別の PowerShell ホスティング プロセスのようです。
PS C:\> Enter-PSSession localhost
[localhost]: PS C:\Users\User\Documents>  [Diagnostics.Process]::GetCurrentProcess() | Format-List


Id      : 1292
Handles : 279
CPU     : 1.6224104
Name    : wsmprovhost



[localhost]: PS C:\Users\User\Documents>
 





AppDomain + セッション
PowerShell に限らず、.NET を扱うプロセスでは、基本的に AppDomain の呪縛からは逃れられませんので、無理に隠ぺいして良いことはないと思うのですがどうなんでしょう・・・? (^_^;)
ヘルプ トピックでは単にセッションという記載しか無いのにも関わらず、実際には AppDomain + セッションを指しているものは、Add-PSSnapin や Add-Type、Assembly 指定版の Import-Module が該当します。無理している感じは、例えば、Add-PSSnapin と対になる Remove-PSSnapin のヘルプ トピックにある、「スナップインは、現在のセッションから削除した後も読み込まれた状態のまま」という説明の苦しさからもわかります。読み込まれた状態ってどこに読み込まれたままなのー???と疑問に思う方は少なくないと思いますが、その答えが AppDomain になります。
PS C:\> # 現在のプロセスの AppDomain に読み込まれている Assembly の数を表示する。
PS C:\> [AppDomain]::CurrentDomain.GetAssemblies().Length
21
PS C:\> # スナップインを追加してみる。
PS C:\> Add-PSSnapin SqlServerCmdletSnapin100
PS C:\> # コマンドは増えた?
PS C:\> (gcm).Length
414
PS C:\> # もう一度 AppDomain に読み込まれている Assembly の数を表示する。
PS C:\> [AppDomain]::CurrentDomain.GetAssemblies().Length
23
PS C:\> # コマンド、読み込まれている Assembly、共に 2 つ増えた!スナップインを削除してみる。
PS C:\> Remove-PSSnapin SqlServerCmdletSnapin100
PS C:\> # コマンド、読み込まれている Assembly を確認すると、コマンドは減ってるけど
PS C:\> # Assembly は減っていない。
PS C:\> (gcm).Length
412
PS C:\> [AppDomain]::CurrentDomain.GetAssemblies().Length
23
PS C:\> 
 

同じ AppDomain に、同一名称の別の型は読み込めません。標準の PowerShell ホスティング プロセスは、基本的にはシングル ドメインしか扱いませんので、一度 Add-Type した型を定義し直したい場合などは、プロンプトもしくは PSSession を使って新しいプロセスを開始する必要があります。
PS C:\> # 入れ子になったプロンプトの開始。
PS C:\> powershell -nol -nop
PS C:\> # 型の追加。
PS C:\> Add-Type NewType -m 'public static string NewTypeMethod() { return "New Type"; }' -names $null
PS C:\> [NewType]::NewTypeMethod()
New Type
PS C:\> # ここでは再定義はできない。
PS C:\> Add-Type NewType -m 'public static string NewTypeMethod() { return "New Other Type"; }' -names $null
Add-Type : 型を追加できません。型名 '.NewType' は既に存在しています。
発生場所 行:1 文字:9
+ Add-Type <<<<  NewType -m 'public static string NewTypeMethod() { return "New Other Type"; }' -names $null
    + CategoryInfo          : InvalidOperation: (.NewType:String) [Add-Type]、Exception
    + FullyQualifiedErrorId : TYPE_ALREADY_EXISTS,Microsoft.PowerShell.Commands.AddTypeCommand

PS C:\> # 入れ子になったプロンプトの終了。
PS C:\> exit
PS C:\> # 入れ子になったプロンプトの再起動。
PS C:\> powershell -nol -nop
PS C:\> # 再定義。
PS C:\> Add-Type NewType -m 'public static string NewTypeMethod() { return "New Other Type"; }' -names $null
PS C:\> [NewType]::NewTypeMethod()
New Other Type
PS C:\>
 





セッション + スコープ
変数、エイリアス、関数、スクリプトブロック、PowerShell ドライブ、スクリプト、モジュールは、親の構成要素のセッションに追加される形で動作します。
変数、エイリアス、関数、スクリプトブロック、PowerShell ドライブ、スクリプトは、それ自身のセッションを持たない代わりにスコープの制御が行われます。モジュールは逆に、それ自身のセッションを持つ代わりにスコープの制御は行われません。

違いが分かりやすい例としてスクリプトとモジュールを取り上げてみましょう。スクリプトをプロンプトのセッションで実行します。変数はスコープを抜けると、セッションから削除されます。
PS C:\> # スクリプト内で、変数を宣言(変数は、プロンプトのセッションに、
PS C:\> # スクリプト スコープのものとして追加される)
PS C:\> @"
>> `$Script1 = 'New Script 1'; gv Script*
>> "@ > Script1.ps1
>>
PS C:\> @"
>> `$Script2 = 'New Script 2'; gv Script*
>> "@ > Script2.ps1
>>
PS C:\> # 各スクリプトの実行が終わると、スクリプト スコープの変数は、
PS C:\> # プロンプトのセッションから削除される。
PS C:\> .\Script1.ps1

Name                           Value
----                           -----
Script1                        New Script 1


PS C:\> gv Script*
PS C:\> .\Script2.ps1

Name                           Value
----                           -----
Script2                        New Script 2


PS C:\> gv Script*
PS C:\> ri .\Script1.ps1; ri .\Script2.ps1
PS C:\>
 

スクリプトはセッションを持ちませんので、スクリプト内でモジュールをインポートした場合、プロンプトのセッションに追加されます。
PS C:\> # スクリプト内で、モジュールをインポート(スクリプトはセッションを持たないため、モジュールは、
PS C:\> # プロンプトのセッションにインポートされる)
PS C:\> @"
>> 'Module1' | nmo { function Module1Func { 'Module 1 Func' } } | ipmo
>> "@ > Script1.ps1
>>
PS C:\> @"
>> 'Module2' | nmo { function Module2Func { 'Module 2 Func' } } | ipmo
>> "@ > Script2.ps1
>>
PS C:\> # モジュールはスコープを持たないため、インポートしたセッションが終了もしくは
PS C:\> # 明示的に削除が行われるまでは残ったまま。
PS C:\> .\Script1.ps1
PS C:\> gmo

ModuleType Name                      ExportedCommands
---------- ----                      ----------------
Script     Module1                   Module1Func


PS C:\> .\Script2.ps1
PS C:\> gmo

ModuleType Name                      ExportedCommands
---------- ----                      ----------------
Script     Module1                   Module1Func
Script     Module2                   Module2Func


PS C:\> rmo *; ri .\Script1.ps1; ri .\Script2.ps1
PS C:\>
 

逆にモジュールはセッションを持ちます。モジュール内でモジュールをインポートした場合、入れ子になったモジュールはモジュールのセッションに追加されることに注意しなければなりません。
特にまとまったものを作る場合、モジュールの中にモジュールを定義したくなる場面が多々あるかと思いますが、このことを忘れなければ混乱せずに済むでしょう。
PS C:\> # モジュール内で、モジュールをインポート(入れ子になったモジュールは、モジュールのセッションに
PS C:\> # インポートされる)
PS C:\> @"
>> 'NestedModule1' | nmo { function NestedModule1Func { 'Nested Module 1 Func' } } | ipmo
>> "@ > Module1.psm1
>>
PS C:\> @"
>> 'NestedModule2' | nmo { function NestedModule2Func { 'Nested Module 2 Func' } } | ipmo
>> "@ > Module2.psm1
>>
PS C:\> # プロンプトのセッションには入れ子になったモジュールは無く、モジュールのセッションにインポート
PS C:\> # されていることがわかる。
PS C:\> $m1 = ipmo .\Module1.psm1 -pa
PS C:\> gmo

ModuleType Name                      ExportedCommands
---------- ----                      ----------------
Script     Module1                   NestedModule1Func


PS C:\> & $m1 { gmo }

ModuleType Name                      ExportedCommands
---------- ----                      ----------------
Script     NestedModule1             NestedModule1Func
Script     Module1                   NestedModule1Func


PS C:\> $m2 = ipmo .\Module2.psm1 -pa
PS C:\> gmo

ModuleType Name                      ExportedCommands
---------- ----                      ----------------
Script     Module1                   NestedModule1Func
Script     Module2                   NestedModule2Func


PS C:\> & $m2 { gmo }

ModuleType Name                      ExportedCommands
---------- ----                      ----------------
Script     NestedModule2             NestedModule2Func
Script     Module1                   NestedModule1Func
Script     Module2                   NestedModule2Func


PS C:\> # ただし、同時に親セッションにメンバーがエクスポートされているため(NestedModule1 -> Module1 -> 
PS C:\> # プロンプト、NestedModule2 -> Module2 -> プロンプト)、関数は呼べる。
PS C:\> NestedModule1Func
Nested Module 1 Func
PS C:\> NestedModule2Func
Nested Module 2 Func
PS C:\> rmo *; ri .\Module1.psm1; ri .\Module2.psm1
PS C:\>
 





終わりに
PowerShell Advent Calendar 2012 の 24 日目、about_SessionState。いかがでしたか。願わくは、いつか PowerShell v3 のヘルプ トピックのどこかに、この辺りの話が載ればと思うばかりです。

ただ、まとめてみて思いましたが・・・ある意味「苦理済ます」にふさわしい、己の心を一人っきりで見つめる修行の日にぴったりの内容になってしまったかも・・・ <(ToT)>
お堅い話ばかりで申し訳ないです。本当は、もう少し実際のライブラリの中を歩きながら解説をしようとも思ったのですが、そちらの進捗が芳しくないですので、出来上がりの頃に改めて記事を書かせていただければと。

あ、話は変わるのですが、こちらばかりじゃなく、本体である Prig も早いところ形にしたいところだったりします。5 年近く同じテーマをやっていてなかなかモノが出てこないのは切ないですし。メモリ周りやビルドの問題も大方片付いたはずですので、近いうちに IL 打ち込む作業に入れるはず。来年こそは、自分を含めた開発者の方の心躍らせられるようなモノ、出せるようにがんばるです。 (`・ω・ )ゝ

さあ、いよいよ明日は大トリ、@mutaguchi さんです。よろしくお願いします!

0 件のコメント:

コメントを投稿