Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.PowerShell.Commands.Internal
{
Expand All @@ -22,15 +23,24 @@ private static string StartProcess(
string stdin = "",
bool readStdout = true)
{
return StartProcess(tool, args, stdin, readStdout, out _);
}

private static string StartProcess(
string tool,
string args,
string stdin,
bool readStdout,
out int processId)
{
processId = -1;
ProcessStartInfo startInfo = new();
startInfo.UseShellExecute = false;
startInfo.RedirectStandardInput = true;
startInfo.RedirectStandardOutput = true;
startInfo.RedirectStandardOutput = readStdout;
startInfo.RedirectStandardError = true;
startInfo.FileName = tool;
startInfo.Arguments = args;
string stdout = string.Empty;

using (Process process = new())
{
process.StartInfo = startInfo;
Expand All @@ -44,19 +54,66 @@ private static string StartProcess(
return string.Empty;
}

process.StandardInput.Write(stdin);
process.StandardInput.Close();
processId = process.Id;

Task stdinTask = process.StandardInput.WriteAsync(stdin).ContinueWith(
_ =>
{
try
{
process.StandardInput.Close();
}
catch (InvalidOperationException)
{
// The process may have exited while stdin was being written.
}
},
CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
Task<string> stdoutTask = readStdout
? process.StandardOutput.ReadToEndAsync()
: Task.FromResult(string.Empty);
Task<string> stderrTask = process.StandardError.ReadToEndAsync();
Task exitTask = Task.Run(() => process.WaitForExit());
Task processTask = Task.WhenAll(stdinTask, stdoutTask, stderrTask, exitTask);

if (readStdout)
try
{
stdout = process.StandardOutput.ReadToEnd();
if (!processTask.Wait(250))
{
try
{
process.Kill(entireProcessTree: true);
}
catch (InvalidOperationException)
{
// The process exited between the timeout and Kill.
}

_ = process.WaitForExit(250);
try
{
_ = processTask.Wait(250);
}
catch (AggregateException)
{
// The terminated process can fault pending stream operations.
}

_clipboardSupported = false;
return string.Empty;
}
}
catch (AggregateException)
{
_clipboardSupported = false;
return string.Empty;
}

process.WaitForExit(250);
_clipboardSupported = process.ExitCode == 0;
return readStdout ? stdoutTask.GetAwaiter().GetResult() : string.Empty;
}

return stdout;
}

public static string GetText()
Expand Down
19 changes: 12 additions & 7 deletions test/powershell/Host/ConsoleHost.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -1191,14 +1191,19 @@ Describe 'Pwsh startup and PATH' -Tag CI {
$path = & $pwsh -noprofile -command '$env:PATH'

$ExpectedPath = $PSHOME
if ($PSHOME.StartsWith("$env:ProgramFiles\WindowsApps\Microsoft.PowerShell")) {
# MSIX installed PowerShell uses the stable path instead of PSHOME.
$ExpectedPath = $PSHOME.Replace($env:ProgramFiles, "$env:LOCALAPPDATA\Microsoft")
$isMsixInstallation = $false
if ($IsWindows) {
$utilsType = [System.Management.Automation.PSObject].Assembly.GetType('System.Management.Automation.Utils')
$isMsixInstallationMethod = $utilsType.GetMethod(
'IsMSIXInstallation',
[System.Reflection.BindingFlags]'NonPublic, Static')
$invokeArguments = [object[]]@($null)
$isMsixInstallation = $isMsixInstallationMethod.Invoke($null, $invokeArguments)
}

# Remove the version-specific part from the expected path.
$version = $PSVersionTable.PSVersion
$verStrRegex = "$($version.Major)\.$($version.Minor)\.$($version.Build).+__"
$ExpectedPath = $ExpectedPath -replace $verStrRegex, ''
if ($isMsixInstallation) {
# MSIX installed PowerShell uses the stable path instead of PSHOME.
$ExpectedPath = Join-Path $env:LOCALAPPDATA "Microsoft\WindowsApps\$($invokeArguments[0])"
}

$path | Should -BeExactly ($ExpectedPath + [System.IO.Path]::PathSeparator)
Expand Down
3 changes: 2 additions & 1 deletion test/powershell/Host/Startup.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ Describe "Validate start of console host" -Tag CI {
}

$diffs = Compare-Object -ReferenceObject $allowedAssemblies -DifferenceObject $loadedAssemblies
$assembliesLoadedButNotAllowed = @()

if ($null -ne $diffs) {
$assembliesAllowedButNotLoaded = $diffs | Where-Object SideIndicator -EQ "<=" | ForEach-Object InputObject
Expand All @@ -109,6 +110,6 @@ Describe "Validate start of console host" -Tag CI {
}
}

$diffs | Should -BeExactly $null
$assembliesLoadedButNotAllowed | Should -BeNullOrEmpty
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,54 @@ Describe "Import-Module for Binary Modules" -Tags 'CI' {
"@
Add-Type -TypeDefinition $src -OutputAssembly $TESTDRIVE\System.dll
Add-Type -TypeDefinition $src -OutputAssembly $TESTDRIVE\System.exe

$receiveTestJob = {
param (
[System.Management.Automation.Job] $Job,
[string] $Description,
[string[]] $Path,
[int] $TimeoutSec = 30
)

$completedJob = $Job | Wait-Job -Timeout $TimeoutSec
if ($null -eq $completedJob) {
$Job | Stop-Job -ErrorAction SilentlyContinue
throw "'$Description' timed out after $TimeoutSec seconds. Job state: $($Job.State)."
}

$receiveErrors = @()
$output = @($Job | Receive-Job -ErrorVariable receiveErrors -ErrorAction SilentlyContinue)
$failureDetails = @(
$Job.JobStateInfo.Reason
$Job.ChildJobs.JobStateInfo.Reason
$Job.ChildJobs.Error
$receiveErrors
) | Where-Object { $null -ne $_ } | ForEach-Object ToString | Select-Object -Unique

if ($Job.State -ne 'Completed' -or $failureDetails.Count -gt 0) {
$verboseDetails = @($Job.ChildJobs.Verbose) |
Where-Object { $null -ne $_ } |
ForEach-Object ToString |
Select-Object -Unique
$pathDetails = foreach ($itemPath in $Path) {
if (Test-Path -LiteralPath $itemPath) {
try {
"$itemPath (SHA256: $((Get-FileHash -LiteralPath $itemPath -Algorithm SHA256 -ErrorAction Stop).Hash))"
}
catch {
"$itemPath (hash unavailable: $($_.Exception.Message))"
}
}
else {
"$itemPath (missing)"
}
}

throw "'$Description' failed. Job state: $($Job.State). Paths: $($pathDetails -join '; '). Errors: $($failureDetails -join '; '). Verbose: $($verboseDetails -join '; ')"
}

$output
}
}

It "PS should try to load the binary module from a file path with extension: <extension>" -TestCases (
Expand Down Expand Up @@ -196,8 +244,8 @@ Describe "Import-Module for Binary Modules" -Tags 'CI' {
New-ModuleManifest -Path $psdFile -RootModule $exe

try {
$job = Start-Job -ScriptBlock { Import-Module $using:psdFile -PassThru }
$module = $job | Wait-Job -Timeout 30 | Receive-Job
$job = Start-Job -ScriptBlock { Import-Module $using:psdFile -PassThru -ErrorAction Stop -Verbose }
$module = & $receiveTestJob -Job $job -Description 'Import executable root module' -Path $psdFile, $exe
}
finally {
$job | Remove-Job -ErrorAction Ignore
Expand All @@ -216,12 +264,12 @@ Describe "Import-Module for Binary Modules" -Tags 'CI' {

try {
$job = Start-Job -ScriptBlock {
$module = Import-Module $using:psdFile -PassThru
$module = Import-Module $using:psdFile -PassThru -ErrorAction Stop -Verbose
# return the module and the nested module assembly location
$module
$module.NestedModules[0].ImplementingAssembly.Location
}
$module, $location = $job | Wait-Job -Timeout 30 | Receive-Job
$module, $location = & $receiveTestJob -Job $job -Description 'Import executable nested module' -Path $psdFile, $exe
}
finally {
$job | Remove-Job -ErrorAction Ignore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,88 @@ Describe 'Clipboard cmdlet tests' -Tag CI {
$result | Should -BeExactly @('hello', 'world')
}
}

Context 'External clipboard process handling' {
BeforeAll {
$clipboardAssembly = (Get-Command Set-Clipboard).ImplementingType.Assembly
$clipboardType = $clipboardAssembly.GetType('Microsoft.PowerShell.Commands.Internal.Clipboard')
$bindingFlags = [System.Reflection.BindingFlags]'NonPublic, Static'
$startProcess = $clipboardType.GetMethods($bindingFlags) |
Where-Object { $_.Name -eq 'StartProcess' -and $_.GetParameters().Count -eq 5 }
$clipboardSupported = $clipboardType.GetField('_clipboardSupported', $bindingFlags)
$pwsh = Join-Path $PSHOME 'pwsh'
}

BeforeEach {
$originalClipboardSupported = $clipboardSupported.GetValue($null)
}

AfterEach {
$clipboardSupported.SetValue($null, $originalClipboardSupported)
}

It 'terminates a clipboard helper that times out' {
$arguments = '-NoProfile -Command "Start-Sleep -Seconds 30"'
$largeInput = 'a' * 1MB
$invokeArguments = [object[]]@(
$pwsh.PSObject.BaseObject
$arguments.PSObject.BaseObject
$largeInput.PSObject.BaseObject
$false
0
)
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()

try {
$result = $startProcess.Invoke($null, $invokeArguments)
$result | Should -BeNullOrEmpty
$helperPid = [int]$invokeArguments[4]
$helperPid | Should -BeGreaterThan 0
Get-Process -Id $helperPid -ErrorAction Ignore | Should -BeNullOrEmpty
$stopwatch.Elapsed | Should -BeLessThan ([TimeSpan]::FromSeconds(5))
$clipboardSupported.GetValue($null) | Should -BeFalse
}
finally {
$stopwatch.Stop()
if ($helperPid) {
Stop-Process -Id $helperPid -Force -ErrorAction Ignore
}
}
}

It 'does not wait for a descendant that retains stdout' -Skip:$IsWindows {
$childPidPath = Join-Path $TestDrive 'child.pid'
$helperPath = Join-Path $TestDrive 'retained-pipe-helper.sh'
@'
sleep 30 &
echo $! > "$1"
'@ | Set-Content -LiteralPath $helperPath

$arguments = '"{0}" "{1}"' -f $helperPath, $childPidPath
$invokeArguments = [object[]]@(
'/bin/sh'
$arguments.PSObject.BaseObject
[string]::Empty
$true
0
)
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()

try {
$result = $startProcess.Invoke($null, $invokeArguments)
$result | Should -BeNullOrEmpty
$childPidPath | Should -Exist
$childPid = [int](Get-Content -LiteralPath $childPidPath)
Get-Process -Id $childPid -ErrorAction Stop | Should -Not -BeNullOrEmpty
$stopwatch.Elapsed | Should -BeLessThan ([TimeSpan]::FromSeconds(5))
$clipboardSupported.GetValue($null) | Should -BeFalse
Comment thread
jshigetomi marked this conversation as resolved.
Resolved
}
finally {
$stopwatch.Stop()
if ($childPid) {
Stop-Process -Id $childPid -Force -ErrorAction Ignore
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2087,6 +2087,9 @@ try
BeforeAll {
if ($skipTest) { return }
$session = New-RemoteSession
if ($null -eq $session) {
throw 'New-RemoteSession returned no session.'
}

# Define a remote function
Invoke-Command -Session $session { function MyFunction { "Remote!" } Set-Alias SafeAlias -Value MyFunction }
Expand Down
23 changes: 12 additions & 11 deletions test/powershell/engine/Module/ModulePath.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -208,23 +208,24 @@ Describe "ModuleIntrinsics.GetPSModulePath API tests" -tag @('CI', 'RequireAdmin
# create a local repostory and install a module
$localSourceName = [Guid]::NewGuid().ToString("n")
$localSourceLocation = Join-Path $PSScriptRoot assets
Register-PSRepository -Name $localSourceName -SourceLocation $localSourceLocation -InstallationPolicy Trusted -ErrorAction SilentlyContinue
Install-Module -Force -Scope AllUsers -Name PowerShell.TestPackage -Repository $localSourceName -ErrorAction SilentlyContinue
Install-Module -Force -Scope CurrentUser -Name PowerShell.TestPackage -Repository $localSourceName -ErrorAction SilentlyContinue
Register-PSRepository -Name $localSourceName -SourceLocation $localSourceLocation -InstallationPolicy Trusted -ErrorAction Stop
Install-Module -Force -Scope AllUsers -Name PowerShell.TestPackage -Repository $localSourceName -ErrorAction Stop
Install-Module -Force -Scope CurrentUser -Name PowerShell.TestPackage -Repository $localSourceName -ErrorAction Stop

$testCases = @(
@{ Name = "User" ; Expected = $IsWindows ?
(Resolve-Path ([Environment]::GetFolderPath("Personal") + "\PowerShell\Modules")).Path :
(Resolve-Path ([System.Management.Automation.Platform]::SelectProductNameForDirectory("USER_MODULES"))).Path
[System.IO.Path]::GetFullPath([Environment]::GetFolderPath("Personal") + "\PowerShell\Modules") :
[System.IO.Path]::GetFullPath([System.Management.Automation.Platform]::SelectProductNameForDirectory("USER_MODULES"))
}
@{ Name = "Machine" ; Expected = $IsWindows ?
[Environment]::GetFolderPath("ProgramFiles") + "\PowerShell\Modules" :
(Resolve-Path ([System.Management.Automation.Platform]::SelectProductNameForDirectory("SHARED_MODULES"))).Path
[System.IO.Path]::GetFullPath([Environment]::GetFolderPath("ProgramFiles") + "\PowerShell\Modules") :
[System.IO.Path]::GetFullPath([System.Management.Automation.Platform]::SelectProductNameForDirectory("SHARED_MODULES"))
}
@{ Name = "Builtin" ; Expected = (Resolve-Path (Join-Path $PSHOME Modules)).Path }
@{ Name = "Builtin" ; Expected = [System.IO.Path]::GetFullPath((Join-Path $PSHOME Modules)) }
)
# resolve the paths to ensure they are in the correct format
$currentModulePathElements = $env:PSModulePath -split [System.IO.Path]::PathSeparator | Foreach-Object { (Resolve-Path $_).Path }
# Normalize the paths without requiring every logical module path to exist.
$currentModulePathElements = $env:PSModulePath -split [System.IO.Path]::PathSeparator |
ForEach-Object { [System.IO.Path]::GetFullPath($_) }
}

AfterAll {
Expand All @@ -243,7 +244,7 @@ Describe "ModuleIntrinsics.GetPSModulePath API tests" -tag @('CI', 'RequireAdmin

It "The current module path should contain the expected paths for '<Name>'" -testcase $testCases {
param ( $Name, $Expected )
$mPath = (Resolve-Path ([System.Management.Automation.ModuleIntrinsics]::GetPSModulePath($name))).Path
$mPath = [System.IO.Path]::GetFullPath([System.Management.Automation.ModuleIntrinsics]::GetPSModulePath($name))
$currentModulePathElements | Should -Contain $mPath
}
}
Expand Down
4 changes: 2 additions & 2 deletions test/tools/Modules/HelpersRemoting/HelpersRemoting.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,9 @@ function New-RemoteSession
$parameters = CreateParameters -Name $Name -ConfigurationName $ConfigurationName -SessionOption $SessionOption -CimSession:$CimSession.IsPresent

if ($CimSession) {
$session = New-CimSession @parameters
$session = New-CimSession @parameters -ErrorAction Stop
} else {
$session = New-PSSession @parameters
$session = New-PSSession @parameters -ErrorAction Stop
}

Write-Verbose "Successfully created remote PSSession on endpoint: $ConfigurationName"
Expand Down
Loading