1 # Copyright (c) 2020 Ansible Project
2 # Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
3
4 #AnsibleRequires -CSharpUtil .Process
5
Resolve-ExecutablePath()6 Function Resolve-ExecutablePath {
7 <#
8 .SYNOPSIS
9 Tries to resolve the file path to a valid executable.
10 #>
11 [CmdletBinding()]
12 param (
13 [Parameter(Mandatory=$true)]
14 $FilePath,
15
16 [String]
17 $WorkingDirectory
18 )
19
20 # Ensure the path has an extension set, default to .exe
21 if (-not [IO.Path]::HasExtension($FilePath)) {
22 $FilePath = "$FilePath.exe"
23 }
24
25 # See the if path is resolvable using the normal PATH logic. Also resolves absolute paths and relative paths if
26 # they exist.
27 $command = Get-Command -Name $FilePath -CommandType Application -ErrorAction SilentlyContinue
28 if ($command) {
29 $command.Path
30 return
31 }
32
33 # If -WorkingDirectory is specified, check if the path is relative to that
34 if ($WorkingDirectory) {
35 $file = $PSCmdlet.GetUnresolvedProviderPathFromPSPath((Join-Path -Path $WorkingDirectory -ChildPath $FilePath))
36 if (Test-Path -LiteralPath $file) {
37 $file
38 return
39 }
40 }
41
42 # Just hope for the best and use whatever was provided.
43 $FilePath
44 }
45
ConvertFrom-EscapedArgumentnull46 Function ConvertFrom-EscapedArgument {
47 <#
48 .SYNOPSIS
49 Extract individual arguments from a command line string.
50
51 .PARAMETER InputObject
52 The command line string to extract the arguments from.
53 #>
54 [CmdletBinding()]
55 [OutputType([String])]
56 param (
57 [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
58 [String[]]
59 $InputObject
60 )
61
62 process {
63 foreach ($command in $InputObject) {
64 # CommandLineToArgv treats \" slightly different for the first argument for some reason (probably because
65 # it expects it to be a filepath). We add a dummy value to ensure it splits the args in the same way as
66 # each other and just discard that first arg in the output.
67 $command = "a $command"
68 [Ansible.Windows.Process.ProcessUtil]::CommandLineToArgv($command) | Select-Object -Skip 1
69 }
70 }
71 }
72
ConvertTo-EscapedArgument()73 Function ConvertTo-EscapedArgument {
74 <#
75 .SYNOPSIS
76 Escapes an argument value so it can be used in a call to CreateProcess.
77
78 .PARAMETER InputObject
79 The argument(s) to escape.
80 #>
81 [CmdletBinding()]
82 [OutputType([String])]
83 param (
84 [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
85 [AllowEmptyString()]
86 [AllowNull()]
87 [String[]]
88 $InputObject
89 )
90
91 process {
92 if (-not $InputObject) {
93 return '""'
94 }
95
96 foreach ($argument in $InputObject) {
97 if (-not $argument) {
98 return '""'
99 }
100 elseif ($argument -notmatch '[\s"]') {
101 return $argument
102 }
103
104 # Replace any double quotes in an argument with '\"'
105 $argument = $argument -replace '"', '\"'
106
107 # Double up on any '\' chars that preceded '\"'
108 $argument = $argument -replace '(\\+)\\"', '$1$1\"'
109
110 # Double up '\' at the end of the argument so it doesn't escape end quote.
111 $argument = $argument -replace '(\\+)$', '$1$1'
112
113 # Finally wrap the entire argument in double quotes now we've escaped the double quotes within
114 '"{0}"' -f $argument
115 }
116 }
117 }
118
Start-AnsibleWindowsProcessnull119 Function Start-AnsibleWindowsProcess {
120 <#
121 .SYNOPSIS
122 Start a process and wait for it to finish.
123
124 .PARAMETER FilePath
125 The file to execute.
126
127 .PARAMETER ArgumentList
128 Arguments to execute, these will be escaped so the literal value is used.
129
130 .PARAMETER CommandLine
131 The raw command line to call with CreateProcess. These values are not escaped for you so use at your own risk.
132
133 .PARAMETER WorkingDirectory
134 The working directory to set on the new process, defaults to the current working dir.
135
136 .PARAMETER Environment
137 Override the environment to set for the new process, if not set then the current environment will be used.
138
139 .PARAMETER InputObject
140 A string or byte[] array to send to the process' stdin when it has started.
141
142 .PARAMETER OutputEncodingOverride
143 The encoding name to use when reading the stdout/stderr of the process. Defaults to utf-8 if not set.
144
145 .PARAMETER WaitChildren
146 Whether to wait for any child process spawned to finish before returning. This only works on Windows hosts on
147 Server 2012/Windows 8 or newer.
148
149 .OUTPUTS
150 [PSCustomObject]@{
151 Command = The final command used to start the process
152 Stdout = The stdout of the process
153 Stderr = The stderr of the process
154 ExitCode = The return code from the process
155 }
156 #>
157 [CmdletBinding(DefaultParameterSetName='ArgumentList')]
158 [OutputType('Ansible.Windows.Process.Info')]
159 param (
160 [Parameter(Mandatory=$true, ParameterSetName='ArgumentList')]
161 [Parameter(ParameterSetName='CommandLine')]
162 [String]
163 $FilePath,
164
165 [Parameter(ParameterSetName='ArgumentList')]
166 [String[]]
167 $ArgumentList,
168
169 [Parameter(Mandatory=$true, ParameterSetName='CommandLine')]
170 [String]
171 $CommandLine,
172
173 [String]
174 # Default to the PowerShell location and not the process location.
175 $WorkingDirectory = (Get-Location -PSProvider FileSystem),
176
177 [Collections.IDictionary]
178 $Environment,
179
180 [Object]
181 $InputObject,
182
183 [String]
184 [Alias('OutputEncoding')]
185 $OutputEncodingOverride,
186
187 [Switch]
188 $WaitChildren
189 )
190
191 if ($WorkingDirectory) {
192 if (-not (Test-Path -LiteralPath $WorkingDirectory)) {
193 Write-Error -Message "Could not find specified -WorkingDirectory '$WorkingDirectory'"
194 return
195 }
196 }
197
198 if ($FilePath) {
199 $applicationName = $FilePath
200 }
201 else {
202 # If -FilePath is not set then -CommandLine must have been used. Select the path based on the first entry.
203 $applicationName = [Ansible.Windows.Process.ProcessUtil]::CommandLineToArgv($CommandLine)[0]
204 }
205 $applicationName = Resolve-ExecutablePath -FilePath $applicationName -WorkingDirectory $WorkingDirectory
206
207 # When -ArgumentList is used, we need to escape each argument, including the FilePath to build our CommandLine.
208 if ($PSCmdlet.ParameterSetName -eq 'ArgumentList') {
209 $CommandLine = ConvertTo-EscapedArgument -InputObject $applicationName
210 if ($ArgumentList.Count) {
211 $escapedArguments = @($ArgumentList | ConvertTo-EscapedArgument)
212 $CommandLine += " $($escapedArguments -join ' ')"
213 }
214 }
215
216 $stdin = $null
217 if ($InputObject) {
218 if ($InputObject -is [byte[]]) {
219 $stdin = $InputObject
220 }
221 elseif ($InputObject -is [string]) {
222 $stdin = [Text.Encoding]::UTF8.GetBytes($InputObject)
223 }
224 else {
225 Write-Error -Message "InputObject must be a string or byte[]"
226 return
227 }
228 }
229
230 $res = [Ansible.Windows.Process.ProcessUtil]::CreateProcess($applicationName, $CommandLine, $WorkingDirectory,
231 $Environment, $stdin, $OutputEncodingOverride, $WaitChildren)
232
233 [PSCustomObject]@{
234 PSTypeName = 'Ansible.Windows.Process.Info'
235 Command = $CommandLine
236 Stdout = $res.StandardOut
237 Stderr = $res.StandardError
238 ExitCode = $res.ExitCode
239 }
240 }
241
242 $export_members = @{
=()243 Function = 'ConvertFrom-EscapedArgument', 'ConvertTo-EscapedArgument', 'Resolve-ExecutablePath', 'Start-AnsibleWindowsProcess'
244 }
245 Export-ModuleMember @export_members
246