Skip to content

Commit 5adbfed

Browse files
authored
[PowerShell SDK] Add Invoke-VstsProcess (#978)
* Update package-lock * Add Invoke-Process function * Add Invoke-Process to members list * Fix $LastExitCode * Revert VstsTaskSdk crlf changes * Revert "refactor: remove Q library from Powershell SDK in favor of native promises (#944)" This reverts commit 4099e05. * Fix proc exit code * Update Invoke-Process * Fix SupportsWorkingDirectory test * Remove encoding param, update encoding test * Fix path in test * update loc resources * Revert "Revert "refactor: remove Q library from Powershell SDK in favor of native promises (#944)"" This reverts commit a703a0a. * Gen doc * Update changelog
1 parent 0ec6b12 commit 5adbfed

9 files changed

+1161
-1403
lines changed

powershell/Docs/Commands.md

Lines changed: 842 additions & 806 deletions
Large diffs are not rendered by default.
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Invoke-VstsProcess
2+
[table of contents](../Commands.md#toc) | [brief](../Commands.md#invoke-vstsprocess)
3+
```
4+
NAME
5+
Invoke-VstsProcess
6+
7+
SYNOPSIS
8+
Executes an external program as a child process.
9+
10+
SYNTAX
11+
Invoke-VstsProcess [-FileName] <String> [[-Arguments] <String>] [[-WorkingDirectory] <String>]
12+
[[-StdOutPath] <String>] [[-StdErrPath] <String>] [-RequireExitCodeZero] [<CommonParameters>]
13+
14+
DESCRIPTION
15+
Executes an external program and waits for the process to exit.
16+
17+
After calling this command, the exit code of the process can be retrieved from the variable $LASTEXITCODE
18+
or from the pipe.
19+
20+
PARAMETERS
21+
-FileName <String>
22+
File name (path) of the program to execute.
23+
24+
Required? true
25+
Position? 1
26+
Default value
27+
Accept pipeline input? false
28+
Accept wildcard characters? false
29+
30+
-Arguments <String>
31+
Arguments to pass to the program.
32+
33+
Required? false
34+
Position? 2
35+
Default value
36+
Accept pipeline input? false
37+
Accept wildcard characters? false
38+
39+
-WorkingDirectory <String>
40+
41+
Required? false
42+
Position? 3
43+
Default value
44+
Accept pipeline input? false
45+
Accept wildcard characters? false
46+
47+
-StdOutPath <String>
48+
Path to a file to write the stdout of the process to.
49+
50+
Required? false
51+
Position? 4
52+
Default value
53+
Accept pipeline input? false
54+
Accept wildcard characters? false
55+
56+
-StdErrPath <String>
57+
Path to a file to write the stderr of the process to.
58+
59+
Required? false
60+
Position? 5
61+
Default value
62+
Accept pipeline input? false
63+
Accept wildcard characters? false
64+
65+
-RequireExitCodeZero [<SwitchParameter>]
66+
Indicates whether to write an error to the error pipeline if the exit code is not zero.
67+
68+
Required? false
69+
Position? named
70+
Default value False
71+
Accept pipeline input? false
72+
Accept wildcard characters? false
73+
74+
<CommonParameters>
75+
This cmdlet supports the common parameters: Verbose, Debug,
76+
ErrorAction, ErrorVariable, WarningAction, WarningVariable,
77+
OutBuffer, PipelineVariable, and OutVariable. For more information, see
78+
about_CommonParameters (https://go.microsoft.com/fwlink/?LinkID=113216).
79+
80+
OUTPUTS
81+
Exit code of the invoked process. Also available through the $LASTEXITCODE.
82+
83+
NOTES
84+
85+
To change output encoding, redirect stdout to file and then read the file with the desired encoding.
86+
```

powershell/Docs/ReleaseNotes.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Release Notes
22

3+
## 0.17.0
4+
5+
* Added `Invoke-VstsProcess` cmdlet (<https://github.com/microsoft/azure-pipelines-task-lib/pull/978>)
6+
37
## 0.16.0
48
* Replaced deprecated "sync-request" libraryr and Added new async methods for DownloadArchive
59

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[CmdletBinding()]
2+
param()
3+
4+
# Arrange.
5+
. $PSScriptRoot\..\lib\Initialize-Test.ps1
6+
$Global:LASTEXITCODE = 1
7+
8+
Invoke-VstsTaskScript -ScriptBlock {
9+
10+
# Act.
11+
$actualEC = Invoke-VstsProcess -FileName 'cmd.exe' -Arguments '/c echo test'
12+
13+
# Assert.
14+
Assert-AreEqual -Expected 0 -Actual $actualEC
15+
Assert-AreEqual 0 $LASTEXITCODE
16+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
[CmdletBinding()]
2+
param()
3+
4+
# Actually, Invoke-VstsProcess does not support encoding by itself, it just allows to get the output of the process in a file, and then you can use Get-Content to read the file with the encoding you want.
5+
6+
# Arrange.
7+
. $PSScriptRoot\..\lib\Initialize-Test.ps1
8+
Invoke-VstsTaskScript -ScriptBlock {
9+
$tempDirectory = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName())
10+
New-Item -Path $tempDirectory -ItemType Directory | ForEach-Object { $_.FullName }
11+
try {
12+
set-Content -LiteralPath $tempDirectory\Program.cs -Value @"
13+
namespace TestEncoding {
14+
public static class Program {
15+
public static void Main() {
16+
System.Text.Encoding encoding = System.Text.Encoding.Unicode;
17+
byte[] bytes = encoding.GetBytes("Hello world");
18+
using (System.IO.Stream stdout = System.Console.OpenStandardOutput()) {
19+
stdout.Write(bytes, 0, bytes.Length);
20+
stdout.Flush();
21+
}
22+
}
23+
}
24+
}
25+
"@
26+
Add-Type -LiteralPath $tempDirectory\Program.cs -OutputType ConsoleApplication -OutputAssembly $tempDirectory\TestEncoding.exe
27+
$originalEncoding = [System.Console]::OutputEncoding
28+
$variableSets = @(
29+
@{ Encoding = $null ; Expected = "H_e_l_l_o_ _w_o_r_l_d_" }
30+
@{ Encoding = 'unicode' ; Expected = "Hello world" }
31+
)
32+
foreach ($variableSet in $variableSets) {
33+
$stdOutPath = [System.IO.Path]::Combine($tempDirectory, [System.IO.Path]::GetRandomFileName())
34+
35+
# Act.
36+
Invoke-VstsProcess `
37+
-FileName $tempDirectory\TestEncoding.exe `
38+
-StdOutPath $stdOutPath `
39+
40+
if ($variableSet.Encoding) {
41+
$actual = Get-Content -LiteralPath $stdOutPath -Encoding $variableSet.Encoding
42+
}
43+
else {
44+
$actual = Get-Content -LiteralPath $stdOutPath -Raw
45+
}
46+
47+
# Assert.
48+
$actual = $actual.Replace("`0", "_") # Replace null characters with spaces in order for the string comparison to be accurate.
49+
Assert-AreEqual $variableSet.Expected $actual
50+
Assert-AreEqual $originalEncoding ([System.Console]::OutputEncoding)
51+
}
52+
}
53+
finally {
54+
Remove-Item $tempDirectory -Recurse
55+
}
56+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
[CmdletBinding()]
2+
param()
3+
4+
# Arrange.
5+
. $PSScriptRoot\..\lib\Initialize-Test.ps1
6+
Invoke-VstsTaskScript -ScriptBlock {
7+
$originalLocation = $PWD
8+
$tempDirectory = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName())
9+
New-Item -Path $tempDirectory -ItemType Directory | ForEach-Object { $_.FullName }
10+
try {
11+
Set-Location $env:TMP
12+
$variableSets = @(
13+
@{ Expected = [System.IO.Path]::GetTempPath().TrimEnd('\') ; Splat = @{ } }
14+
@{ Expected = [System.IO.Path]::GetTempPath().TrimEnd('\') ; Splat = @{ WorkingDirectory = [System.IO.Path]::GetTempPath() } }
15+
@{ Expected = $tempDirectory ; Splat = @{ WorkingDirectory = $tempDirectory } }
16+
)
17+
foreach ($variableSet in $variableSets) {
18+
$splat = $variableSet.Splat
19+
20+
$stdOutPath = [System.IO.Path]::Combine($tempDirectory, [System.IO.Path]::GetRandomFileName())
21+
22+
# Act.
23+
Invoke-VstsProcess `
24+
-FileName 'cmd.exe' `
25+
-Arguments '/c "CD"' `
26+
-StdOutPath $stdOutPath `
27+
@splat
28+
29+
$actual = Get-Content -LiteralPath $stdOutPath -Encoding UTF8
30+
31+
# Assert.
32+
Assert-AreEqual $variableSet.Expected $actual
33+
Assert-AreEqual ([System.IO.Path]::GetTempPath().TrimEnd('\')) (Get-Location).Path
34+
}
35+
} finally {
36+
Set-Location $originalLocation
37+
Remove-Item $tempDirectory -Recurse
38+
}
39+
}

powershell/VstsTaskSdk/ToolFunctions.ps1

Lines changed: 99 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ function Assert-Path {
3939

4040
if ($PathType -eq [Microsoft.PowerShell.Commands.TestPathType]::Any) {
4141
Write-Verbose "Asserting path exists: '$LiteralPath'"
42-
} else {
42+
}
43+
else {
4344
Write-Verbose "Asserting $("$PathType".ToLowerInvariant()) path exists: '$LiteralPath'"
4445
}
4546

@@ -75,7 +76,7 @@ This parameter not required for most scenarios. Indicates how to interpret the e
7576
.PARAMETER RequireExitCodeZero
7677
Indicates whether to write an error to the error pipeline if the exit code is not zero.
7778
#>
78-
function Invoke-Tool { # TODO: RENAME TO INVOKE-PROCESS?
79+
function Invoke-Tool {
7980
[CmdletBinding()]
8081
param(
8182
[ValidatePattern('^[^\r\n]*$')]
@@ -107,7 +108,8 @@ function Invoke-Tool { # TODO: RENAME TO INVOKE-PROCESS?
107108
Write-Host "##[command]""$FileName"" $Arguments"
108109
try {
109110
Invoke-Expression "& '$FileName' --% $Arguments"
110-
} catch [System.Management.Automation.Host.HostException] {
111+
}
112+
catch [System.Management.Automation.Host.HostException] {
111113
if ($IgnoreHostException -eq $False) {
112114
throw
113115
}
@@ -118,7 +120,8 @@ function Invoke-Tool { # TODO: RENAME TO INVOKE-PROCESS?
118120
if ($RequireExitCodeZero -and $LASTEXITCODE -ne 0) {
119121
Write-Error (Get-LocString -Key PSLIB_Process0ExitedWithCode1 -ArgumentList ([System.IO.Path]::GetFileName($FileName)), $LASTEXITCODE)
120122
}
121-
} finally {
123+
}
124+
finally {
122125
if ($originalEncoding) {
123126
[System.Console]::OutputEncoding = $originalEncoding
124127
}
@@ -130,3 +133,95 @@ function Invoke-Tool { # TODO: RENAME TO INVOKE-PROCESS?
130133
Trace-LeavingInvocation $MyInvocation
131134
}
132135
}
136+
137+
<#
138+
.SYNOPSIS
139+
Executes an external program as a child process.
140+
141+
.DESCRIPTION
142+
Executes an external program and waits for the process to exit.
143+
144+
After calling this command, the exit code of the process can be retrieved from the variable $LASTEXITCODE or from the pipe.
145+
146+
.PARAMETER FileName
147+
File name (path) of the program to execute.
148+
149+
.PARAMETER Arguments
150+
Arguments to pass to the program.
151+
152+
.PARAMETER StdOutPath
153+
Path to a file to write the stdout of the process to.
154+
155+
.PARAMETER StdErrPath
156+
Path to a file to write the stderr of the process to.
157+
158+
.PARAMETER RequireExitCodeZero
159+
Indicates whether to write an error to the error pipeline if the exit code is not zero.
160+
161+
.OUTPUTS
162+
Exit code of the invoked process. Also available through the $LASTEXITCODE.
163+
164+
.NOTES
165+
To change output encoding, redirect stdout to file and then read the file with the desired encoding.
166+
#>
167+
function Invoke-Process {
168+
[CmdletBinding()]
169+
param(
170+
[ValidatePattern('^[^\r\n]*$')]
171+
[Parameter(Mandatory = $true)]
172+
[string]$FileName,
173+
[ValidatePattern('^[^\r\n]*$')]
174+
[Parameter()]
175+
[string]$Arguments,
176+
[string]$WorkingDirectory,
177+
[string]$StdOutPath,
178+
[string]$StdErrPath,
179+
[switch]$RequireExitCodeZero
180+
)
181+
182+
Trace-EnteringInvocation $MyInvocation
183+
try {
184+
$FileName = $FileName.Replace('"', '').Replace("'", "''")
185+
Write-Host "##[command]""$FileName"" $Arguments"
186+
187+
$processOptions = @{
188+
FilePath = $FileName
189+
NoNewWindow = $true
190+
PassThru = $true
191+
}
192+
if ($Arguments) {
193+
$processOptions.Add("ArgumentList", $Arguments)
194+
}
195+
if ($WorkingDirectory) {
196+
$processOptions.Add("WorkingDirectory", $WorkingDirectory)
197+
}
198+
if ($StdOutPath) {
199+
$processOptions.Add("RedirectStandardOutput", $StdOutPath)
200+
}
201+
if ($StdErrPath) {
202+
$processOptions.Add("RedirectStandardError", $StdErrPath)
203+
}
204+
205+
# TODO: For some reason, -Wait is not working on agent.
206+
# Agent starts executing the System usage metrics and hangs the step forever.
207+
$proc = Start-Process @processOptions
208+
209+
# https://stackoverflow.com/a/23797762
210+
$null = $($proc.Handle)
211+
$proc.WaitForExit()
212+
213+
$procExitCode = $proc.ExitCode
214+
Write-Verbose "Exit code: $procExitCode"
215+
216+
if ($RequireExitCodeZero -and $procExitCode -ne 0) {
217+
Write-Error (Get-LocString -Key PSLIB_Process0ExitedWithCode1 -ArgumentList ([System.IO.Path]::GetFileName($FileName)), $procExitCode)
218+
}
219+
220+
$global:LASTEXITCODE = $procExitCode
221+
222+
return $procExitCode
223+
}
224+
finally {
225+
Trace-LeavingInvocation $MyInvocation
226+
}
227+
}

powershell/VstsTaskSdk/VstsTaskSdk.psm1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ Export-ModuleMember -Function @(
8383
'Assert-Agent'
8484
'Assert-Path'
8585
'Invoke-Tool'
86+
'Invoke-Process'
8687
# Trace functions.
8788
'Trace-EnteringInvocation'
8889
'Trace-LeavingInvocation'

0 commit comments

Comments
 (0)