しばたテックブログ

気分で書いている技術ブログです。

AutomationNullとは何なのか?

ちょっと前のはなしなのですがこちらのGistを読みました。

こちらのGistではPowerShellを使う上でハマりがちな罠について詳しく記述されておりPowerShellに対する非常に良い批判となっています。

で、このGistの中で$nullAutomationNullの2つのNULLについて触れられています。
私自身AutomationNullの存在は知っていたのですが表に露出しない内部的なものだと認識しており、これまで全然気にかけていませんでした。

本エントリではこのAutomationNullについて分かる範囲で説明したいと思います。

お断り

本エントリの内容はあくまで私個人の調査結果でありPowerShellとして公式な何かを表明するものではありません。

極力間違いの無い様に調べた結果を書いていますが内容に誤りがある可能性は多分にあります。
その点は割り引いてご覧ください。

あと本エントリに対するフィードバックは大歓迎です。
おかしな点などありましたら是非お知らせください。

$nullとは何なのか

現在PowerShellはオープンソースとなっています。
$nullAutomationNullの2つのNULLについて知るにはその実装を見てみるのが一番手っ取り早いでしょう。

最初に$nullについてその実装を見てみます。
PowerShellにおいてNULLを表現する変数である$nullSystem.Management.Automation.NullVariableに定義されています。

/// ShellVariable.cs(Ver.6.0.1) より引用

/// <summary>
/// This class is used for $null.  It always returns null as a value and accepts
/// any value when it is set and throws it away.
/// </summary>
///
internal class NullVariable : PSVariable
{
    /// <summary>
    /// Constructor that calls the base class constructor with name "null" and
    /// value null.
    /// </summary>
    ///
    internal NullVariable() : base(StringLiterals.Null, null, ScopedItemOptions.Constant | ScopedItemOptions.AllScope)
    {
    }

    /// <summary>
    /// Always returns null from get, and always accepts
    /// but ignores the value on set.
    /// </summary>
    public override object Value
    {
        get
        {
            return null;
        }

        set
        {
            // All values are just ignored
        }
    }

    /// <summary>
    /// Gets the description for $null.
    /// </summary>
    public override string Description
    {
        get { return _description ?? (_description = SessionStateStrings.DollarNullDescription); }
        set { /* Do nothing */ }
    }
    private string _description;

    /// <summary>
    /// Gets the scope options for $null which is always None.
    /// </summary>
    public override ScopedItemOptions Options
    {
        get { return ScopedItemOptions.None; }
        set { /* Do nothing */ }
    }
}

Valueプロパティが常にnullを返し、入力はすべて破棄される様になっています。
$nullはPowerShellにおいて.NET Frameworkのnullを表現するものであることがわかります。

ちなみにこの変数のDescriptionには

PS C:\> (Get-Variable 'null').Description
References to the null variable always return the null value. Assignments have no effect.

References to the null variable always return the null value. Assignments have no effect.

と記述されています。

AutomationNullとは何なのか?

そしてAutomationNullSystem.Management.Automation.AutomatinNullに定義されています。

/// AutomationNull.cs(Ver.6.0.1) より引用

/// <summary>
/// This is a singleton object that is used to indicate a void return result.
/// </summary>
/// <remarks>
/// It's a singleton class. Sealed to prevent subclassing. Any operation that
/// returns no actual value should return this object AutomationNull.Value.
/// Anything that evaluates an MSH expression should be prepared to deal
/// with receiving this result and discarding it. When received in an
/// evaluation where a value is required, it should be replaced with null.
/// </remarks>
public static class AutomationNull
{
    #region private_members

    // Private member for Value.

    #endregion private_members

    #region public_property

    /// <summary>
    /// Returns the singleton instance of this object.
    /// </summary>
    public static PSObject Value { get; } = new PSObject();

    #endregion public_property
}

ソースを見る限りいわゆるNULLオブジェクトとして定義されており非常にシンプルな内容です。
実装から見えるものはほとんど無いですが、summaryとremarksタグのコメントに非常に重要なことが書かれています。

