しばたテックブログ

PowerShellを中心に気分で書いている技術ブログです。

PowerShell Core 6.1のPowerShell Remotingにおける改善点について

本エントリではPowerShell Core 6.1のPowerShell Remotingにおける改善点について解説します。

docs.microsoft.com

元ネタは上記のDocsで、現在こちらに対するまとめを作成しているのですが、PowerShell Remotingについては少しボリュームが増えたので本エントリで先出しします。

コンテナ向けPowerShell Directの改善

PowerShell Direct

PowerShell DirectはWindows10/Windows Server 2016以降で利用でき、ホストから仮想マシンおよびコンテナへネットワークを介さず直接接続する機能になります。

利用可能なホストOSではリモート操作系のコマンドレットに-VMName-VMId-VMGuid-ContainerIdのパラメーターが付いており、これらのパラメーターを指定することで直接ゲストに接続します。

# VM向けPowerShell Directの例
Invoke-Command -VMName TestVM -ScriptBlock { $PSVersionTable } -Credential $cred

# コンテナ向けPowerShell Directの例
Invoke-Command -ContainerId 78438d708b648217899f600c3a0ac72b36814714a8a19d2f4bc03fbb08936519 -ScriptBlock { $PSVersionTable }

仕組みとしては、対VMの場合はHyper-V Socketを使った通信を行い、対コンテナの場合はHost Compute Service(HCS)のAPI(例えばHcsCreateProcessなど)を使ってコンテナ内部のPowerShellを起動したプロセス間通信を行っています。

PowerShell Core 6.1での改善

ここまでをふまえてPowerShell Core 6.1での改善点について説明します。

PowerShell Core 6.0までは対コンテナ向けのPowerShell Directで起動するPowerShellのプロセスはpowershell.exeで決め打ちされていました。
対してPowerShell Core 6.1では、最初にpwsh.exeの起動を試みpwsh.exeが無ければpowershell.exeを起動する様に改善されています。

例としてWindows Server 2019*1で以下のコマンドを試してみます。
予めdockerをインストールし、PowerShell Core 6.1をインストール済みのWindows Server Containerが起動された状態です。*2

# 起動しているコンテナの確認
docker ps

# コンテナIDの取得
# ※起動しているコンテナが1つだけの前提
$containerId = (docker inspect $(docker ps -q) | ConvertFrom-Json).Id

# PowerShell Directによるリモートコマンド実行
Invoke-Command -ContainerId $containerId -ScriptBlock { $PSVersionTable.PSVersion } 

Windows PowerShellからこのコマンドを実行すると、結果のバージョンが5.1とWindows PowerShell(powershell.exe)が実行されていることが分かります。

f:id:stknohg:20181013233855p:plain

対してPowerShell Core 6.1から実行すると、結果のバージョンが6.1.0とPowerShell Core(pwsh.exe)が起動され改善されていることがわかります。

f:id:stknohg:20181013233916p:plain

ちなみに、対VMのPowerShell Directはこれまで通りデフォルト指定ではWindows PowerShellとの通信になり、PowerShell Coreと通信する場合は-ConfigurationNameパラメーターを明示してやる必要があります。
こちらについては次項で説明します。

余談1

