Skip to content

Commit 1a9ad64

Browse files
(FM-8465) Document architecture
1 parent a6cf092 commit 1a9ad64

File tree

2 files changed

+68
-2
lines changed

2 files changed

+68
-2
lines changed

DESIGN.md

+68-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,70 @@
11
# PowerShell Manager: Design and Architecture
22

3-
<!-- TODO: Fill this out! -->
4-
<!-- NOTE: Not multi-thread safe, executions against the same manager in parallel can get _weird_ -->
3+
This gem allows the use of a long-lived manager to which Ruby can send PowerShell invocations and receive the exection output.
4+
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.
5+
6+
The manager operates by instantiating a custom PowerShell host process to which Ruby can then send commands over an IO pipe—
7+
on Windows machines, named pipes, on Unix/Linux, Unix Domain Sockets.
8+
9+
## Communication Between Ruby and PowerShell Host Process
10+
11+
Communication between Ruby and the PowerShell host process uses binary encoded strings with a [4-byte prefix indicating how long the message is](https://en.wikipedia.org/wiki/Type-length-value).
12+
The length prefix is a Little Endian encoded 32-bit integer.
13+
The string being passed is always UTF-8.
14+
15+
Before a command string is sent to the PowerShell host process, a single 1-byte command identifier is sent—
16+
`0` to tell the process to exit, `1` to tell the process to execute the next incoming string.
17+
18+
The PowerShell code to be executed is always wrapped in the following for execution to standardize behavior inside the PowerShell host process:
19+
20+
```powershell
21+
$params = @{
22+
Code = @'
23+
#{powershell_code}
24+
'@
25+
TimeoutMilliseconds = #{timeout_ms}
26+
WorkingDirectory = "#{working_dir}"
27+
ExecEnvironmentVariables = #{exec_environment_variables}
28+
}
29+
30+
Invoke-PowerShellUserCode @params
31+
```
32+
33+
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.
34+
35+
![Diagram of communication flow for execution between Ruby and PowerShell manager](./design-comms.png)
36+
37+
### Output
38+
39+
The return from a Ruby command will always include:
40+
41+
+ `stdout` from the output streams, as if using `*>&1` to redirect
42+
+ `exitcode` for the exit code of the execution; this will always be `0`, unless an exit code is specified or an exception is _thrown_.
43+
+ `stderr` will always be an empty array.
44+
+ `errormessage` will be the exception message of any thrown exception during execution.
45+
+ `native_stdout` will always be nil.
46+
47+
#### Error Handling
48+
49+
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.
50+
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.
51+
Using `Throw` or any other method of generating a terminating exception _will_ set the `exitcode` to `1` and populate the `errormessage` field.
52+
53+
## Multiple PowerShell Host Processes
54+
55+
Until told otherwise, or they break, or their parent process closes, the instantiated PowerShell host processes will remain alive and waiting for further commands.
56+
The significantly speeds up the execution of serialized commands, making continual interoperation between Ruby and PowerShell less complex for the developer leveraging this library.
57+
58+
In order to do this, the manager class has a class variable, `@@instances`, which contains a hash of the PowerShell hosts:
59+
60+
+ The key is the unique combination of options - path to the executable, flags, and additional options - passed to create the instance.
61+
+ The value is the current state of that instance of the PowerShell host process.
62+
63+
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.
64+
65+
In test executions, standup of an instance takes around 1.5 seconds - accessing a pre-existing instance takes thousandths of a second.
66+
67+
## Multithreading
68+
69+
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.
70+
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.

design-comms.png

47.3 KB
Loading

0 commit comments

Comments
 (0)