AutomationNullはPowerShellにおけるVoidである

summaryコメントにAutomationNullの用途がズバリ書き記されています。

This is a singleton object that is used to indicate a void return result.

indicate a void return resultとある様にAutomationNullはPowerShellにおける式や文を評価した際に戻り値がないことを表すクラスであり、いわゆるVoid型と言われるものに相当します。

そしてremarksコメントに

Any operation that returns no actual value should return this object AutomationNull.Value.
Anything that evaluates an MSH expression should be prepared to deal
with receiving this result and discarding it. When received in an
evaluation where a value is required, it should be replaced with null.

とあり、AutomationNull.Valueを受け取った文や式はその値を切り捨てるべきとされ、値が必要とされる場合はnull(=$null)に置き換えられる様に要請されています。


AutomationNullがVoidであるわかりやすい例として、[void]型(System.Void型)へのキャストはAutomationNull.Valueを返す実装となっている点が挙げられます。

/// LanguagePrimitives.cs(Ver.6.0.1) より一部引用

    public abstract class PSTypeConverter
    {
        // ・・・ (省略) ・・・

        //
        // [void]型へのキャストは AutomationNull.Value を返す様になっている
        //
        private static object ConvertToVoid(object valueToConvert,
                                            Type resultType,
                                            bool recursion,
                                            PSObject originalValueToConvert,
                                            IFormatProvider formatProvider,
                                            TypeTable backupTable)
        {
            typeConversion.WriteLine("returning AutomationNull.Value.");
            return AutomationNull.Value;
        }

        // ・・・ (省略) ・・・

        private static ConversionData FigureLanguageConversion(Type fromType, Type toType,
                                                               out PSConverter<object> valueDependentConversion,
                                                               out ConversionRank valueDependentRank)
        {

            // ・・・ (省略) ・・・

            if (toType == typeof(void))
            {
                return CacheConversion<object>(fromType, toType, LanguagePrimitives.ConvertToVoid, ConversionRank.Language);
            }

            // ・・・ (省略) ・・・
        }

        // ・・・ (省略) ・・・
    }

このほかにも、その実装までは紹介できませんが、AutomationNullはパイプラインに渡されず破棄される、ストリームの出力も無視されるなどの挙動が確認できており、これがAutomationNullがVoidであることの裏付けになっていると私は考えています。

$nullとAutomationNullの違い

ここまででPowerShellに2つのNULLである$nullAutomationNullが存在している理由については明らかにできたかと思います。

これまで私はAutomationNullについてはあくまで内部用で表には露出しないと思っていたのですが、実態は先のGistにある様に当たり前の様に露出します。
(できるだけユーザーに対して見えない様にしていると感じますが、見えないけど確実に存在する状態が現状と思えます)

それどころか、何気なくPowerShellを使い$nullだと思っているもののほとんどが実はAutomationNull.Valueでした。

これは私の予想ですが、PowerShellにおけるNULLはAutomationNullが基本であり、$nullは.NET Frameworkの機能を呼び出すとき等でNULL値を明示する必要がある際に使う特殊な値と認識するのが正解の様です。

ただ、AutomationNullはユーザーに対して見えない様にしたいがために全くドキュメント化されておらず$nullと同じに見える挙動さえします。

この様な現状に対しては正直批判も止む無しと思いますが、直ちにどうにかすることができないのも現実なので割り切りは必要です。

$nullとAutomationNullの挙動の違い

ここから$nullAutomationNullの挙動の違いについて触れます。

原則として、

  • AutomationNullはVoidであり、評価を打ち切るものである
  • $nullは.NET Frameworkのnullを明示するものである

という役割の違いが挙動の違いに表れています。

先述のGistの例を出すと、

# NULL値を明示している
> @($null).Length
1

これはNULL値を明示的に利用する必要があるため要素数1の配列となります。

# AutomationNull.Valueの評価は打ち切られる
> @([System.Management.Automation.Internal.AutomationNull]::Value).Length
0

