This gem allows the use of a long-lived manager to which Ruby can send PowerShell invocations and receive the exection output. This reduces the overhead time to execute PowerShell commands from seconds to milliseconds because each execution does not need to spin up a PowerShell process, execute a single pipeline, and tear the process down.
The manager operates by instantiating a custom PowerShell host process to which Ruby can then send commands over an IO pipe— on Windows machines, named pipes, on Unix/Linux, Unix Domain Sockets.
Communication between Ruby and the PowerShell host process uses binary encoded strings with a 4-byte prefix indicating how long the message is. The length prefix is a Little Endian encoded 32-bit integer. The string being passed is always UTF-8.
Before a command string is sent to the PowerShell host process, a single 1-byte command identifier is sent—
0
to tell the process to exit, 1
to tell the process to execute the next incoming string.
The PowerShell code to be executed is always wrapped in the following for execution to standardize behavior inside the PowerShell host process:
$params = @{
Code = @'
#{powershell_code}
'@
TimeoutMilliseconds = #{timeout_ms}
WorkingDirectory = "#{working_dir}"
ExecEnvironmentVariables = #{exec_environment_variables}
}
Invoke-PowerShellUserCode @params
The code itself is placed inside a herestring and the timeout (integer), working directory (string), and environment variables (hash), if any, are passed as well.
The return from a Ruby command will always include:
stdout
from the output streams, as if using*>&1
to redirectexitcode
for the exit code of the execution; this will always be0
, unless an exit code is specified or an exception is thrown.stderr
will always be an empty array.errormessage
will be the exception message of any thrown exception during execution.native_stdout
will always be nil.
Because PowerShell does not halt execution when an error is encountered, only when an terminating exception is thrown, the manager also continues to execute until it encounters a terminating exception when executing commands.
This means that Write-Error
messages will go to the stdout log but will not cause a change in the exitcode
or populate the errormessage
field.
Using Throw
or any other method of generating a terminating exception will set the exitcode
to 1
and populate the errormessage
field.
Until told otherwise, or they break, or their parent process closes, the instantiated PowerShell host processes will remain alive and waiting for further commands. The significantly speeds up the execution of serialized commands, making continual interoperation between Ruby and PowerShell less complex for the developer leveraging this library.
In order to do this, the manager class has a class variable, @@instances
, which contains a hash of the PowerShell hosts:
- The key is the unique combination of options - path to the executable, flags, and additional options - passed to create the instance.
- The value is the current state of that instance of the PowerShell host process.
If you attempt to instantiate an instance of the manager using the instance
method, it will first look to see if the specified manager and host process are already built and alive - if the manager instance does not exist or the host process is dead, then it will spin up a new host process.
In test executions, standup of an instance takes around 1.5 seconds - accessing a pre-existing instance takes thousandths of a second.
The manager and PowerShell host process are designed to be used single-threadedly with the PowerShell host expecting a single command and returning a single output at a time. It does not at this time have additional guarding against being sent commands by multiple processes, but since the handles are unique IDs, this should not happen in practice.