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