こちらは配列部分式演算子(@())をAutomationNull.Valueに対して評価することになり、評価を打ち切る必要があるため空の配列となります。

こちらの記述だと違和感があるかもしれませんが、以下の様な記述であれば0を返すことにさほど違和感を感じないのではないかと思います。

# [void]キャストはAutomationNull.Valueを返す
> @([void](Get-Date)).Length
0

補足資料

本エントリでの説明はこれくらいにしておきたいと思います。

AutomationNullの挙動についてはGitHubのIssueにもいくつか挙げられおり、その是非について広く議論されています。
最後にいくつかIssueを紹介しますので興味があればぜひご覧ください。

github.com

github.com

github.com

PowerShell Remoting over SSHを試す - 再び

以前に試した

blog.shibata.tech

ですが、PowerShell Core 6.0が正式リリースされ当時の内容では正しく動作しなくなっていたので改めてやり直しました。

PowerShell Remoting over SSHとは

従来のPowerShell Remoting Protocol(PSRP)では通信の下回りにHTTP/HTTPSを使いますが、PowerShell Remoting over SSHでは名前の通りSSHを使った通信を行います。

基本的な仕組みとしては、

  1. クライアント側はpowershell(.exe)がリモートセッション生成時にssh(.exe)を呼び出し、powershell(.exe)→ssh(.exe)のプロセス間通信を経由してサーバーへの通信を行う。

  2. サーバー側はSSHdのサブシステムにPowerShellを登録。
    クライアントからの接続があった場合はsshd(.exe)→powershell(.exe)とリモートシェルが実行される。

となっています。

PowerShell Remoting over SSHを試す

手順はGitHubで公開されているのでこの手順をベースに行います。
https://github.com/PowerShell/PowerShell/tree/master/demos/SSHRemotinggithub.com