PowerShell Core 6.1のリリースバージョンではコンテナ接続に関するバグ(#5794)があるため、この機能を試すには最新のmasterから自分でビルドする必要があります。
おそらく次のリリース(PowerShell Core 6.1.1)に取り込まれると思いますので、ビルド環境のない方はそれまでお待ちください。

【2019.04.23追記】

#5794の修正はPowerShell Core 6.2.0で反映されました。

【追記ここまで】

余談2

当初この点についてドキュメントの内容に疑念があったためIssueを上げたところ回答と改善をしてもらえました。

github.com

あまりいないとは思いますが改善前のドキュメントを見ている人向けの補足です。

PowerShell Remotingのエンドポイントがプレビュー版と分離されました

PowerShell Remotingのエンドポイント

普段意識することは少ないでしょうがPowerShell Remotingでは接続対象となるホストに接続先(エンドポイント)が存在します。

このエンドポイントはGet-PSSessionConfigurationコマンドレットで確認でき、例えばWindows PowerShellにおいては下図の様に4つのエンドポイントがあることが分かります。*3

# 要管理者権限
# 接続される側のホストで実行する
Get-PSSessionConfiguration

f:id:stknohg:20181013234030p:plain

で、PowerShell CoreにおいてPowerShell Remotingを有効にした際のエンドポイントがどうなるかというと、バージョンによって何度か変遷しており、PowerShell Core 6.1では以下の2つのエンドポイントが作成されます。

  • PowerShell.6 : メジャーバージョンのみのエンドポイント
  • PowerShell.6.x.y : バージョンを厳密に指定したエンドポイント

Get-PSSessionConfigurationで確認するとこんな感じです。

# 要管理者権限
# PowerShell Coreインストール時にPowerShell Remotingを有効にしてない場合に実行
Enable-PSRemoting

# 接続される側のホストで実行する
Get-PSSessionConfiguration

f:id:stknohg:20181013234055p:plain

また、今後PowerShell Core 6.2に向けたプレビュー版でPowerShell Remotingを有効にした場合はプレビューバージョン向けの2つのエンドポイントが追加される予定となっています。

  • PowerShell.6-preview : メジャーバージョンのみのエンドポイント
  • PowerShell.6.x-preview.y : バージョンを厳密に指定したエンドポイント
Name          : PowerShell.6-preview
PSVersion     : 6.2
StartupScript :
RunAsUser     :
Permission    : NT AUTHORITY\INTERACTIVE AccessAllowed, BUILTIN\Administrators AccessAllowed, BUILTIN\Remote Management Users AccessAllowed

Name          : PowerShell.6.2-preview.1
PSVersion     : 6.2
StartupScript :
RunAsUser     :
Permission    : NT AUTHORITY\INTERACTIVE AccessAllowed, BUILTIN\Administrators AccessAllowed, BUILTIN\Remote Management Users AccessAllowed

これにより安定版とプレビュー版のエンドポイントを分けて利用できるというのが今回の改善点になります。

接続する側の設定について

補足として接続する側について触れておきます。

接続する側はデフォルトでMicrosoft.Powershellというエンドポイントが設定されており、これはWindows PowerShell/PowerShell Coreどちらでも変わりません。
このためPowerShell CoreからEnter-PSSessionなどのコマンドレットを使ってリモート接続してもデフォルトではWindows PowerShellが利用されます。

PowerShell Coreに対してPowerShell Remotingで接続する場合は-ConfigurationNameパラメーターを使って接続するエンドポイントを明示してやる必要があります。

たとえばInvoke-Commandであれば以下の様にしてやります。

# リモートのPowerShell Core 6に接続する場合はエンドポイントを明示する
$cred = Get-Credential
Invoke-Command -ConputerName [Server Name] -Credential $cred -ScriptBlock { $PSVersionTable.PSVersion } -ConfigurationName 'PowerShell.6'

# VM向けPowerShell Directでも同様
Invoke-Command -VMName [VM Name] -Credential $cred -ScriptBlock { $PSVersionTable.PSVersion } -ConfigurationName 'PowerShell.6'

f:id:stknohg:20181013234124p:plain

f:id:stknohg:20181013234136p:plain

PowerShell Remoting over SSHでuser@host:port形式の接続が可能になりました

ssh.exeといったSSHクライアントで使われるuser@host:port形式の接続がコマンドレットでもサポートされました。
PowerShell Remoting over SSHでは-HostNameパラメーターを使用して接続先を決めますが、ここに上記の形式を指定できます。

# Docsの例をそのまま引用
Enter-PSSession -HostName fooUser@ssh.contoso.com:2222

*1:PowerShell Directを利用するにはHyper-Vモジュールが利用可能である必要があり、このためこの改善を確認するためのホストOSはWindows 10 October 2018 Update(1809)かWindows Server 2019でなければなりません

*2:環境構築の手順は端折ります。

*3:正確にはSession Configurationとエンドポイントは別なのでしょうが、本エントリではわかりやすさのために同一視します

PowerShellのCountプロパティについてあれやこれや (2018年10月版)

3年ほど前に

blog.shibata.tech

というエントリを書いたのですが、その後新たに分かったことや新しいバージョンでの変更も発生したため、内容を更新してリライトしました。

便利なCountプロパティ

たとえばコマンドを実行して出力した結果の件数を取得したい場合、

([コマンド]).Count

とするとその件数を取得することができます。

具体例として以下の様にコマンドを実行するとC:\Program Files配下にあるEXEファイルの総数を取得することができます。

# Countプロパティを使うと、評価した結果(System.IO.FileInfo[])の要素数を取得することができます。
PS C:\> (dir -Path "C:\Program Files" -File -Filter "*.exe" -Recurse).Count
815

Countプロパティの実体

System.Array.Count?

このCount、要はコマンドを実行した結果(この場合はSystem.IO.FileInfo[])の要素数を取得しているだけなのですが、しかしながら、.NETの配列(System.Array)にはLengthプロパティはあれどもCountプロパティはありません。

それでもPowerShellではCountプロパティを利用することができています。

バージョン間の差異

この謎めいたCountプロパティはPowerShellのバージョンによってその裏にある実体が異なり、その挙動も異なります。

大きくはPowerShell 2.0までとPowerShell 3.0以降に分かれ、加えてPowerShell Core 6.0で若干の仕様変更が入り現在に至っています。

以降、バージョンごとの実体について触れていきます。

PowerShell 2.0までCountプロパティの実体

PowerShell 2.0までのCountプロパティの実体はSystem.Arrayに追加された型データになります。

型データとは.NETの型にPowerShell独自のデータを付与できる機能になります。
詳細については牟田口先生の以下のエントリを参照してください。

winscript.jp

型データの定義が記載されているC:\Windows\System32\WindowsPowerShell\v1.0\types.ps1xmlを見るとCountプロパティは以下の様に定義されており、System.Array.Lengthに対するAliasPropertyとなっていることがわかります。

# C:\Windows\System32\WindowsPowerShell\v1.0\types.ps1xmlより抜粋
    <Type>
        <Name>System.Array</Name>
        <Members>
            <AliasProperty>
                <Name>Count</Name>
                <ReferencedMemberName>Length</ReferencedMemberName>
            </AliasProperty>
        </Members>
    </Type>

Get-Memberで空の配列@()のメンバを確認してみるとこんな結果を返します。

PS C:\> Get-Member -InputObject @() -View Extended

   TypeName: System.Object[]

Name  MemberType    Definition
----  ----------    ----------
Count AliasProperty Count = Length

この型データにより本来System.Arrayには存在しないCountプロパティを利用することができます。

制限事項

PowerShell 2.0までのCountプロパティはSystem.Arrayに対するものなので配列以外では使うことができません。

例えば以下の様なコードは$nullを返し、配列以外ではCountプロパティが存在しないことがわかります。

# PowerShell 2.0
PS C:\> $PSVersionTable.PSVersion

Major  Minor  Build  Revision
-----  -----  -----  --------
2      0      -1     -1

PS C:\> (123).Count
PS C:\>

配列でないオブジェクトに対してCountプロパティを使いたいときは配列化してやる必要があります。

# PowerShell 2.0
# 要素数1の配列化してCountプロパティを取得
PS C:\> @(123).Count
1

PowerShell 3.0 ~ 5.1のCountプロパティの実体

PowerShell 2.0では配列にしかCountプロパティが使えませんでしたが、PowerShell 3.0からは配列でないスカラ値や$nullに対してもCountプロパティが使える様に機能が拡張されています。

# PowerShell 3.0 以降
PS C:\> $PSVersionTable.PSVersion

Major  Minor  Build  Revision
-----  -----  -----  --------
5      1      17763  1

# スカラ値のCountは 1
PS C:\> (123).Count
1

# $nullのCountは 0
PS C:\> ($null).Count
0

この変更については公にはWindows PowerShell Blogのこの記事で触れられる程度の情報しかなく、機能の実体については謎でした。

しかしながらPowerShellがオープンソースとなったため実際にソースを確認してみたところ、このCountプロパティは動的オブジェクトのメンバーフォールバックとして実装されていることが分かりました。

PowerShellは3.0ではランタイムが.NET 4ベースで刷新され、PowerShellすべてのオブジェクトの基本となるPSObjectDLRを使った動的オブジェクトとして定義されています。
これによりこれまでリフレクションによって行われていたメソッドやプロパティの解決が高速化され、PowerShell全体のパフォーマンス改善につながっています。

そしてDLRでは、主にエラーハンドリングのために、メソッドやプロパティの解決に失敗したときの処理を定義するフォールバック機能が提供されており、Countプロパティにおいては、

  1. オブジェクトのCount(またはLength)プロパティを取得しようとする

  2. Count(またはLength)プロパティが存在しないオブジェクトの場合、DLRのフォールバック(FallbackGetMember)が呼び出される

  3. フォールバック処理内で元のオブジェクトの種類に応じて1または0を返す

といった動作をしています。

(これがすべてではありませんが)ソースコードとしてはこのあたりの処理が参考になります。

参考として以下にフォールバックの一部を列挙しておきます。

//
// PowerShell 6.0時点の Binders.cs より一部引用
// 

/// <summary>
/// Return the binding result when no property exists.
/// </summary>
private DynamicMetaObject PropertyDoesntExist(DynamicMetaObject target, BindingRestrictions restrictions)
{

    // ・・・ 省略 ・・・

    // As part of our effort to hide how a command can return a singleton or array, we want to allow people to iterate
    // over singletons with the foreach statement (which has worked since V1) and a for loop, for example:
    //     for ($i = 0; $i -lt $x.Length; $i++) { $x[$i] }
    // If $x is a singleton, we want to return 1 for the length so code like this works correctly.
    // We do not want this magic to show up in Get-Member output, tab completion, intellisense, etc.
    if (Name.Equals("Length", StringComparison.OrdinalIgnoreCase) || Name.Equals("Count", StringComparison.OrdinalIgnoreCase))
    {
        // $null.Count should be 0, anything else should be 1
        var resultCount = PSObject.Base(target.Value) == null ? 0 : 1;
        return new DynamicMetaObject(
            Expression.Condition(
                Compiler.IsStrictMode(2),
                ThrowPropertyNotFoundStrict(),
                ExpressionCache.Constant(resultCount).Cast(typeof(object))), restrictions);
    }

    // ・・・ 省略 ・・・

}

ちなみに配列に対しては従来通り型データによるAliasPropertyが使われています。

制限事項

PowerShell 3.0で配列以外にもCountプロパティが使える様になったのですが、ぎたぱそ先生の以下のエントリによるとPScustomObjectに対してはCountプロパティは$nullを返してしまいます。

tech.guitarrapc.com

実際に試してみると確かに$nullを返します。

# PowerShell 3.0 ~ 6.0
PS C:\> $PSVersionTable.PSVersion

Major  Minor  Build  Revision
-----  -----  -----  --------
5      1      17763  1

PS C:\> ([PScustomObject]@{hoge = "hoge"}).Count
PS C:\>

このためPScustomObjectに対してCountプロパティを使用する際は注意が必要です。

PowerShell 6.0以降のCountプロパティの実体

PowerShell Core 6.0においてもCountプロパティの実体が動的オブジェクトのメンバーフォールバックであることは変わらないのですが、いくつか挙動が変更されています。

AliasPropertyの削除 (PowerShell Core 6.0より)

最初に説明したSystem.Array.Lengthに対するAliasPropertyですが、これはPowerShell Core 6.0で削除されました。

理由としてはこのAliasPropertyが無くても問題がない*1点と、ConvertTo-Json等の処理で不要なCountプロパティが露出してしまう問題(#3222)があったため削除されています。

github.com

PSCustomObjectに対するCount (PowerShell Core 6.1より)

前項で触れたPScustomObjectに対するCountですが、PowerShell Core 6.1で挙動が改善されPScustomObjectでもCountプロパティが取得できる様に改善されています。

PS C:\> $PSVersionTable.PSVersion

Major  Minor  Patch  PreReleaseLabel BuildLabel
-----  -----  -----  --------------- ----------
6      1      0

PS C:\> ([PScustomObject]@{hoge = "hoge"}).Count
1

【補足】PowerShell 4.0のWhere、ForEachメソッドについて

最後にちょっとだけおまけを。

PowerShell 4.0から配列などのコレクションオブジェクトに対してWhere()およびForEach()メソッドが使える様に機能拡張されています。

# PowerShell 4.0以降
PS C:\> @(1,2,3).Where({$_ -eq 2})
2

PS C:\> @(1,2,3).ForEach({$_ * 2})
2
4
6

これらのメソッドも動的オブジェクトのメンバーフォールバックとして実装されいます。
ソースコードとしてはこのあたりが参考になると思いますので興味のある方はご覧ください。

*1:前項で説明を端折りましたが、動的オブジェクトのメンバーフォールバックは配列の場合も考慮されいます

Server Core App Compatibility Feature on DemandのInternet Explorerに対する雑感

前のエントリ

blog.shibata.tech

でServer Core App Compatibility Feature on Demand(FoD)を使いServer CoreにInternet Explorer(IE)がインストールできることを述べましたが、本エントリではこの点について思ったことを書いていきます。

そもそもServer CoreにIEは必要なのか?

そもそもServer CoreにIEは必要なのか?

私が本エントリを書こうと思った理由はこの一点に尽きます。

もともとServer Coreはサーバーとして必須でない機能を削ってOS全体の軽量化とセキュリティの強化を図ったものです。

IEの用途は基本的にWEBブラウスで、サーバー上であればソフトウェアのインストーラーなどのリソースのダウンロードに使うくらいしかないでしょう。
従来からリソースのダウンロードにはPowerShellが使えましたし、たかがダウンロードのためだけにセキュリティ的にナイーブな機能であるIEを追加するというのは、個人的には全く割に合わないものだと思っています。

公式にIEを追加すべき動機について一切アナウンスされておらず、いまのところはこの疑問に対する答えを得ることはできません...

一応想像するかぎりでは、

  • IEコンポーネントを内部的に使うアプリケーションとの互換性確保のため?
  • かつてのmodern.ieの様な、テスト用途のIE入りWindows Server Containerを目論んでいる?

などを思いつきましたが、あくまでも私の妄想であり何とも言えません。

Server CoreでのWEBアクセスについて

私の妄想はさておき、Server CoreでのWEBアクセスについての基本的なところとIEを導入することによる挙動の変化について触れておきます。

IEがなくてもWEBアクセスの方法は以下に示す様に十分ありますし、IEが必要なケースがでればFoDで導入するとよいでしょう。

Windows PowerShellの利用

従来のServer CoreではIEが無いため、WEBアクセスは基本的にPowerShellで、Invoke-WebRequestInvoke-RestMethodを使う必要がありました。

これらの機能は内部的にIEコンポーネントを利用しているのですが、IEコンポーネントを利用しないWEBアクセスを行うために-UseBasicParsingというパラメーターがあり、Server Coreではこのパラメーターを指定してやる必要があります。
また、単純にファイルをダウンロードするだけであればこのパラメーターをつけなくてもServer Coreで動作させることは可能です。

# Server Coreでは-UseBasicParsingをつけないとエラーになる
Invoke-WebRequest -Uri 'https://www.yahoo.co.jp/'

f:id:stknohg:20181004160245p:plain

# -UseBasicParsingをつけてエラーを回避
Invoke-WebRequest -Uri 'https://www.yahoo.co.jp/' -UseBasicParsing

f:id:stknohg:20181004160308p:plain

# ファイルのダウンロードだけなら-UseBasicParsingはなくても良い
Invoke-WebRequest -Uri 'https://download.sysinternals.com/files/SysinternalsSuite.zip' -OutFile '.\SysinternalsSuite.zip'

f:id:stknohg:20181004160326p:plain

そして、FoDでIEを追加すると最初の-UseBasicParsingをつけない場合でも動作する様になります。

f:id:stknohg:20181004160341p:plain

これによりフルのWindows Serverと同等の互換性を得ることはできますが、この程度の互換が必要かといわれると疑問ですし、その様なシナリオも皆無と思っています。

curl.exeの利用

Windows Server 2019にはWindowsにポートされたcurl.exeが導入されており、Server Coreでも利用可能です。

f:id:stknohg:20181004160358p:plain

このためPowerShellに頼らなくてもcurlにより強力なWEBアクセスができる様になっています。

curl.exe https://www.yahoo.co.jp

f:id:stknohg:20181004160408p:plain

curl.exe https://download.sysinternals.com/files/SysinternalsSuite.zip -o .\SysinternalsSuite.zip

f:id:stknohg:20181004160418p:plain

PowerShell Coreの利用

こちらはおまけです。

もしWindows PowerShellのWEBアクセスで機能が足りていない部分があるのであれば、PowerShell Coreを追加でインストールするのも一つの解となるかもしれません。
PowerShell CoreのWebCmdletsはIEへの依存が排除されており、また、新しいWEBの仕組みへの対応もいくつかなされています。

インストール自体は手作業で行う必要がありますが、拙作のインストーラーを使えばワンライナーでインストールできます。

blog.shibata.tech

REM Command prompt
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "[Net.ServicePointManager]::SecurityProtocol=[Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12;iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/stknohg/PSCoreUpdate/master/FirstTimeInstaller/Install-LatestPowerShell.ps1'))"

f:id:stknohg:20181004161620p:plain

f:id:stknohg:20181004161751p:plain