ServerspecでWinRMクライアントをC#で作成したコマンドに置き換えてみた

# はじめに

以前書いた記事[Serverspecで使うWinRMクライアントをInvoke-commandに置き換えてみた](http://qiita.com/h-ymmr/items/627aa60b57171f67d295)では処理速度がデフォルトのWinRMより1.3倍くらい遅くなったので、C#でWinRMクライアントを実装したら少しは高速化が可能か確認してみることにしました。

# バージョン

記事作成で用いたOS・モジュールのバージョンはこちらです。

- Windows 7 Pro SP1 32bit 日本語版
- ruby 2.2.3p173 (2015-08-18 revision 51636) [i386-mingw32]
- rspec (3.4.0)
- rspec-core (3.4.1)
- rspec-its (1.2.0)
- serverspec (2.26.0)
- specinfra (2.47.0)

※rubyスクリプトは全て文字コードを「UTF-8」で保存しています。
※バッチファイルは「Shift-JIS」で保存しています。

# C#でコマンド（rmtcmd.exe）の作成

```csharp:Class/Util/ClsWinRs.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Security;
using System.Collections.Concurrent;
using System.Collections.ObjectModel;
using rmtcmd.Class.Module;

namespace rmtcmd.Class.Util
{
  class ClsWinRs
  {
    private ClsLog _objLog = null;
    private String _strRemoteHost = "localhost";
    private String _strUsername = "";
    private String _strPassword = "";
    private String _strComSpec = null;
    private Boolean _blnIsStdOutCheck = false;
    private Boolean _blnIsLogWrite = false;
    private int _intExecMode = MdlConst.EXEC_MODE_CMD;
    private int _intVerbose = 0;

    public ClsWinRs()
    {
      _strComSpec = Environment.GetEnvironmentVariable("ComSpec");
    }

    public String strRemoteHost { get { return _strRemoteHost; } set { _strRemoteHost = value; } }
    public String strUsername { get { return _strUsername; } set { _strUsername = value; } }
    public String strPassword { get { return _strPassword; } set { _strPassword = value; } }
    public int intExecMode { get { return _intExecMode; } set { _intExecMode = value; } }
    public int intVerbose { get { return _intVerbose; } set { _intVerbose = value; } }
    public ClsLog objLog { get { return _objLog; } set { _objLog = value; } }
    public Boolean blnIsStdOutCheck { get { return _blnIsStdOutCheck; } set { _blnIsStdOutCheck = value; } }
    public Boolean blnIsLogWrite { get { return _blnIsLogWrite; } set { _blnIsLogWrite = value; } }

    public int Execute( String strCmd)
    {
      Runspace objRunspace = null;
      SecureString objSecurePasswd = new SecureString();
      WSManConnectionInfo objConnInfo = new WSManConnectionInfo();
      int intExitCode = MdlConst.LVL_DEBUG;
      if (null == _objLog) _blnIsLogWrite = false;
      try
      {
        objConnInfo.ComputerName = _strRemoteHost;
        _strPassword.ToCharArray().ToList().ForEach(p => objSecurePasswd.AppendChar(p));
        objConnInfo.Credential = new PSCredential(_strUsername, objSecurePasswd);
        objRunspace = RunspaceFactory.CreateRunspace(objConnInfo);
        objRunspace.Open();
        using (PowerShell objPshell = PowerShell.Create())
        {
          objPshell.Runspace = objRunspace;
          objPshell.AddScript(getInvokeCommand(strCmd));
          Collection<PSObject> objAdapts = objPshell.Invoke();
          foreach (PSObject objResult in objAdapts)
          {
            if (_blnIsLogWrite) _objLog.Writeln(MdlConst.LVL_NONE, objResult.ToString());
          }
        }
        using (PowerShell objPshell = PowerShell.Create())
        {
          objPshell.Runspace = objRunspace;
          objPshell.AddScript("$LASTEXITCODE");
          Collection<PSObject> objAdapts = objPshell.Invoke();
          foreach (PSObject objResult in objAdapts)
          {
            String strRetCode = objResult.ToString();
            if (!String.IsNullOrEmpty(strRetCode) && MdlUtil.IsNumeric(strRetCode)) intExitCode = int.Parse(strRetCode);
            if (_blnIsLogWrite && _intVerbose > 3) _objLog.Writeln(MdlConst.LVL_NONE, objResult.ToString());
          }
        }
      }
      catch (Exception objExcptn)
      {
        _objLog.Writeln(MdlConst.LVL_NONE, "EXCEPTION : " + objExcptn.Message);
      }
      finally
      {
        if (null != objRunspace)
        {
          objRunspace.Close();
          objRunspace.Dispose();
        }
      }
      return intExitCode;
    }

    public String getInvokeCommand(String strRemoteCommand)
    {
      byte[] bytes = null;
      String strInvokeCommand = strRemoteCommand;
      switch (_intExecMode)
      {
        case MdlConst.EXEC_MODE_CMD:
          strInvokeCommand = _strComSpec + " /c " + strRemoteCommand.Trim() + " 2>&1";
          break;
        case MdlConst.EXEC_MODE_EC:
          bytes = System.Convert.FromBase64String(strRemoteCommand.Trim());
          strRemoteCommand = System.Text.Encoding.GetEncoding(932).GetString(bytes);
          strInvokeCommand = strRemoteCommand.Trim();
          break;
        case MdlConst.EXEC_MODE_ECMD:
          bytes = System.Convert.FromBase64String(strRemoteCommand.Trim());
          strRemoteCommand = System.Text.Encoding.GetEncoding(932).GetString(bytes);
          strInvokeCommand = _strComSpec + " /c " + strRemoteCommand.Trim() + " 2>&1";
          break;
        case MdlConst.EXEC_MODE_PS:
          bytes = System.Text.Encoding.GetEncoding("UNICODE").GetBytes(strRemoteCommand.Trim());
          String strEncoded = System.Convert.ToBase64String(bytes);
          strInvokeCommand = _strComSpec + " /c powershell -encodedCommand \"" + strEncoded + "\" 2>&1";
          break;
        case MdlConst.EXEC_MODE_ES:
          strInvokeCommand = _strComSpec + " /c powershell -encodedCommand \"" + strRemoteCommand.Trim() + "\" 2>&1";
          break;
      }
      if (_blnIsLogWrite && _intVerbose > 2) _objLog.Writeln(MdlConst.LVL_NONE, "== INVOKE CMD  = " + strInvokeCommand);
      return strInvokeCommand;
    }
  }
}
```

# Serverspecで実行してみる

「Serverspecで使うWinRMクライアントをInvoke-commandに置き換えてみた」で作成・修正した状態から変更を加えていきます。

「WinRm.ps1」から「rmtcmd.exe」へ変更します。

```ruby:C：\serverspec\sample\spec\spec_helper.rb
require 'serverspec'
require 'yaml'
require 'CustomWinRm'
params = {}
params['hostname'] = ENV['TARGET_HOST']
params['run_env'] = "development"
puts ""
puts "##################################################"
puts "HOST: #{params['hostname']} : #{params['run_env']}"
puts "##################################################"
properties = YAML.load_file("env/properties.#{params['run_env']}.yml")
set_property = properties
set :backend, :winrm
set :os, :family => 'windows'
Specinfra.configuration.customwinrm = true
winrm = CustomWinRm.new({"user" => "#{properties[:winrm_user]}", "pass" => "#{properties[:winrm_pass]}", "host" => "#{ENV['TARGET_HOST']}", "winrm" => ".\\lib\\rmtcmd.exe"})
Specinfra.configuration.winrm = winrm
```

実行してみます。

```serverspec
PS C:\serverspec\sample> rake

##################################################
HOST: REMOTEHOST : development
##################################################

Command "ipconfig /all | Select-String ノード"
  stdout
    should match /ハイブリッド/

File "C:\serverspec\test.cmd"
  should be file
  should contain "あいうえお"
  should not contain "かきくけこ"
  content
    should match /あいうえお/
  content
    should not match /かきくけこ/

Finished in 9.51 seconds (files took 1.34 seconds to load)
6 examples, 0 failures

PS C:\serverspec\sample>
```

デフォルトのWinRMを使うよりもはやくなりました。
672ケース（168ケースx4サーバー）のテスト所要時間は次の通りです。
- デフォルトのWinRM：427秒
- Invoke-Command：541秒
- C#自作コマンド：301秒