【2018/05/20更新】
いつのまにかGitHubの手順が削除されてしまいました... (#6628)
手順は一致しないかもしれませんが、Docsのこちらの手順が参考になりますので代わりにご覧ください。

もしくは削除直前のこちらのバージョンをご確認ください。

試験環境

試験環境はVirtualBox上でWindows Server 2012 R2とCentOS 7.4.1708を使い双方向で通信させてみます。
他のOSでも細かい部分に違いはあるでしょうが基本的には同じ流れになると思います。

その他細かい条件は以下。

項目 Windows Server 2012 R2 CentOS 7.4.1708
基本設定 インストール直後の状態+最新のWindows Updateを実施 bento/centos7.4のBoxにyum updateを実施
IP 192.168.33.210 192.168.33.209
ユーザー vagrant, administrator vagrant, root

Windows → CentOSへの通信(パスワード認証)

CentOS側の設定

CentOSにPowerShell Coreをインストールして、sshd_configの設定を変更します。
パスワード認証、鍵認証の可否については試験環境のデフォルト設定のままとしました。

# CentOS PowerShell for Linuxのインストール
curl https://packages.microsoft.com/config/rhel/7/prod.repo | sudo tee /etc/yum.repos.d/microsoft.repo
sudo yum install -y powershell

# /etc/ssh/sshd_config の設定
# '# override default of no subsystems'のコメント行の下に設定を追加
# ※位置指定をかなり決め打ちにしているので注意
sudo sed -i.orig -e "/# override default of no subsystems/a Subsystem powershell /usr/bin/pwsh -sshs -NoLogo -NoProfile" /etc/ssh/sshd_config

# sshdの再起動
sudo systemctl restart sshd.service

Windows側の設定

標準でインストールされているPowerShell 4.0ではPowerShell Remoting over SSHできませんので、こちらにもPowerShell Coreをインストールします。
前回と比べてGitHubへの接続にTLS 1.2が必須となったため、Windows PowerShell側の設定を若干変更する必要があります。

# Windows PowerShell 4.0
# GitHubへ接続するためにTLS 1.2を有効化
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12

# PowerShell Coreのインストール
Invoke-WebRequest -Uri "https://github.com/PowerShell/PowerShell/releases/download/v6.0.1/PowerShell-6.0.1-win-x64.msi" -OutFile "PowerShell-6.0.1-win-x64.msi"
Start-Process -FilePath msiexec.exe -ArgumentList @("/i", "$($pwd.Path)\PowerShell-6.0.1-win-x64.msi", "/passive") -Wait -PassThru

次にSSHをインストールします。 Zipの解凍をしたいので先ほどインストールたPowerShell Coreから以下の手順でインストールします。

# PowerShell Core 6.0.1
# ssh.exeのインストール 
Invoke-WebRequest -Uri "https://github.com/PowerShell/Win32-OpenSSH/releases/download/v7.6.0.0p1-Beta/OpenSSH-Win64.zip" -OutFile "OpenSSH-Win64.zip"
Expand-Archive -Path ".\OpenSSH-Win64.zip" -DestinationPath "C:\"
[Environment]::SetEnvironmentVariable('PATH', [Environment]::GetEnvironmentVariable('PATH') + ';C:\OpenSSH-Win64', 'Machine')

インストール先はC:\OpenSSH-Win64としましたが、こちらについては必要に応じて変更しても構いません。
また、PowerShell Remoting over SSHではssh.exeに対してPATHが通っていること(正確にはssh.exeだけでコマンド呼び出しができること)が必須であるためPATHは必ず通す様にしてください。

テスト接続

ここから実際に試してみます。

まずはNew-PSSessionでPSセッションを作成します。 -HostName(-ComputerNameではない)、-UserNameパラメーターを指定するとPowerShell Remoting over SSHで通信することになります。

# PowerShell Core
$Session = New-PSSession -HostName 192.168.33.209 -UserName vagrant

実行すると以下の様にssh.exeを使ってサーバーに接続する際のあれやこれやを聞かれるので適切な情報を入力していきます。

f:id:stknohg:20180312193552p:plain

入力後、エラーが出なければ成功です。
$Sessionの中身を確認すると以下の様になり、名前がSSHになっていることがわかります。
このセッションがssh.exeと紐づいています。

f:id:stknohg:20180312193616p:plain

この$sessionを使いEnter-PSSessionしてリモート接続します。

# PowerShell Core
Enter-PSSession -Session $Session

実行結果は以下の様になり、無事CentOSにリモート接続できました。

f:id:stknohg:20180312193640p:plain

リモートからClear-Hostが使えないのは相変わらずでした...
TERM環境変数を適当に設定してみましたが、Clear-Hostしてもエスケープシーケンスが表示されるだけなのでOSが対応しないとダメそうな雰囲気を感じます。
(Windows10やWindows Server 2016だと上手くいくやも?)

最後にRemove-PSSessionしてセッション情報を削除すると紐づいていたssh.exeも終了します。

# PowerShell Core
Remove-PSSession -Session $Session
  
# 個別に消すのが面倒なら以下で一気に消しても良い
Get-PSSession | Remove-PSSession

Windows → CentOSへの通信(鍵認証)

鍵認証で通信する場合、基本的にSSHの鍵認証の方法と同一です。

先ずはWindows側でssh-keygen.exeで鍵を作ります。
鍵の種類やパスフレーズの有無は適当にしてください。

以下の実行例ではRSA2 4096Byte、パスフレーズ有りにしています。

# Windows PowerShell 4.0 / PowerShell Core どちらでも可
#  作成する鍵の設定は適当に
cd C:\OpenSSH-Win64\
.\ssh-keygen.exe -t rsa -b 4096

f:id:stknohg:20180312193705p:plain

あとは作った公開鍵(id_rsa.pub)をCentOS側に転送し~/.ssh/authorized_keysに追加するだけです。 以下の例では/vagrant/id_rsa.pubに鍵を転送しています。

# CentOS bash
# ユーザーはvagrantユーザー
cat /vagrant/id_rsa.pub >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

これで鍵認証の準備は完了です。
鍵認証で接続する場合はNew-PSSession-KeyFilePathパラメーターを使用します。

実行例は以下

# PowerShell Core
$Session = New-PSSession -HostName 192.168.33.209 -UserName vagrant -KeyFilePath ~\.ssh\id_rsa

f:id:stknohg:20180312193737p:plain

セッションが生成された後はパスワード認証の場合と同様です。

CentOS → Windowsへの通信(パスワード認証)

最後に、CentOSをクライアント、Windowsをサーバーにした場合の接続を試します。
前回同様パスワード認証のみとします。

まずはWindows上でsshdを動作させます。 基本的にはGitHub上のインストール手順に従うだけなので問題ないかと思います。

# Windows PowerShell 4.0
cd C:\OpenSSH-Win64\

# sshdのインストール
.\install-sshd.ps1

# Firewall設定
New-NetFirewallRule -Name sshd -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22

# サービス起動
Start-Service sshd

sshd_configの設定はCentOSの場合と同様でサブシステムにPowerShell(Coreの方)を登録します。
なお、sshd_configC:\ProgramData\ssh\sshd_configを修正してください。(これはVer.1.0.0.0-betaからの変更になります)

追加例 )

