diff --git a/docs/learning-powershell/create-powershell-scripts.md b/docs/learning-powershell/create-powershell-scripts.md index dced379cb9d..5f93eb97ab3 100644 --- a/docs/learning-powershell/create-powershell-scripts.md +++ b/docs/learning-powershell/create-powershell-scripts.md @@ -62,4 +62,4 @@ else $IP | Where-Object {$_ -ne '127.0.0.1'} ``` -[run-ps]:https://windowsitpro.com/powershell/running-powershell-scripts-easy-1-2-3 +[run-ps]:https://www.itprotoday.com/powershell/running-powershell-scripts-easy-1-2-3 diff --git a/docs/learning-powershell/powershell-beginners-guide.md b/docs/learning-powershell/powershell-beginners-guide.md index 8564dc0822a..de9e79f9729 100644 --- a/docs/learning-powershell/powershell-beginners-guide.md +++ b/docs/learning-powershell/powershell-beginners-guide.md @@ -334,7 +334,7 @@ For more details, see [Create and Run PowerShell Script Guide][create-run-script [create-ps-module]:https://www.business.com/articles/powershell-modules/ [remoting]:https://channel9.msdn.com/Series/GetStartedPowerShell3/06 [in-depth]: https://channel9.msdn.com/events/MMS/2012/SV-B406 -[remote-mgmt]:https://windowsitpro.com/powershell/powershell-basics-remote-management +[remote-mgmt]:https://www.itprotoday.com/powershell/powershell-basics-remote-management [remote-commands]:https://docs.microsoft.com/powershell/scripting/core-powershell/running-remote-commands?view=powershell-6 [examples]:https://examples.oreilly.com/9780596528492/ [examples-ps-module]:https://msdn.microsoft.com/library/dd878340%28v=vs.85%29.aspx diff --git a/test/powershell/Host/ConsoleHost.Tests.ps1 b/test/powershell/Host/ConsoleHost.Tests.ps1 index 3b41812dec9..4af135e4983 100644 --- a/test/powershell/Host/ConsoleHost.Tests.ps1 +++ b/test/powershell/Host/ConsoleHost.Tests.ps1 @@ -119,6 +119,7 @@ Describe "ConsoleHost unit tests" -tags "Feature" { { & $powershell -input blah -comm { $input } } | Should -Throw -ErrorId "IncorrectValueForFormatParameter" } } + Context "CommandLine" { It "simple -args" { & $powershell -noprofile { $args[0] } -args "hello world" | Should -Be "hello world" @@ -611,6 +612,26 @@ foo } } } + + Context "CustomPipeName startup tests" { + + It "Should create pipe file if CustomPipeName is specified" { + $pipeName = [System.IO.Path]::GetRandomFileName() + $pipePath = Get-PipePath $pipeName + + # The pipePath should be created by the time the -Command is executed. + & $powershell -CustomPipeName $pipeName -Command "Test-Path '$pipePath'" | Should -BeTrue + } + + It "Should throw if CustomPipeName is too long on Linux or macOS" -Skip:($IsWindows) { + # Generate a string that is larger than the max pipe name length. + $longPipeName = [string]::new("A", 200) + + "`$pid" | & $powershell -CustomPipeName $longPipeName -c - + # 64 is the ExitCode for BadCommandLineParameter + $LASTEXITCODE | Should -Be 64 + } + } } Describe "WindowStyle argument" -Tag Feature { diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/Enter-PSHostProcess.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/Enter-PSHostProcess.Tests.ps1 index 13413bfb4bf..b74f1ba345f 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Core/Enter-PSHostProcess.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/Enter-PSHostProcess.Tests.ps1 @@ -1,134 +1,181 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +$powershell = Join-Path -Path $PsHome -ChildPath "pwsh" + +function Wait-JobPid { + param ( + $Job + ) + + # This is to prevent hanging in the test. + $startTime = [DateTime]::Now + $TimeoutInMilliseconds = 10000 + + # This will receive the pid of the Job process and nothing more since that was the only thing written to the pipeline. + do { + Start-Sleep -Seconds 1 + $pwshId = Receive-Job $Job + + if (([DateTime]::Now - $startTime).TotalMilliseconds -gt $timeoutInMilliseconds) { + throw "Unable to receive PowerShell process id." + } + } while (!$pwshId) + + $pwshId +} + +# Executes the Enter/Exit PSHostProcess script that returns the pid of the process that's started. +function Invoke-PSHostProcessScript { + param ( + [string] $ArgumentString, + [int] $Id, + [int] $Retry = 5 # Default retry of 5 times + ) + + $sb = { + # use $i as an incrementally growing pause based on the attempt number + # so that it's more likely to succeed. + $commandStr = @' +Start-Sleep -Seconds {0} +Enter-PSHostProcess {1} -ErrorAction Stop +$pid +Exit-PSHostProcess +'@ -f $i, $ArgumentString + + ($commandStr | & $powershell -c -) -eq $Id + } + + $result = $false + $failures = 0 + foreach ($i in 1..$Retry) { + if ($sb.Invoke()) { + $result = $true + break + } + + $failures++ + } + + if($failures) { + Write-Warning "Enter-PSHostProcess script failed $i out of $Retry times." + } + + $result +} + Describe "Enter-PSHostProcess tests" -Tag Feature { Context "By Process Id" { - BeforeAll { - $params = @{ - FilePath = "pwsh" - PassThru = $true - RedirectStandardOutput = "TestDrive:\pwsh_out.log" - RedirectStandardError = "TestDrive:\pwsh_err.log" - } - $pwsh = Start-Process @params - - if ($IsWindows) { - $params = @{ - FilePath = "powershell" - PassThru = $true - RedirectStandardOutput = "TestDrive:\powershell_out.log" - RedirectStandardError = "TestDrive:\powershell_err.log" + + BeforeEach { + # Start a normal job where the first thing it does is return $pid. After that, spin forever. + # We will use this job as the target process for Enter-PSHostProcess + $pwshJob = Start-Job { + $pid + while ($true) { + Start-Sleep -Seconds 30 | Out-Null } - $powershell = Start-Process @params } + $pwshId = Wait-JobPid $pwshJob } - AfterAll { - $pwsh | Stop-Process - - if ($IsWindows) { - $powershell | Stop-Process - } + AfterEach { + $pwshJob | Stop-Job -PassThru | Remove-Job } - It "Can enter and exit another PSHost" -Pending:$true { - Wait-UntilTrue { (Get-PSHostProcessInfo -Id $pwsh.Id) -ne $null } + It "Can enter, exit, and re-enter another PSHost" { + Wait-UntilTrue { [bool](Get-PSHostProcessInfo -Id $pwshId) } - "Enter-PSHostProcess -Id $($pwsh.Id) -ErrorAction Stop - `$pid - Exit-PSHostProcess" | pwsh -c - | Should -Be $pwsh.Id + # This will enter and exit another process + Invoke-PSHostProcessScript -ArgumentString "-Id $pwshId" -Id $pwshId | + Should -BeTrue -Because "The script was able to enter another process and grab the pid of '$pwshId'." + + # Re-enter and exit the other process + Invoke-PSHostProcessScript -ArgumentString "-Id $pwshId" -Id $pwshId | + Should -BeTrue -Because "The script was able to re-enter another process and grab the pid of '$pwshId'." } - It "Can enter and exit another Windows PowerShell PSHost" -Skip:(!$IsWindows) { - Wait-UntilTrue { (Get-PSHostProcessInfo -Id $powershell.Id) -ne $null } + It "Can enter, exit, and re-enter another Windows PowerShell PSHost" -Skip:(!$IsWindows) { + # Start a Windows PowerShell job where the first thing it does is return $pid. After that, spin forever. + # We will use this job as the target process for Enter-PSHostProcess + $powershellJob = Start-Job -PSVersion 5.1 { + $pid + while ($true) { + Start-Sleep -Seconds 30 | Out-Null + } + } - "Enter-PSHostProcess -Id $($powershell.Id) -ErrorAction Stop - `$pid - Exit-PSHostProcess" | pwsh -c - | Should -Be $powershell.Id + $powershellId = Wait-JobPid $powershellJob + + try { + Wait-UntilTrue { [bool](Get-PSHostProcessInfo -Id $powershellId) } + + # This will enter and exit another process + Invoke-PSHostProcessScript -ArgumentString "-Id $powershellId" -Id $powershellId | + Should -BeTrue -Because "The script was able to enter another process and grab the pid of '$powershellId'." + + # Re-enter and exit the other process + Invoke-PSHostProcessScript -ArgumentString "-Id $powershellId" -Id $powershellId | + Should -BeTrue -Because "The script was able to re-enter another process and grab the pid of '$powershellId'." + + } finally { + $powershellJob | Stop-Job -PassThru | Remove-Job + } } - It "Can enter using NamedPipeConnectionInfo" -Pending:$true { + It "Can enter using NamedPipeConnectionInfo" { try { - $npInfo = [System.Management.Automation.Runspaces.NamedPipeConnectionInfo]::new($pwsh.Id) + Wait-UntilTrue { [bool](Get-PSHostProcessInfo -Id $pwshId) } + + $npInfo = [System.Management.Automation.Runspaces.NamedPipeConnectionInfo]::new($pwshId) $rs = [runspacefactory]::CreateRunspace($npInfo) $rs.Open() $ps = [powershell]::Create() $ps.Runspace = $rs - $ps.AddScript('$pid').Invoke() | Should -Be $pwsh.Id + $ps.AddScript('$pid').Invoke() | Should -Be $pwshId } finally { $rs.Dispose() + $ps.Dispose() } } } Context "By CustomPipeName" { - BeforeAll { - # Helper function to get the correct path for the named pipe. - function Get-PipePath { - param ( - $PipeName - ) - if ($IsWindows) { - return "\\.\pipe\$PipeName" - } - "$([System.IO.Path]::GetTempPath())CoreFxPipe_$PipeName" - } + It "Can enter, exit, and re-enter using CustomPipeName" { $pipeName = [System.IO.Path]::GetRandomFileName() - $params = @{ - FilePath = "pwsh" - ArgumentList = @("-CustomPipeName",$pipeName) - PassThru = $true - RedirectStandardOutput = "TestDrive:\pwsh_out.log" - RedirectStandardError = "TestDrive:\pwsh_err.log" - } - $pwsh = Start-Process @params - $pipePath = Get-PipePath -PipeName $pipeName - } - AfterAll { - $pwsh | Stop-Process - } + # Start a job where the first thing it does is set the custom pipe name, then return $pid. + # After that, spin forever. + # We will use this job as the target process for Enter-PSHostProcess + $pwshJob = Start-Job -ArgumentList $pipeName { + [System.Management.Automation.Remoting.RemoteSessionNamedPipeServer]::CreateCustomNamedPipeServer($args[0]) + $pid + while ($true) { Start-Sleep -Seconds 30 | Out-Null } + } - It "Can enter using CustomPipeName" -Pending:$true { - Wait-UntilTrue { Test-Path $pipePath } + $pwshId = Wait-JobPid $pwshJob - "Enter-PSHostProcess -CustomPipeName $pipeName -ErrorAction Stop - `$pid - Exit-PSHostProcess" | pwsh -c - | Should -Be $pwsh.Id - } + try { + Wait-UntilTrue { Test-Path $pipePath } - It "Can enter, exit, and re-enter using CustomPipeName" -Pending:$true { - Wait-UntilTrue { Test-Path $pipePath } + # This will enter and exit another process + Invoke-PSHostProcessScript -ArgumentString "-CustomPipeName $pipeName" -Id $pwshId | + Should -BeTrue -Because "The script was able to enter another process and grab the pipe of '$pipeName'." - "Enter-PSHostProcess -CustomPipeName $pipeName -ErrorAction Stop - `$pid - Exit-PSHostProcess" | pwsh -c - | Should -Be $pwsh.Id + # Re-enter and exit the other process + Invoke-PSHostProcessScript -ArgumentString "-CustomPipeName $pipeName" -Id $pwshId | + Should -BeTrue -Because "The script was able to re-enter another process and grab the pipe of '$pipeName'." - "Enter-PSHostProcess -CustomPipeName $pipeName -ErrorAction Stop - `$pid - Exit-PSHostProcess" | pwsh -c - | Should -Be $pwsh.Id + } finally { + $pwshJob | Stop-Job -PassThru | Remove-Job + } } It "Should throw if CustomPipeName does not exist" { - Wait-UntilTrue { Test-Path $pipePath } - { Enter-PSHostProcess -CustomPipeName badpipename } | Should -Throw -ExpectedMessage "No named pipe was found with CustomPipeName: badpipename." } - - It "Should throw if CustomPipeName is too long on Linux or macOS" { - $longPipeName = "DoggoipsumwaggywagssmolborkingdoggowithalongsnootforpatsdoingmeafrightenporgoYapperporgolongwatershoobcloudsbigolpupperlengthboy" - - if (!$IsWindows) { - "`$pid" | pwsh -CustomPipeName $longPipeName -c - - # 64 is the ExitCode for BadCommandLineParameter - $LASTEXITCODE | Should -Be 64 - } else { - "`$pid" | pwsh -CustomPipeName $longPipeName -c - - $LASTEXITCODE | Should -Be 0 - } - } } } diff --git a/test/tools/Modules/HelpersRemoting/HelpersRemoting.psd1 b/test/tools/Modules/HelpersRemoting/HelpersRemoting.psd1 index 80b531f8886..b2537d22289 100644 --- a/test/tools/Modules/HelpersRemoting/HelpersRemoting.psd1 +++ b/test/tools/Modules/HelpersRemoting/HelpersRemoting.psd1 @@ -16,6 +16,6 @@ Copyright = 'Copyright (c) Microsoft Corporation. All rights reserved.' Description = 'Temporary module for remoting tests' -FunctionsToExport = 'New-RemoteRunspace', 'New-RemoteSession', 'Enter-RemoteSession', 'Invoke-RemoteCommand', 'Connect-RemoteSession', 'New-RemoteRunspacePool' +FunctionsToExport = 'New-RemoteRunspace', 'New-RemoteSession', 'Enter-RemoteSession', 'Invoke-RemoteCommand', 'Connect-RemoteSession', 'New-RemoteRunspacePool', 'Get-PipePath' } diff --git a/test/tools/Modules/HelpersRemoting/HelpersRemoting.psm1 b/test/tools/Modules/HelpersRemoting/HelpersRemoting.psm1 index 67f3056df33..6bf0728806c 100644 --- a/test/tools/Modules/HelpersRemoting/HelpersRemoting.psm1 +++ b/test/tools/Modules/HelpersRemoting/HelpersRemoting.psm1 @@ -248,3 +248,13 @@ function Connect-RemoteSession $parameters = CreateParameters -ComputerName $ComputerName -Name $Name -Session $Session -ConfigurationName $ConfigurationName Connect-PSSession @parameters } + +function Get-PipePath { + param ( + $PipeName + ) + if ($IsWindows) { + return "\\.\pipe\$PipeName" + } + "$([System.IO.Path]::GetTempPath())CoreFxPipe_$PipeName" +}