# C:\ProgramData\ssh\sshd_config

# override default of no subsystems の欄に追記
Subsystem   sftp    sftp-server.exe
Subsystem powershell c:/program files/powershell/6.0.1/pwsh.exe -sshs -NoLogo -NoProfile

あとパスワード認証も許可しておきます。

# コメント解除
PasswordAuthentication yes

サービスを再起動して設定を反映させます。

Restart-Service sshd

これで準備は完了です。

CentOSからWindowsへ接続してみます。

# CentOS PowerShell Core
$Session = New-PSSession -HostName 192.168.33.210 -UserName administrator

セッション情報はこんな感じになります。

f:id:stknohg:20180312193850p:plain

Enter-PSSessionでWindowsへ接続できます。

# CentOS PowerShell Core
Enter-PSSession -Session $Session

f:id:stknohg:20180312193909p:plain

前回同様にPowerShell Core→PowerShell 4.0(Desktop)とネストしてPowerShellを対話的に実行できないままで、powershell.exe -Command {ほげ}の様にコマンド実行であればネストして実行可能でした。

Windows Subsystem for Linux(Ubuntu)にRe:VIEWをインストールする

github.com

流行りのRe:VIEWを試してみたくなったので手元のWindows 10にインストールしてみました。

Re:VIEW on Windows

Windows上でRe:VIEWを試すには色々な方法があり、公式にはDocker Toolboxを使う方法が紹介されています。
今回私はWindows Subsystem for Linux(WSL)のUbuntu上にRe:VIEWをインストールする方法を採りました。

環境は以下となります。

  • 最新のWindows Updateを適用した64bit版 Windows 10 Pro (1709)
  • WSL上のUbuntu 16.04

Re:VIEWのインストール

本エントリではWSL上のUbuntuを用意する手順は端折ります。
MicrosoftストアからUbuntuをインストールして初期ユーザー設定を済ませた状態をスタート地点とします。

rbenvとRubyのインストール

Re:VIEWはRuby製アプリなので最初にRubyの実行環境を用意します。
以下のドキュメントを参考にしてrbenvとRubyをインストールします。

WSLのUbuntu(bash.exe)を起動し以下のコマンドを順に実行します。
今回はRuby 2.5.0をインストールしました。

# 前準備の apt-get update
sudo apt-get update -y
# rbenvのコンパイルに必要
sudo apt-get install -y gcc make
# Rubyのビルドに必要。バージョンによって変わるやも?
sudo apt-get install -y libssl-dev libreadline-dev zlib1g-dev

# rbenvのインストール
git clone https://github.com/rbenv/rbenv.git ~/.rbenv
chmod go-w -R ~/.rbenv
cd ~/.rbenv && src/configure && make -C src
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
source ~/.bash_profile

# ruby-buildのインストール (rbenv installコマンドに必要)
mkdir -p "$(rbenv root)"/plugins
git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build

# Rubyのインストール
#   今回は Ruby 2.5.0をインストール
rbenv install 2.5.0

# システム全体でのデフォルトバージョンを指定
rbenv global 2.5.0

Re:VIEWインストールの前準備

WSL側から見たWindows(/mnt/c/配下のディレクトリ)の任意の場所にRe:VIEW用のディレクトリを作ります。
今回は/mnt/c/Temp/review(C:\Temp\review)を対象します。

# Windows側のディレクトリにファイルを作る
mkdir /mnt/c/Temp/review

Re:VIEWのインストール

前項で作成したディレクトリに移動してrbenvの設定とRe:VIEWのインストールを行います。
インストール自体はgem installでサクッと行けます。

# 移動
cd /mnt/c/Temp/review/

# このディレクトリで利用するRubyのバージョンを設定
rbenv local 2.5.0
# Re:VIEWのインストール
rbenv exec gem install review

(オプション)TeX Liveのインストール

Re:VIEWでPDFのドキュメントを生成する場合システムにTeXがインストールされている必要があります。
(PDFを出力しない場合は不要です)

本エントリでは以下のサイトの手順を参考にTeX Liveをインストールします。

TeX Liveのインストールはapt-get installでも可能ですが、最新のバージョンを使おうと思いネットワークインストーラを使用したインストールにしました。

# TeX Liveのインストール
cd ~
# 今回はJAISTを選んでいますが、ダウンロード先のアドレスはミラーサイトから適当に選んでください
curl -O http://ftp.jaist.ac.jp/pub/CTAN/systems/texlive/tlnet/install-tl-unx.tar.gz
tar xvf install-tl-unx.tar.gz
cd install-tl*
sudo ./install-tl --repository http://ftp.jaist.ac.jp/pub/CTAN/systems/texlive/tlnet/

# あとはインストーラーの指示に従う。
# 今回はデフォルト設定のままインストール続行。

# インストール後パス追加
sudo /usr/local/texlive/????/bin/*/tlmgr path add

また、現時点のRe:VIEW 2.5.0でソースコードのリストを使う場合、PDF生成時にjlisting.styが不足している旨のエラーが出ます。
こちらは以下のIssueでも認識されておりRe:VIEW 3.0で対処する見込みの様です。

とりあえず現状は公開されているサイトからjlisting.styをダウンロードして追加する必要があるので、以下のコマンドで追加しておきます。

# ソースコードのリストを使う場合はjlisting.styを追加
curl -OL https://ja.osdn.net/projects/mytexpert/downloads/26068/jlisting.sty.bz2
bzip2 -d ./jlisting.sty.bz2
sudo mv ./jlisting.sty /usr/local/texlive/2017/texmf-dist/tex/latex/listings
sudo chmod 644 /usr/local/texlive/2017/texmf-dist/tex/latex/listings/jlisting.sty
sudo mktexlsr

これで一通りのインストールは完了です。

Re:VIEWを試す

最後にインストールしたRe:VIEWを試してみます。

雛形作成

review-initコマンドでドキュメントの雛形を作成できますので適当な名前で作成します。
(今回はsample-docにしました)

cd /mnt/c/Temp/review/

# サンプル作成
review-init sample-doc

Windows側のディレクトリにサンプルを作っているのでVisual Studio Codeから内容を確認することができます。
Re:VIEW拡張をインストールしてればプレビュー表示も可能です。

f:id:stknohg:20180309174342p:plain

(上図ではクイックスタートガイドにあるテキストを試しています)

ドキュメント生成

前項で作成した雛形のルートフォルダでreview-epubmakerreview-pdfmakerコマンドを実行すればEPUBおよびPDFドキュメントを生成できます。
雛形にはRakeの設定も含まれているのでrakeコマンドから生成することも可能です。

cd /mnt/c/Temp/review/sample-doc/

# EPUB生成
review-epubmaker config.yml
# rake epub でも良い

# PDF生成
review-pdfmaker config.yml
# rake pdf でも良い

生成されたEPUBとPDFは以下の様な感じになりました。

f:id:stknohg:20180309174416p:plain

f:id:stknohg:20180309174425p:plain

とても良い感じですね。