diff --git a/.gitignore b/.gitignore index 088159d3d..91c573d9c 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ registered_data.ini .dotnet/ module/Plaster module/PSScriptAnalyzer +module/PSReadLine docs/_site/ docs/_repo/ docs/metadata/ diff --git a/.travis.yml b/.travis.yml index 8209d54c8..015e2dae1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,12 +3,13 @@ language: cpp git: depth: 1000 -os: - - linux - - osx -sudo: required -dist: trusty -osx_image: xcode8.3 +matrix: + include: + - os: linux + dist: trusty + sudo: required + - os: osx + osx_image: xcode9.4 before_install: - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then diff --git a/NuGet.Config b/NuGet.Config index 45f5379b9..6efc7f7b9 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -3,10 +3,4 @@ - - - - - - - \ No newline at end of file + diff --git a/PowerShellEditorServices.Common.props b/PowerShellEditorServices.Common.props index baaba85ea..751a5f07e 100644 --- a/PowerShellEditorServices.Common.props +++ b/PowerShellEditorServices.Common.props @@ -1,6 +1,6 @@ - 1.10.3 + 2.0.0 Microsoft © Microsoft Corporation. All rights reserved. PowerShell;editor;development;language;debugging @@ -9,6 +9,5 @@ git https://github.com/PowerShell/PowerShellEditorServices portable - 1.0.3 diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index 409eb5ee6..8f543c3a4 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -20,10 +20,96 @@ param( $script:IsCIBuild = $env:APPVEYOR -ne $null $script:IsUnix = $PSVersionTable.PSEdition -and $PSVersionTable.PSEdition -eq "Core" -and !$IsWindows -$script:TargetFrameworksParam = "/p:TargetFrameworks=\`"$(if (!$script:IsUnix) { "net452;" })netstandard1.6\`"" -$script:SaveModuleSupportsAllowPrerelease = (Get-Command Save-Module).Parameters.ContainsKey("AllowPrerelease") +$script:TargetPlatform = "netstandard2.0" +$script:TargetFrameworksParam = "/p:TargetFrameworks=`"$script:TargetPlatform`"" +$script:RequiredSdkVersion = "2.1.402" +$script:NugetApiUriBase = 'https://www.nuget.org/api/v2/package' +$script:ModuleBinPath = "$PSScriptRoot/module/PowerShellEditorServices/bin/" +$script:VSCodeModuleBinPath = "$PSScriptRoot/module/PowerShellEditorServices.VSCode/bin/" +$script:WindowsPowerShellFrameworkTarget = 'net461' +$script:NetFrameworkPlatformId = 'win' +$script:NetCoreTestingFrameworkVersion = '2.1.4' $script:BuildInfoPath = [System.IO.Path]::Combine($PSScriptRoot, "src", "PowerShellEditorServices.Host", "BuildInfo", "BuildInfo.cs") +$script:PSCoreModulePath = $null + +$script:TestRuntime = @{ + 'Core' = 'netcoreapp2.1' + 'Desktop' = 'net461' +} + +<# +Declarative specification of binary assets produced +in the build that need to be binplaced in the module. +Schema is: +{ + : { + : [ + + ] + } +} +#> +$script:RequiredBuildAssets = @{ + $script:ModuleBinPath = @{ + 'PowerShellEditorServices' = @( + 'publish/Serilog.dll', + 'publish/Serilog.Sinks.Async.dll', + 'publish/Serilog.Sinks.Console.dll', + 'publish/Serilog.Sinks.File.dll', + 'Microsoft.PowerShell.EditorServices.dll', + 'Microsoft.PowerShell.EditorServices.pdb' + ) + + 'PowerShellEditorServices.Host' = @( + 'publish/UnixConsoleEcho.dll', + 'publish/runtimes/osx-64/native/libdisablekeyecho.dylib', + 'publish/runtimes/linux-64/native/libdisablekeyecho.so', + 'publish/Newtonsoft.Json.dll', + 'Microsoft.PowerShell.EditorServices.Host.dll', + 'Microsoft.PowerShell.EditorServices.Host.pdb' + ) + + 'PowerShellEditorServices.Protocol' = @( + 'Microsoft.PowerShell.EditorServices.Protocol.dll', + 'Microsoft.PowerShell.EditorServices.Protocol.pdb' + ) + } + + $script:VSCodeModuleBinPath = @{ + 'PowerShellEditorServices.VSCode' = @( + 'Microsoft.PowerShell.EditorServices.VSCode.dll', + 'Microsoft.PowerShell.EditorServices.VSCode.pdb' + ) + } +} + +<# +Declares the binary shims we need to make the netstandard DLLs hook into .NET Framework. +Schema is: +{ + : [{ + 'PackageName': , + 'PackageVersion': , + 'TargetRuntime': , + 'DllName'?: + }] +} +#> +$script:RequiredNugetBinaries = @{ + 'Desktop' = @( + @{ PackageName = 'System.Security.Principal.Windows'; PackageVersion = '4.5.0'; TargetRuntime = 'net461' }, + @{ PackageName = 'System.Security.AccessControl'; PackageVersion = '4.5.0'; TargetRuntime = 'net461' }, + @{ PackageName = 'System.IO.Pipes.AccessControl'; PackageVersion = '4.5.1'; TargetRuntime = 'net461' } + ) + + '6.0' = @( + @{ PackageName = 'System.Security.Principal.Windows'; PackageVersion = '4.5.0'; TargetRuntime = 'netcoreapp2.0' }, + @{ PackageName = 'System.Security.AccessControl'; PackageVersion = '4.5.0'; TargetRuntime = 'netcoreapp2.0' }, + @{ PackageName = 'System.IO.Pipes.AccessControl'; PackageVersion = '4.5.1'; TargetRuntime = 'netstandard2.0' } + ) +} + if (Get-Command git -ErrorAction SilentlyContinue) { # ignore changes to this file git update-index --assume-unchanged "$PSScriptRoot/src/PowerShellEditorServices.Host/BuildInfo/BuildInfo.cs" @@ -33,9 +119,65 @@ if ($PSVersionTable.PSEdition -ne "Core") { Add-Type -Assembly System.IO.Compression.FileSystem } -task SetupDotNet -Before Clean, Build, TestHost, TestServer, TestProtocol, TestPowerShellApi, PackageNuGet { +function Restore-NugetAsmForRuntime { + param( + [ValidateNotNull()][string]$PackageName, + [ValidateNotNull()][string]$PackageVersion, + [string]$DllName, + [string]$DestinationPath, + [string]$TargetPlatform = $script:NetFrameworkPlatformId, + [string]$TargetRuntime = $script:WindowsPowerShellFrameworkTarget + ) + + $tmpDir = [System.IO.Path]::GetTempPath() - $requiredSdkVersion = "2.0.0" + if (-not $DllName) { + $DllName = "$PackageName.dll" + } + + if ($DestinationPath -eq $null) { + $DestinationPath = Join-Path $tmpDir $DllName + } elseif (Test-Path $DestinationPath -PathType Container) { + $DestinationPath = Join-Path $DestinationPath $DllName + } + + $packageDirPath = Join-Path $tmpDir "$PackageName.$PackageVersion" + if (-not (Test-Path $packageDirPath)) { + $guid = New-Guid + $tmpNupkgPath = Join-Path $tmpDir "$guid.zip" + if (Test-Path $tmpNupkgPath) { + Remove-Item -Force $tmpNupkgPath + } + + try { + $packageUri = "$script:NugetApiUriBase/$PackageName/$PackageVersion" + Invoke-WebRequest -Uri $packageUri -OutFile $tmpNupkgPath + Expand-Archive -Path $tmpNupkgPath -DestinationPath $packageDirPath + } finally { + Remove-Item -Force $tmpNupkgPath -ErrorAction SilentlyContinue + } + } + + $internalPath = [System.IO.Path]::Combine($packageDirPath, 'runtimes', $TargetPlatform, 'lib', $TargetRuntime, $DllName) + + Copy-Item -Path $internalPath -Destination $DestinationPath -Force + + return $DestinationPath +} + +function Invoke-WithCreateDefaultHook { + param([scriptblock]$ScriptBlock) + + try + { + $env:PSES_TEST_USE_CREATE_DEFAULT = 1 + & $ScriptBlock + } finally { + Remove-Item env:PSES_TEST_USE_CREATE_DEFAULT + } +} + +task SetupDotNet -Before Clean, Build, TestHost, TestServer, TestProtocol, PackageNuGet { $dotnetPath = "$PSScriptRoot/.dotnet" $dotnetExePath = if ($script:IsUnix) { "$dotnetPath/dotnet" } else { "$dotnetPath/dotnet.exe" } @@ -56,7 +198,7 @@ task SetupDotNet -Before Clean, Build, TestHost, TestServer, TestProtocol, TestP # dotnet --version can return a semver that System.Version can't handle # e.g.: 2.1.300-preview-01. The replace operator is used to remove any build suffix. $version = (& $dotnetExePath --version) -replace '[+-].*$','' - if ([version]$version -ge [version]$requiredSdkVersion) { + if ($version -and [version]$version -ge [version]$script:RequiredSdkVersion) { $script:dotnetExe = $dotnetExePath } else { @@ -71,21 +213,21 @@ task SetupDotNet -Before Clean, Build, TestHost, TestServer, TestProtocol, TestP if ($script:dotnetExe -eq $null) { - Write-Host "`n### Installing .NET CLI $requiredSdkVersion...`n" -ForegroundColor Green + Write-Host "`n### Installing .NET CLI $script:RequiredSdkVersion...`n" -ForegroundColor Green # The install script is platform-specific $installScriptExt = if ($script:IsUnix) { "sh" } else { "ps1" } # Download the official installation script and run it $installScriptPath = "$([System.IO.Path]::GetTempPath())dotnet-install.$installScriptExt" - Invoke-WebRequest "https://raw.githubusercontent.com/dotnet/cli/v2.0.0/scripts/obtain/dotnet-install.$installScriptExt" -OutFile $installScriptPath + Invoke-WebRequest "https://raw.githubusercontent.com/dotnet/cli/v$script:RequiredSdkVersion/scripts/obtain/dotnet-install.$installScriptExt" -OutFile $installScriptPath $env:DOTNET_INSTALL_DIR = "$PSScriptRoot/.dotnet" if (!$script:IsUnix) { - & $installScriptPath -Version $requiredSdkVersion -InstallDir "$env:DOTNET_INSTALL_DIR" + & $installScriptPath -Version $script:RequiredSdkVersion -InstallDir "$env:DOTNET_INSTALL_DIR" } else { - & /bin/bash $installScriptPath -Version $requiredSdkVersion -InstallDir "$env:DOTNET_INSTALL_DIR" + & /bin/bash $installScriptPath -Version $script:RequiredSdkVersion -InstallDir "$env:DOTNET_INSTALL_DIR" $env:PATH = $dotnetExeDir + [System.IO.Path]::PathSeparator + $env:PATH } @@ -106,6 +248,7 @@ task SetupDotNet -Before Clean, Build, TestHost, TestServer, TestProtocol, TestP } task Clean { + exec { & $script:dotnetExe restore } exec { & $script:dotnetExe clean } Remove-Item $PSScriptRoot\module\PowerShellEditorServices\bin -Recurse -Force -ErrorAction Ignore Remove-Item $PSScriptRoot\module\PowerShellEditorServices.VSCode\bin -Recurse -Force -ErrorAction Ignore @@ -136,20 +279,6 @@ task GetProductVersion -Before PackageNuGet, PackageModule, UploadArtifacts { Write-Host "`n### Product Version: $script:FullVersion`n" -ForegroundColor Green } -function BuildForPowerShellVersion($version) { - Write-Host -ForegroundColor Green "`n### Testing API usage for PowerShell $version...`n" - exec { & $script:dotnetExe build -f net452 .\src\PowerShellEditorServices\PowerShellEditorServices.csproj /p:PowerShellVersion=$version } -} - -task TestPowerShellApi -If { !$script:IsUnix } { - BuildForPowerShellVersion v3 - BuildForPowerShellVersion v4 - BuildForPowerShellVersion v5r1 - - # Do a final restore to put everything back to normal - exec { & $script:dotnetExe restore .\src\PowerShellEditorServices\PowerShellEditorServices.csproj } -} - task CreateBuildInfo -Before Build { $buildVersion = "" $buildOrigin = "" @@ -196,15 +325,9 @@ namespace Microsoft.PowerShell.EditorServices.Host } task Build { - exec { & $script:dotnetExe publish -c $Configuration .\src\PowerShellEditorServices.Host\PowerShellEditorServices.Host.csproj -f netstandard1.6 } - if (!$script:IsUnix) { - exec { & $script:dotnetExe publish -c $Configuration .\src\PowerShellEditorServices.Host\PowerShellEditorServices.Host.csproj -f net452 } - } + exec { & $script:dotnetExe publish -c $Configuration .\src\PowerShellEditorServices\PowerShellEditorServices.csproj -f $script:TargetPlatform } + exec { & $script:dotnetExe publish -c $Configuration .\src\PowerShellEditorServices.Host\PowerShellEditorServices.Host.csproj -f $script:TargetPlatform } exec { & $script:dotnetExe build -c $Configuration .\src\PowerShellEditorServices.VSCode\PowerShellEditorServices.VSCode.csproj $script:TargetFrameworksParam } - exec { & $script:dotnetExe publish -c $Configuration .\src\PowerShellEditorServices\PowerShellEditorServices.csproj -f netstandard1.6 } - Copy-Item $PSScriptRoot\src\PowerShellEditorServices\bin\$Configuration\netstandard1.6\publish\UnixConsoleEcho.dll -Destination $PSScriptRoot\src\PowerShellEditorServices.Host\bin\$Configuration\netstandard1.6 - Copy-Item $PSScriptRoot\src\PowerShellEditorServices\bin\$Configuration\netstandard1.6\publish\runtimes\osx-64\native\libdisablekeyecho.dylib -Destination $PSScriptRoot\src\PowerShellEditorServices.Host\bin\$Configuration\netstandard1.6 - Copy-Item $PSScriptRoot\src\PowerShellEditorServices\bin\$Configuration\netstandard1.6\publish\runtimes\linux-64\native\libdisablekeyecho.so -Destination $PSScriptRoot\src\PowerShellEditorServices.Host\bin\$Configuration\netstandard1.6 } function UploadTestLogs { @@ -232,22 +355,40 @@ function XunitTraitFilter { task Test TestServer,TestProtocol -task TestServer -If { !$script:IsUnix } { +task TestServer { Set-Location .\test\PowerShellEditorServices.Test\ - exec { & $script:dotnetExe build -c $Configuration -f net452 } - exec { & $script:dotnetExe xunit -configuration $Configuration -framework net452 -verbose -nobuild (XunitTraitFilter) } + + if (-not $script:IsUnix) { + exec { & $script:dotnetExe test -f $script:TestRuntime.Desktop } + } + + Invoke-WithCreateDefaultHook -NewModulePath $script:PSCoreModulePath { + exec { & $script:dotnetExe test -f $script:TestRuntime.Core } + } } -task TestProtocol -If { !$script:IsUnix } { +task TestProtocol { Set-Location .\test\PowerShellEditorServices.Test.Protocol\ - exec { & $script:dotnetExe build -c $Configuration -f net452 } - exec { & $script:dotnetExe xunit -configuration $Configuration -framework net452 -verbose -nobuild (XunitTraitFilter) } + + if (-not $script:IsUnix) { + exec { & $script:dotnetExe test -f $script:TestRuntime.Desktop } + } + + Invoke-WithCreateDefaultHook { + exec { & $script:dotnetExe test -f $script:TestRuntime.Core } + } } -task TestHost -If { !$script:IsUnix } { +task TestHost { Set-Location .\test\PowerShellEditorServices.Test.Host\ - exec { & $script:dotnetExe build -c $Configuration -f net452 } - exec { & $script:dotnetExe xunit -configuration $Configuration -framework net452 -verbose -nobuild (XunitTraitFilter) } + + if (-not $script:IsUnix) { + exec { & $script:dotnetExe build -f $script:TestRuntime.Desktop } + exec { & $script:dotnetExe test -f $script:TestRuntime.Desktop } + } + + exec { & $script:dotnetExe build -c $Configuration -f $script:TestRuntime.Core } + exec { & $script:dotnetExe test -f $script:TestRuntime.Core } } task CITest ?Test, { @@ -260,52 +401,40 @@ task CITest ?Test, { } task LayoutModule -After Build { - # Lay out the PowerShellEditorServices module's binaries - New-Item -Force $PSScriptRoot\module\PowerShellEditorServices\bin\ -Type Directory | Out-Null - New-Item -Force $PSScriptRoot\module\PowerShellEditorServices\bin\Desktop -Type Directory | Out-Null - New-Item -Force $PSScriptRoot\module\PowerShellEditorServices\bin\Core -Type Directory | Out-Null - - Copy-Item -Force -Path $PSScriptRoot\src\PowerShellEditorServices\bin\$Configuration\netstandard1.6\publish\Serilog*.dll -Destination $PSScriptRoot\module\PowerShellEditorServices\bin\Core\ - Copy-Item -Force -Path $PSScriptRoot\src\PowerShellEditorServices\bin\$Configuration\netstandard1.6\publish\System.Runtime.InteropServices.RuntimeInformation.dll -Destination $PSScriptRoot\module\PowerShellEditorServices\bin\Core\ - - Copy-Item -Force -Path $PSScriptRoot\src\PowerShellEditorServices.Host\bin\$Configuration\netstandard1.6\* -Filter Microsoft.PowerShell.EditorServices*.dll -Destination $PSScriptRoot\module\PowerShellEditorServices\bin\Core\ - Copy-Item -Force -Path $PSScriptRoot\src\PowerShellEditorServices.Host\bin\$Configuration\netstandard1.6\UnixConsoleEcho.dll -Destination $PSScriptRoot\module\PowerShellEditorServices\bin\Core\ - Copy-Item -Force -Path $PSScriptRoot\src\PowerShellEditorServices.Host\bin\$Configuration\netstandard1.6\libdisablekeyecho.* -Destination $PSScriptRoot\module\PowerShellEditorServices\bin\Core\ - Copy-Item -Force -Path $PSScriptRoot\src\PowerShellEditorServices.Host\bin\$Configuration\netstandard1.6\publish\runtimes\win\lib\netstandard1.3\* -Filter System.IO.Pipes*.dll -Destination $PSScriptRoot\module\PowerShellEditorServices\bin\Core\ - - if (!$script:IsUnix) { - Copy-Item -Force -Path $PSScriptRoot\src\PowerShellEditorServices\bin\$Configuration\net452\Serilog*.dll -Destination $PSScriptRoot\module\PowerShellEditorServices\bin\Desktop - Copy-Item -Force -Path $PSScriptRoot\src\PowerShellEditorServices\bin\$Configuration\net452\System.Runtime.InteropServices.RuntimeInformation.dll -Destination $PSScriptRoot\module\PowerShellEditorServices\bin\Desktop\ - - Copy-Item -Force -Path $PSScriptRoot\src\PowerShellEditorServices.Host\bin\$Configuration\net452\* -Filter Microsoft.PowerShell.EditorServices*.dll -Destination $PSScriptRoot\module\PowerShellEditorServices\bin\Desktop\ - Copy-Item -Force -Path $PSScriptRoot\src\PowerShellEditorServices.Host\bin\$Configuration\net452\Newtonsoft.Json.dll -Destination $PSScriptRoot\module\PowerShellEditorServices\bin\Desktop\ - Copy-Item -Force -Path $PSScriptRoot\src\PowerShellEditorServices.Host\bin\$Configuration\net452\UnixConsoleEcho.dll -Destination $PSScriptRoot\module\PowerShellEditorServices\bin\Desktop\ - } - # Copy Third Party Notices.txt to module folder Copy-Item -Force -Path "$PSScriptRoot\Third Party Notices.txt" -Destination $PSScriptRoot\module\PowerShellEditorServices - # Lay out the PowerShellEditorServices.VSCode module's binaries - New-Item -Force $PSScriptRoot\module\PowerShellEditorServices.VSCode\bin\ -Type Directory | Out-Null - New-Item -Force $PSScriptRoot\module\PowerShellEditorServices.VSCode\bin\Desktop -Type Directory | Out-Null - New-Item -Force $PSScriptRoot\module\PowerShellEditorServices.VSCode\bin\Core -Type Directory | Out-Null - - Copy-Item -Force -Path $PSScriptRoot\src\PowerShellEditorServices.VSCode\bin\$Configuration\netstandard1.6\* -Filter Microsoft.PowerShell.EditorServices.VSCode*.dll -Destination $PSScriptRoot\module\PowerShellEditorServices.VSCode\bin\Core\ - if (!$script:IsUnix) { - Copy-Item -Force -Path $PSScriptRoot\src\PowerShellEditorServices.VSCode\bin\$Configuration\net452\* -Filter Microsoft.PowerShell.EditorServices.VSCode*.dll -Destination $PSScriptRoot\module\PowerShellEditorServices.VSCode\bin\Desktop\ + # Lay out the PowerShellEditorServices module's binaries + # For each binplace destination + foreach ($destDir in $script:RequiredBuildAssets.Keys) { + # Create the destination dir + $null = New-Item -Force $destDir -Type Directory + + # For each PSES subproject + foreach ($projectName in $script:RequiredBuildAssets[$destDir].Keys) { + # Get the project build dir path + $basePath = [System.IO.Path]::Combine($PSScriptRoot, 'src', $projectName, 'bin', $Configuration, $script:TargetPlatform) + + # For each asset in the subproject + foreach ($bin in $script:RequiredBuildAssets[$destDir][$projectName]) { + # Get the asset path + $binPath = Join-Path $basePath $bin + + # Binplace the asset + Copy-Item -Force -Verbose $binPath $destDir + } + } } - if ($Configuration -eq "Debug") { - Copy-Item -Force -Path $PSScriptRoot\src\PowerShellEditorServices.VSCode\bin\$Configuration\netstandard1.6\Microsoft.PowerShell.EditorServices.VSCode.pdb -Destination $PSScriptRoot\module\PowerShellEditorServices.VSCode\bin\Core\ - Copy-Item -Force -Path $PSScriptRoot\src\PowerShellEditorServices\bin\$Configuration\netstandard1.6\Microsoft.PowerShell.EditorServices.pdb -Destination $PSScriptRoot\module\PowerShellEditorServices\bin\Core\ - Copy-Item -Force -Path $PSScriptRoot\src\PowerShellEditorServices.Host\bin\$Configuration\netstandard1.6\Microsoft.PowerShell.EditorServices.Host.pdb -Destination $PSScriptRoot\module\PowerShellEditorServices\bin\Core\ - Copy-Item -Force -Path $PSScriptRoot\src\PowerShellEditorServices.Protocol\bin\$Configuration\netstandard1.6\Microsoft.PowerShell.EditorServices.Protocol.pdb -Destination $PSScriptRoot\module\PowerShellEditorServices\bin\Core\ + # Get and place the shim bins for net461 + foreach ($binDestinationDir in $script:RequiredNugetBinaries.Keys) { + $binDestPath = Join-Path $script:ModuleBinPath $binDestinationDir + if (-not (Test-Path $binDestPath)) { + New-Item -Path $binDestPath -ItemType Directory + } - if (!$script:IsUnix) { - Copy-Item -Force -Path $PSScriptRoot\src\PowerShellEditorServices.VSCode\bin\$Configuration\net452\Microsoft.PowerShell.EditorServices.VSCode.pdb -Destination $PSScriptRoot\module\PowerShellEditorServices.VSCode\bin\Desktop\ - Copy-Item -Force -Path $PSScriptRoot\src\PowerShellEditorServices\bin\$Configuration\net452\Microsoft.PowerShell.EditorServices.pdb -Destination $PSScriptRoot\module\PowerShellEditorServices\bin\Desktop\ - Copy-Item -Force -Path $PSScriptRoot\src\PowerShellEditorServices.Host\bin\$Configuration\net452\Microsoft.PowerShell.EditorServices.Host.pdb -Destination $PSScriptRoot\module\PowerShellEditorServices\bin\Desktop\ - Copy-Item -Force -Path $PSScriptRoot\src\PowerShellEditorServices.Protocol\bin\$Configuration\net452\Microsoft.PowerShell.EditorServices.Protocol.pdb -Destination $PSScriptRoot\module\PowerShellEditorServices\bin\Desktop\ + foreach ($packageDetails in $script:RequiredNugetBinaries[$binDestinationDir]) { + Restore-NugetAsmForRuntime -DestinationPath $binDestPath @packageDetails } } } @@ -323,6 +452,7 @@ task RestorePsesModules -After Build { Name = $name MinimumVersion = $_.Value.MinimumVersion MaximumVersion = $_.Value.MaximumVersion + AllowPrerelease = $_.Value.AllowPrerelease Repository = if ($_.Value.Repository) { $_.Value.Repository } else { $DefaultModuleRepository } Path = $submodulePath } @@ -332,11 +462,6 @@ task RestorePsesModules -After Build { throw "EditorServices module listed without name in '$ModulesJsonPath'" } - if ($script:SaveModuleSupportsAllowPrerelease) - { - $body += @{ AllowPrerelease = $_.Value.AllowPrerelease } - } - $moduleInfos.Add($name, $body) } @@ -355,19 +480,16 @@ task RestorePsesModules -After Build { Name = $moduleName MinimumVersion = $moduleInstallDetails.MinimumVersion MaximumVersion = $moduleInstallDetails.MaximumVersion + AllowPrerelease = $moduleInstallDetails.AllowPrerelease Repository = if ($moduleInstallDetails.Repository) { $moduleInstallDetails.Repository } else { $DefaultModuleRepository } Path = $submodulePath } - if ($script:SaveModuleSupportsAllowPrerelease) - { - $splatParameters += @{ AllowPrerelease = $moduleInstallDetails.AllowPrerelease } - } - - Write-Host "`tInstalling module: ${moduleName}" + Write-Host "`tInstalling module: ${moduleName} with arguments $(ConvertTo-Json $splatParameters)" Save-Module @splatParameters } + Write-Host "`n" } @@ -399,4 +521,4 @@ task UploadArtifacts -If ($script:IsCIBuild) { } # The default task is to run the entire CI build -task . GetProductVersion, Clean, Build, TestPowerShellApi, CITest, BuildCmdletHelp, PackageNuGet, PackageModule, UploadArtifacts +task . GetProductVersion, Clean, Build, CITest, BuildCmdletHelp, PackageNuGet, PackageModule, UploadArtifacts diff --git a/appveyor.yml b/appveyor.yml index 4e1d1ba15..c66710822 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -version: '1.10.3{build}' +version: '2.0.0-{build}' image: Visual Studio 2017 clone_depth: 10 skip_tags: true @@ -6,6 +6,7 @@ skip_tags: true branches: only: - master + - 2.0.0 environment: DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true # Don't download unneeded packages @@ -13,7 +14,12 @@ environment: install: - ps: | - Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force | Out-Null + Get-Module PowerShellGet,PackageManagement | Remove-Module -Force -Verbose + powershell -Command { Install-Module -Name PowershellGet -MinimumVersion 1.6 -force -confirm:$false -verbose } + powershell -Command { Install-Module -Name PackageManagement -MinimumVersion 1.1.7.0 -Force -Confirm:$false -Verbose } + Import-Module -Name PowerShellGet -MinimumVersion 1.6 -Force + Import-Module -Name PackageManagement -MinimumVersion 1.1.7.0 -Force + Install-PackageProvider -Name NuGet -Force | Out-Null Import-PackageProvider NuGet -Force | Out-Null Set-PSRepository -Name PSGallery -InstallationPolicy Trusted | Out-Null Install-Module InvokeBuild -MaximumVersion 5.1.0 -Scope CurrentUser -Force | Out-Null diff --git a/module/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.psm1 b/module/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.psm1 index f5d53be4c..e7b34e076 100644 --- a/module/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.psm1 +++ b/module/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.psm1 @@ -3,12 +3,7 @@ # Licensed under the MIT license. See LICENSE file in the project root for full license information. # -if (!$PSVersionTable.PSEdition -or $PSVersionTable.PSEdition -eq "Desktop") { - Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Desktop/Microsoft.PowerShell.EditorServices.VSCode.dll" -} -else { - Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Core/Microsoft.PowerShell.EditorServices.VSCode.dll" -} +Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Microsoft.PowerShell.EditorServices.VSCode.dll" if ($psEditor -is [Microsoft.PowerShell.EditorServices.Extensions.EditorObject]) { [Microsoft.PowerShell.EditorServices.VSCode.ComponentRegistration]::Register($psEditor.Components) diff --git a/module/PowerShellEditorServices/PowerShellEditorServices.psd1 b/module/PowerShellEditorServices/PowerShellEditorServices.psd1 index 236d68520..7a03e12be 100644 --- a/module/PowerShellEditorServices/PowerShellEditorServices.psd1 +++ b/module/PowerShellEditorServices/PowerShellEditorServices.psd1 @@ -12,7 +12,7 @@ RootModule = 'PowerShellEditorServices.psm1' # Version number of this module. -ModuleVersion = '1.10.3' +ModuleVersion = '2.0.0' # ID used to uniquely identify this module GUID = '9ca15887-53a2-479a-9cda-48d26bcb6c47' diff --git a/module/PowerShellEditorServices/PowerShellEditorServices.psm1 b/module/PowerShellEditorServices/PowerShellEditorServices.psm1 index dd8f463a6..d0378f0af 100644 --- a/module/PowerShellEditorServices/PowerShellEditorServices.psm1 +++ b/module/PowerShellEditorServices/PowerShellEditorServices.psm1 @@ -3,16 +3,21 @@ # Licensed under the MIT license. See LICENSE file in the project root for full license information. # -if (!$PSVersionTable.PSEdition -or $PSVersionTable.PSEdition -eq "Desktop") { - Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Desktop/Microsoft.PowerShell.EditorServices.dll" - Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Desktop/Microsoft.PowerShell.EditorServices.Host.dll" -} -else { - Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Core/Microsoft.PowerShell.EditorServices.dll" - Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Core/Microsoft.PowerShell.EditorServices.Protocol.dll" - Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Core/Microsoft.PowerShell.EditorServices.Host.dll" +# Need to load pipe handling shim assemblies in Windows PowerShell and PSCore 6.0 because they don't have WCP +if ($PSEdition -eq 'Desktop') { + Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Desktop/System.IO.Pipes.AccessControl.dll" + Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Desktop/System.Security.AccessControl.dll" + Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Desktop/System.Security.Principal.Windows.dll" +} elseif ($PSVersionTable.PSVersion -ge '6.0' -and $PSVersionTable.PSVersion -lt '6.1' -and $IsWindows) { + Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/6.0/System.IO.Pipes.AccessControl.dll" + Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/6.0/System.Security.AccessControl.dll" + Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/6.0/System.Security.Principal.Windows.dll" } +Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Microsoft.PowerShell.EditorServices.dll" +Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Microsoft.PowerShell.EditorServices.Host.dll" +Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Microsoft.PowerShell.EditorServices.Protocol.dll" + function Start-EditorServicesHost { [CmdletBinding()] param( diff --git a/module/PowerShellEditorServices/Start-EditorServices.ps1 b/module/PowerShellEditorServices/Start-EditorServices.ps1 index 32135d7af..5618759e9 100644 --- a/module/PowerShellEditorServices/Start-EditorServices.ps1 +++ b/module/PowerShellEditorServices/Start-EditorServices.ps1 @@ -349,7 +349,7 @@ try { "status" = "not started"; "languageServiceTransport" = $PSCmdlet.ParameterSetName; "debugServiceTransport" = $PSCmdlet.ParameterSetName; - }; + } # Create the Editor Services host Log "Invoking Start-EditorServicesHost" @@ -368,7 +368,8 @@ try { -BundledModulesPath $BundledModulesPath ` -EnableConsoleRepl:$EnableConsoleRepl.IsPresent ` -DebugServiceOnly:$DebugServiceOnly.IsPresent ` - -WaitForDebugger:$WaitForDebugger.IsPresent + -WaitForDebugger:$WaitForDebugger.IsPresent ` + -FeatureFlags $FeatureFlags break } @@ -392,7 +393,8 @@ try { -BundledModulesPath $BundledModulesPath ` -EnableConsoleRepl:$EnableConsoleRepl.IsPresent ` -DebugServiceOnly:$DebugServiceOnly.IsPresent ` - -WaitForDebugger:$WaitForDebugger.IsPresent + -WaitForDebugger:$WaitForDebugger.IsPresent ` + -FeatureFlags $FeatureFlags Set-PipeFileResult $resultDetails "languageServiceReadPipeName" $LanguageServiceInPipeName Set-PipeFileResult $resultDetails "languageServiceWritePipeName" $LanguageServiceOutPipeName @@ -417,7 +419,8 @@ try { -BundledModulesPath $BundledModulesPath ` -EnableConsoleRepl:$EnableConsoleRepl.IsPresent ` -DebugServiceOnly:$DebugServiceOnly.IsPresent ` - -WaitForDebugger:$WaitForDebugger.IsPresent + -WaitForDebugger:$WaitForDebugger.IsPresent ` + -FeatureFlags $FeatureFlags Set-PipeFileResult $resultDetails "languageServicePipeName" $LanguageServicePipeName Set-PipeFileResult $resultDetails "debugServicePipeName" $DebugServicePipeName diff --git a/modules.json b/modules.json index e4555e5de..621655464 100644 --- a/modules.json +++ b/modules.json @@ -8,5 +8,10 @@ "MinimumVersion":"1.0", "MaximumVersion":"1.99", "AllowPrerelease":false + }, + "PSReadLine":{ + "MinimumVersion":"2.0.0-beta3", + "MaximumVersion":"2.1", + "AllowPrerelease":true } } diff --git a/scripts/travis.ps1 b/scripts/travis.ps1 index 5af67b0dd..abcb12fe2 100644 --- a/scripts/travis.ps1 +++ b/scripts/travis.ps1 @@ -1,4 +1,4 @@ - +$ErrorActionPreference = 'Stop' # Install InvokeBuild Install-Module InvokeBuild -MaximumVersion 5.1.0 -Scope CurrentUser -Force diff --git a/src/PowerShellEditorServices.Channel.WebSocket/WebsocketClientChannel.cs b/src/PowerShellEditorServices.Channel.WebSocket/WebsocketClientChannel.cs index ea1142f76..edf2fd52f 100644 --- a/src/PowerShellEditorServices.Channel.WebSocket/WebsocketClientChannel.cs +++ b/src/PowerShellEditorServices.Channel.WebSocket/WebsocketClientChannel.cs @@ -39,7 +39,7 @@ public WebsocketClientChannel(string url) this.serverUrl = url; } - public override async Task WaitForConnection() + public override async Task WaitForConnectionAsync() { try { @@ -52,7 +52,7 @@ public override async Task WaitForConnection() { Logger.Write(LogLevel.Warning, string.Format("Failed to connect to WebSocket server. Error was '{0}'", wsException.Message)); - + } throw; @@ -99,7 +99,7 @@ protected override void Shutdown() } /// - /// Extension of that sends data to a WebSocket during FlushAsync + /// Extension of that sends data to a WebSocket during FlushAsync /// and reads during WriteAsync. /// internal class ClientWebSocketStream : MemoryStream @@ -110,7 +110,7 @@ internal class ClientWebSocketStream : MemoryStream /// Constructor /// /// - /// It is expected that the socket is in an Open state. + /// It is expected that the socket is in an Open state. /// /// public ClientWebSocketStream(ClientWebSocket socket) @@ -119,7 +119,7 @@ public ClientWebSocketStream(ClientWebSocket socket) } /// - /// Reads from the WebSocket. + /// Reads from the WebSocket. /// /// /// @@ -138,7 +138,7 @@ public override async Task ReadAsync(byte[] buffer, int offset, int count, { result = await socket.ReceiveAsync(new ArraySegment(buffer, offset, count), cancellationToken); } while (!result.EndOfMessage); - + if (result.MessageType == WebSocketMessageType.Close) { await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", cancellationToken); diff --git a/src/PowerShellEditorServices.Channel.WebSocket/WebsocketServerChannel.cs b/src/PowerShellEditorServices.Channel.WebSocket/WebsocketServerChannel.cs index 07acf953a..c3cb907de 100644 --- a/src/PowerShellEditorServices.Channel.WebSocket/WebsocketServerChannel.cs +++ b/src/PowerShellEditorServices.Channel.WebSocket/WebsocketServerChannel.cs @@ -17,8 +17,8 @@ namespace Microsoft.PowerShell.EditorServices.Channel.WebSocket { /// - /// Implementation of that implements the streams necessary for - /// communicating via OWIN WebSockets. + /// Implementation of that implements the streams necessary for + /// communicating via OWIN WebSockets. /// public class WebSocketServerChannel : ChannelBase { @@ -42,22 +42,22 @@ protected override void Initialize(IMessageSerializer messageSerializer) this.MessageWriter = new MessageWriter( - new WebSocketStream(socketConnection), + new WebSocketStream(socketConnection), messageSerializer); } /// - /// Dispatches data received during calls to OnMessageReceived in the class. + /// Dispatches data received during calls to OnMessageReceivedAsync in the class. /// /// - /// This method calls an overriden version of the that dispatches messages on - /// demand rather than running on a background thread. + /// This method calls an overriden version of the that dispatches messages on + /// demand rather than running on a background thread. /// /// /// - public async Task Dispatch(ArraySegment message) + public async Task DispatchAsync(ArraySegment message) { - //Clear our stream + //Clear our stream inStream.SetLength(0); //Write data and dispatch to handlers @@ -70,7 +70,7 @@ protected override void Shutdown() this.socketConnection.Close(WebSocketCloseStatus.NormalClosure, "Server shutting down"); } - public override Task WaitForConnection() + public override Task WaitForConnectionAsync() { // TODO: Need to update behavior here return Task.FromResult(true); @@ -78,11 +78,11 @@ public override Task WaitForConnection() } /// - /// Overriden that sends data through a during the FlushAsync call. + /// Overriden that sends data through a during the FlushAsync call. /// /// /// FlushAsync will send data via the SendBinary method of the class. The memory streams length will - /// then be set to 0 to reset the stream for additional data to be written. + /// then be set to 0 to reset the stream for additional data to be written. /// internal class WebSocketStream : MemoryStream { @@ -106,7 +106,7 @@ public override async Task FlushAsync(CancellationToken cancellationToken) /// public abstract class EditorServiceWebSocketConnection : WebSocketConnection { - protected EditorServiceWebSocketConnection() + protected EditorServiceWebSocketConnection() { Channel = new WebSocketServerChannel(this); } @@ -120,9 +120,9 @@ public override void OnOpen() Server.Start(); } - public override async Task OnMessageReceived(ArraySegment message, WebSocketMessageType type) + public override async Task OnMessageReceivedAsync(ArraySegment message, WebSocketMessageType type) { - await Channel.Dispatch(message); + await Channel.DispatchAsync(message); } public override Task OnCloseAsync(WebSocketCloseStatus? closeStatus, string closeStatusDescription) diff --git a/src/PowerShellEditorServices.Host/CodeLens/CodeLensFeature.cs b/src/PowerShellEditorServices.Host/CodeLens/CodeLensFeature.cs index ac5f388a3..e7e2f6910 100644 --- a/src/PowerShellEditorServices.Host/CodeLens/CodeLensFeature.cs +++ b/src/PowerShellEditorServices.Host/CodeLens/CodeLensFeature.cs @@ -49,11 +49,11 @@ public static CodeLensFeature Create( messageHandlers.SetRequestHandler( CodeLensRequest.Type, - codeLenses.HandleCodeLensRequest); + codeLenses.HandleCodeLensRequestAsync); messageHandlers.SetRequestHandler( CodeLensResolveRequest.Type, - codeLenses.HandleCodeLensResolveRequest); + codeLenses.HandleCodeLensResolveRequestAsync); codeLenses.Providers.Add( new ReferencesCodeLensProvider( @@ -111,7 +111,7 @@ public CodeLens[] ProvideCodeLenses(ScriptFile scriptFile) /// /// Parameters on the CodeLens request that was received. /// - private async Task HandleCodeLensRequest( + private async Task HandleCodeLensRequestAsync( CodeLensRequest codeLensParams, RequestContext requestContext) { @@ -132,7 +132,7 @@ private async Task HandleCodeLensRequest( _jsonSerializer); } - await requestContext.SendResult(codeLensResponse); + await requestContext.SendResultAsync(codeLensResponse); } /// @@ -140,7 +140,7 @@ private async Task HandleCodeLensRequest( /// /// The CodeLens to be resolved/updated. /// - private async Task HandleCodeLensResolveRequest( + private async Task HandleCodeLensResolveRequestAsync( LanguageServer.CodeLens codeLens, RequestContext requestContext) { @@ -178,13 +178,13 @@ await originalProvider.ResolveCodeLensAsync( originalCodeLens, CancellationToken.None); - await requestContext.SendResult( + await requestContext.SendResultAsync( resolvedCodeLens.ToProtocolCodeLens( _jsonSerializer)); } else { - await requestContext.SendError( + await requestContext.SendErrorAsync( $"Could not find provider for the original CodeLens: {codeLensData.ProviderId}"); } } diff --git a/src/PowerShellEditorServices.Host/CodeLens/ReferencesCodeLensProvider.cs b/src/PowerShellEditorServices.Host/CodeLens/ReferencesCodeLensProvider.cs index c833aec27..6a5312a93 100644 --- a/src/PowerShellEditorServices.Host/CodeLens/ReferencesCodeLensProvider.cs +++ b/src/PowerShellEditorServices.Host/CodeLens/ReferencesCodeLensProvider.cs @@ -82,7 +82,7 @@ public async Task ResolveCodeLensAsync( codeLens.ScriptExtent.StartLineNumber, codeLens.ScriptExtent.StartColumnNumber); - FindReferencesResult referencesResult = await _editorSession.LanguageService.FindReferencesOfSymbol( + FindReferencesResult referencesResult = await _editorSession.LanguageService.FindReferencesOfSymbolAsync( foundSymbol, references, _editorSession.Workspace); diff --git a/src/PowerShellEditorServices.Host/EditorServicesHost.cs b/src/PowerShellEditorServices.Host/EditorServicesHost.cs index 516b0bba9..2f3275bf3 100644 --- a/src/PowerShellEditorServices.Host/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Host/EditorServicesHost.cs @@ -125,9 +125,7 @@ public EditorServicesHost( #endif // Catch unhandled exceptions for logging purposes -#if !CoreCLR AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; -#endif } #endregion @@ -146,11 +144,8 @@ public void StartLogging(string logFilePath, LogLevel logLevel) .AddLogFile(logFilePath) .Build(); -#if CoreCLR - FileVersionInfo fileVersionInfo = FileVersionInfo.GetVersionInfo(this.GetType().GetTypeInfo().Assembly.Location); -#else - FileVersionInfo fileVersionInfo = FileVersionInfo.GetVersionInfo(this.GetType().Assembly.Location); -#endif + FileVersionInfo fileVersionInfo = + FileVersionInfo.GetVersionInfo(this.GetType().GetTypeInfo().Assembly.Location); string osVersion = RuntimeInformation.OSDescription; @@ -196,7 +191,7 @@ public void StartLanguageService( this.languageServiceListener = CreateServiceListener(MessageProtocolType.LanguageServer, config); - this.languageServiceListener.ClientConnect += this.OnLanguageServiceClientConnect; + this.languageServiceListener.ClientConnect += this.OnLanguageServiceClientConnectAsync; this.languageServiceListener.Start(); this.logger.Write( @@ -206,7 +201,7 @@ public void StartLanguageService( config.TransportType, config.Endpoint)); } - private async void OnLanguageServiceClientConnect( + private async void OnLanguageServiceClientConnectAsync( object sender, ChannelBase serverChannel) { @@ -236,10 +231,10 @@ private async void OnLanguageServiceClientConnect( this.serverCompletedTask, this.logger); - await this.editorSession.PowerShellContext.ImportCommandsModule( + await this.editorSession.PowerShellContext.ImportCommandsModuleAsync( Path.Combine( Path.GetDirectoryName(this.GetType().GetTypeInfo().Assembly.Location), - @"..\..\Commands")); + @"..\Commands")); this.languageServer.Start(); @@ -252,7 +247,7 @@ await this.editorSession.PowerShellContext.ImportCommandsModule( .AddCommand("Microsoft.PowerShell.Core\\Import-Module") .AddParameter("Name", module); - await this.editorSession.PowerShellContext.ExecuteCommand( + await this.editorSession.PowerShellContext.ExecuteCommandAsync( command, sendOutputToHost: false, sendErrorToHost: true); @@ -378,7 +373,7 @@ private EditorSession CreateSession( bool enableConsoleRepl) { EditorSession editorSession = new EditorSession(this.logger); - PowerShellContext powerShellContext = new PowerShellContext(this.logger); + PowerShellContext powerShellContext = new PowerShellContext(this.logger, this.featureFlags.Contains("PSReadLine")); EditorServicesPSHostUserInterface hostUserInterface = enableConsoleRepl @@ -418,7 +413,9 @@ private EditorSession CreateDebugSession( bool enableConsoleRepl) { EditorSession editorSession = new EditorSession(this.logger); - PowerShellContext powerShellContext = new PowerShellContext(this.logger); + PowerShellContext powerShellContext = new PowerShellContext( + this.logger, + this.featureFlags.Contains("PSReadLine")); EditorServicesPSHostUserInterface hostUserInterface = enableConsoleRepl @@ -452,19 +449,13 @@ private void ProtocolEndpoint_UnhandledException(object sender, Exception e) this.serverCompletedTask.SetException(e); } -#if !CoreCLR private void CurrentDomain_UnhandledException( object sender, UnhandledExceptionEventArgs e) { // Log the exception - this.logger.Write( - LogLevel.Error, - string.Format( - "FATAL UNHANDLED EXCEPTION:\r\n\r\n{0}", - e.ExceptionObject.ToString())); + this.logger.Write(LogLevel.Error, $"FATAL UNHANDLED EXCEPTION: {e.ExceptionObject}"); } -#endif private IServerListener CreateServiceListener(MessageProtocolType protocol, EditorServiceTransportConfig config) { @@ -502,7 +493,6 @@ private IServerListener CreateServiceListener(MessageProtocolType protocol, Edit /// private string GetOSArchitecture() { -#if !CoreCLR // If on win7 (version 6.1.x), avoid System.Runtime.InteropServices.RuntimeInformation if (Environment.OSVersion.Platform == PlatformID.Win32NT && Environment.OSVersion.Version < new Version(6, 2)) { @@ -513,7 +503,7 @@ private string GetOSArchitecture() return "X86"; } -#endif + return RuntimeInformation.OSArchitecture.ToString(); } diff --git a/src/PowerShellEditorServices.Host/PSHost/PromptHandlers.cs b/src/PowerShellEditorServices.Host/PSHost/PromptHandlers.cs index bae68616f..d8ec88c4e 100644 --- a/src/PowerShellEditorServices.Host/PSHost/PromptHandlers.cs +++ b/src/PowerShellEditorServices.Host/PSHost/PromptHandlers.cs @@ -37,7 +37,7 @@ protected override void ShowPrompt(PromptStyle promptStyle) base.ShowPrompt(promptStyle); messageSender - .SendRequest( + .SendRequestAsync( ShowChoicePromptRequest.Type, new ShowChoicePromptRequest { @@ -51,7 +51,7 @@ protected override void ShowPrompt(PromptStyle promptStyle) .ConfigureAwait(false); } - protected override Task ReadInputString(CancellationToken cancellationToken) + protected override Task ReadInputStringAsync(CancellationToken cancellationToken) { this.readLineTask = new TaskCompletionSource(); return this.readLineTask.Task; @@ -120,7 +120,7 @@ protected override void ShowFieldPrompt(FieldDetails fieldDetails) base.ShowFieldPrompt(fieldDetails); messageSender - .SendRequest( + .SendRequestAsync( ShowInputPromptRequest.Type, new ShowInputPromptRequest { @@ -131,7 +131,7 @@ protected override void ShowFieldPrompt(FieldDetails fieldDetails) .ConfigureAwait(false); } - protected override Task ReadInputString(CancellationToken cancellationToken) + protected override Task ReadInputStringAsync(CancellationToken cancellationToken) { this.readLineTask = new TaskCompletionSource(); return this.readLineTask.Task; @@ -176,7 +176,7 @@ private void HandlePromptResponse( this.readLineTask = null; } - protected override Task ReadSecureString(CancellationToken cancellationToken) + protected override Task ReadSecureStringAsync(CancellationToken cancellationToken) { // TODO: Write a message to the console throw new NotImplementedException(); diff --git a/src/PowerShellEditorServices.Host/PSHost/ProtocolPSHostUserInterface.cs b/src/PowerShellEditorServices.Host/PSHost/ProtocolPSHostUserInterface.cs index c9a07e4a6..76b1f7252 100644 --- a/src/PowerShellEditorServices.Host/PSHost/ProtocolPSHostUserInterface.cs +++ b/src/PowerShellEditorServices.Host/PSHost/ProtocolPSHostUserInterface.cs @@ -47,7 +47,7 @@ public void Dispose() // Make sure remaining output is flushed before exiting if (this.outputDebouncer != null) { - this.outputDebouncer.Flush().Wait(); + this.outputDebouncer.FlushAsync().Wait(); this.outputDebouncer = null; } } @@ -82,7 +82,7 @@ public override void WriteOutput( ConsoleColor backgroundColor) { // TODO: This should use a synchronous method! - this.outputDebouncer.Invoke( + this.outputDebouncer.InvokeAsync( new OutputWrittenEventArgs( outputString, includeNewLine, @@ -102,7 +102,7 @@ protected override void UpdateProgress( { } - protected override Task ReadCommandLine(CancellationToken cancellationToken) + protected override Task ReadCommandLineAsync(CancellationToken cancellationToken) { // This currently does nothing because the "evaluate" request // will cancel the current prompt and execute the user's diff --git a/src/PowerShellEditorServices.Host/PowerShellEditorServices.Host.csproj b/src/PowerShellEditorServices.Host/PowerShellEditorServices.Host.csproj index b2d3e934f..309bf484a 100644 --- a/src/PowerShellEditorServices.Host/PowerShellEditorServices.Host.csproj +++ b/src/PowerShellEditorServices.Host/PowerShellEditorServices.Host.csproj @@ -4,30 +4,19 @@ PowerShell Editor Services Host Process Provides a process for hosting the PowerShell Editor Services library exposed by a JSON message protocol. - netstandard1.6;net452 + netstandard2.0 Microsoft.PowerShell.EditorServices.Host + + + - - - 10.0.3 - - - 6.0.0-alpha13 - - - - - $(DefineConstants);CoreCLR - - - - - - diff --git a/src/PowerShellEditorServices.Host/Symbols/DocumentSymbolFeature.cs b/src/PowerShellEditorServices.Host/Symbols/DocumentSymbolFeature.cs index 3db2a8dc8..da3402bde 100644 --- a/src/PowerShellEditorServices.Host/Symbols/DocumentSymbolFeature.cs +++ b/src/PowerShellEditorServices.Host/Symbols/DocumentSymbolFeature.cs @@ -33,7 +33,7 @@ public DocumentSymbolFeature( messageHandlers.SetRequestHandler( DocumentSymbolRequest.Type, - this.HandleDocumentSymbolRequest); + this.HandleDocumentSymbolRequestAsync); } public static DocumentSymbolFeature Create( @@ -69,7 +69,7 @@ public IEnumerable ProvideDocumentSymbols( .SelectMany(r => r); } - protected async Task HandleDocumentSymbolRequest( + protected async Task HandleDocumentSymbolRequestAsync( DocumentSymbolParams documentSymbolParams, RequestContext requestContext) { @@ -109,7 +109,7 @@ protected async Task HandleDocumentSymbolRequest( symbols = new SymbolInformation[0]; } - await requestContext.SendResult(symbols); + await requestContext.SendResultAsync(symbols); } } } diff --git a/src/PowerShellEditorServices.Protocol/Client/DebugAdapterClientBase.cs b/src/PowerShellEditorServices.Protocol/Client/DebugAdapterClientBase.cs index c90730a6d..b226fd0c4 100644 --- a/src/PowerShellEditorServices.Protocol/Client/DebugAdapterClientBase.cs +++ b/src/PowerShellEditorServices.Protocol/Client/DebugAdapterClientBase.cs @@ -28,12 +28,12 @@ public DebugAdapterClient(ChannelBase clientChannel, ILogger logger) logger); } - public async Task Start() + public async Task StartAsync() { this.protocolEndpoint.Start(); // Initialize the debug adapter - await this.SendRequest( + await this.SendRequestAsync( InitializeRequest.Type, new InitializeRequestArguments { @@ -48,34 +48,34 @@ public void Stop() this.protocolEndpoint.Stop(); } - public async Task LaunchScript(string scriptFilePath) + public async Task LaunchScriptAsync(string scriptFilePath) { - await this.SendRequest( + await this.SendRequestAsync( LaunchRequest.Type, new LaunchRequestArguments { Script = scriptFilePath }, true); - await this.SendRequest( + await this.SendRequestAsync( ConfigurationDoneRequest.Type, null, true); } - public Task SendEvent(NotificationType eventType, TParams eventParams) + public Task SendEventAsync(NotificationType eventType, TParams eventParams) { - return ((IMessageSender)protocolEndpoint).SendEvent(eventType, eventParams); + return ((IMessageSender)protocolEndpoint).SendEventAsync(eventType, eventParams); } - public Task SendRequest(RequestType requestType, TParams requestParams, bool waitForResponse) + public Task SendRequestAsync(RequestType requestType, TParams requestParams, bool waitForResponse) { - return ((IMessageSender)protocolEndpoint).SendRequest(requestType, requestParams, waitForResponse); + return ((IMessageSender)protocolEndpoint).SendRequestAsync(requestType, requestParams, waitForResponse); } - public Task SendRequest(RequestType0 requestType0) + public Task SendRequestAsync(RequestType0 requestType0) { - return ((IMessageSender)protocolEndpoint).SendRequest(requestType0); + return ((IMessageSender)protocolEndpoint).SendRequestAsync(requestType0); } public void SetRequestHandler(RequestType requestType, Func, Task> requestHandler) diff --git a/src/PowerShellEditorServices.Protocol/Client/LanguageClientBase.cs b/src/PowerShellEditorServices.Protocol/Client/LanguageClientBase.cs index c8dc383dc..0ccb3d5b5 100644 --- a/src/PowerShellEditorServices.Protocol/Client/LanguageClientBase.cs +++ b/src/PowerShellEditorServices.Protocol/Client/LanguageClientBase.cs @@ -41,46 +41,46 @@ public Task Start() this.protocolEndpoint.Start(); // Initialize the implementation class - return this.Initialize(); + return this.InitializeAsync(); } - public async Task Stop() + public async Task StopAsync() { - await this.OnStop(); + await this.OnStopAsync(); // First, notify the language server that we're stopping var response = - await this.SendRequest( + await this.SendRequestAsync( ShutdownRequest.Type); - await this.SendEvent(ExitNotification.Type, new object()); + await this.SendEventAsync(ExitNotification.Type, new object()); this.protocolEndpoint.Stop(); } - protected virtual Task OnStop() + protected virtual Task OnStopAsync() { return Task.FromResult(true); } - protected virtual Task Initialize() + protected virtual Task InitializeAsync() { return Task.FromResult(true); } - public Task SendEvent(NotificationType eventType, TParams eventParams) + public Task SendEventAsync(NotificationType eventType, TParams eventParams) { - return ((IMessageSender)protocolEndpoint).SendEvent(eventType, eventParams); + return ((IMessageSender)protocolEndpoint).SendEventAsync(eventType, eventParams); } - public Task SendRequest(RequestType requestType, TParams requestParams, bool waitForResponse) + public Task SendRequestAsync(RequestType requestType, TParams requestParams, bool waitForResponse) { - return ((IMessageSender)protocolEndpoint).SendRequest(requestType, requestParams, waitForResponse); + return ((IMessageSender)protocolEndpoint).SendRequestAsync(requestType, requestParams, waitForResponse); } - public Task SendRequest(RequestType0 requestType0) + public Task SendRequestAsync(RequestType0 requestType0) { - return ((IMessageSender)protocolEndpoint).SendRequest(requestType0); + return ((IMessageSender)protocolEndpoint).SendRequestAsync(requestType0); } public void SetRequestHandler(RequestType requestType, Func, Task> requestHandler) diff --git a/src/PowerShellEditorServices.Protocol/Client/LanguageServiceClient.cs b/src/PowerShellEditorServices.Protocol/Client/LanguageServiceClient.cs index 8878814dd..e2b1491fd 100644 --- a/src/PowerShellEditorServices.Protocol/Client/LanguageServiceClient.cs +++ b/src/PowerShellEditorServices.Protocol/Client/LanguageServiceClient.cs @@ -24,10 +24,10 @@ public LanguageServiceClient(ChannelBase clientChannel, ILogger logger) { } - protected override Task Initialize() + protected override Task InitializeAsync() { // Add handlers for common events - this.SetEventHandler(PublishDiagnosticsNotification.Type, HandlePublishDiagnosticsEvent); + this.SetEventHandler(PublishDiagnosticsNotification.Type, HandlePublishDiagnosticsEventAsync); // Send the 'initialize' request and wait for the response var initializeParams = new InitializeParams @@ -36,7 +36,7 @@ protected override Task Initialize() Capabilities = new ClientCapabilities() }; - return this.SendRequest( + return this.SendRequestAsync( InitializeRequest.Type, initializeParams, true); @@ -58,7 +58,7 @@ protected void OnDiagnosticsReceived(string filePath) #region Private Methods - private Task HandlePublishDiagnosticsEvent( + private Task HandlePublishDiagnosticsEventAsync( PublishDiagnosticsNotification diagnostics, EventContext eventContext) { diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/ShowHelpRequest.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/ShowHelpRequest.cs index ae2922ebd..0d73074d2 100644 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/ShowHelpRequest.cs +++ b/src/PowerShellEditorServices.Protocol/LanguageServer/ShowHelpRequest.cs @@ -9,14 +9,6 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer { - [Obsolete("This class is deprecated. Use ShowHelpRequest instead.")] - public class ShowOnlineHelpRequest - { - public static readonly - RequestType Type = - RequestType.Create("powerShell/showOnlineHelp"); - } - public class ShowHelpRequest { public static readonly diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeClientChannel.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeClientChannel.cs index 3fd6d5b5e..07f2cfe39 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeClientChannel.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeClientChannel.cs @@ -49,7 +49,7 @@ protected override void Shutdown() } } - public static async Task Connect( + public static async Task ConnectAsync( string pipeFile, MessageProtocolType messageProtocolType, ILogger logger) @@ -69,24 +69,7 @@ public static async Task Connect( PipeDirection.InOut, PipeOptions.Asynchronous); -#if CoreCLR await pipeClient.ConnectAsync(); -#else - while (!pipeClient.IsConnected) - { - try - { - // Wait for 500 milliseconds so that we don't tie up the thread - pipeClient.Connect(500); - } - catch (TimeoutException) - { - // Connect timed out, wait and try again - await Task.Delay(1000); - continue; - } - } -#endif var clientChannel = new NamedPipeClientChannel(pipeClient, logger); clientChannel.Start(messageProtocolType); diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioServerChannel.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioServerChannel.cs index 119f42110..95318d860 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioServerChannel.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioServerChannel.cs @@ -27,11 +27,15 @@ public StdioServerChannel(ILogger logger) protected override void Initialize(IMessageSerializer messageSerializer) { -#if !CoreCLR - // Ensure that the console is using UTF-8 encoding - System.Console.InputEncoding = Encoding.UTF8; - System.Console.OutputEncoding = Encoding.UTF8; -#endif + if (System.Console.InputEncoding != Encoding.UTF8) + { + System.Console.InputEncoding = Encoding.UTF8; + } + + if (System.Console.OutputEncoding != Encoding.UTF8) + { + System.Console.OutputEncoding = Encoding.UTF8; + } // Open the standard input/output streams this.inputStream = System.Console.OpenStandardInput(); diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/EventContext.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/EventContext.cs index 7af362f4e..4da757913 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/EventContext.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/EventContext.cs @@ -21,11 +21,11 @@ public EventContext(MessageWriter messageWriter) this.messageWriter = messageWriter; } - public async Task SendEvent( + public async Task SendEventAsync( NotificationType eventType, TParams eventParams) { - await this.messageWriter.WriteEvent( + await this.messageWriter.WriteEventAsync( eventType, eventParams); } diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/IMessageSender.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/IMessageSender.cs index e32556cdc..804bbe74c 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/IMessageSender.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/IMessageSender.cs @@ -9,16 +9,16 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol { public interface IMessageSender { - Task SendEvent( + Task SendEventAsync( NotificationType eventType, TParams eventParams); - Task SendRequest( + Task SendRequestAsync( RequestType requestType, TParams requestParams, bool waitForResponse); - Task SendRequest( + Task SendRequestAsync( RequestType0 requestType0); } } diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageDispatcher.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageDispatcher.cs index a81ee4446..ff7ae487e 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageDispatcher.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageDispatcher.cs @@ -115,7 +115,7 @@ public void SetEventHandler( #region Private Methods - public async Task DispatchMessage( + public async Task DispatchMessageAsync( Message messageToDispatch, MessageWriter messageWriter) { diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageReader.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageReader.cs index dcf7aaa94..fec0d175b 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageReader.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageReader.cs @@ -75,12 +75,12 @@ public MessageReader( #region Public Methods - public async Task ReadMessage() + public async Task ReadMessageAsync() { string messageContent = null; // Do we need to read more data or can we process the existing buffer? - while (!this.needsMoreData || await this.ReadNextChunk()) + while (!this.needsMoreData || await this.ReadNextChunkAsync()) { // Clear the flag since we should have what we need now this.needsMoreData = false; @@ -144,7 +144,7 @@ public async Task ReadMessage() #region Private Methods - private async Task ReadNextChunk() + private async Task ReadNextChunkAsync() { // Do we need to resize the buffer? See if less than 1/4 of the space is left. if (((double)(this.messageBuffer.Length - this.bufferEndOffset) / this.messageBuffer.Length) < 0.25) diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageWriter.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageWriter.cs index f2082efba..5baa40038 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageWriter.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageWriter.cs @@ -49,7 +49,7 @@ public MessageWriter( // TODO: This method should be made protected or private - public async Task WriteMessage(Message messageToWrite) + public async Task WriteMessageAsync(Message messageToWrite) { Validate.IsNotNull("messageToWrite", messageToWrite); @@ -111,7 +111,7 @@ public async Task WriteMessage(Message messageToWrite) } } - public async Task WriteRequest( + public async Task WriteRequestAsync( RequestType requestType, TParams requestParams, int requestId) @@ -122,14 +122,14 @@ public async Task WriteRequest( JToken.FromObject(requestParams, contentSerializer) : null; - await this.WriteMessage( + await this.WriteMessageAsync( Message.Request( requestId.ToString(), requestType.Method, contentObject)); } - public async Task WriteResponse(TResult resultContent, string method, string requestId) + public async Task WriteResponseAsync(TResult resultContent, string method, string requestId) { // Allow null content JToken contentObject = @@ -137,14 +137,14 @@ public async Task WriteResponse(TResult resultContent, string method, s JToken.FromObject(resultContent, contentSerializer) : null; - await this.WriteMessage( + await this.WriteMessageAsync( Message.Response( requestId, method, contentObject)); } - public async Task WriteEvent(NotificationType eventType, TParams eventParams) + public async Task WriteEventAsync(NotificationType eventType, TParams eventParams) { // Allow null content JToken contentObject = @@ -152,7 +152,7 @@ public async Task WriteEvent(NotificationType SendRequest( + public Task SendRequestAsync( RequestType0 requestType0) { - return this.SendRequest( + return this.SendRequestAsync( RequestType.ConvertToRequestType(requestType0), null); } @@ -138,14 +138,14 @@ public Task SendRequest( /// /// /// - public Task SendRequest( + public Task SendRequestAsync( RequestType requestType, TParams requestParams) { - return this.SendRequest(requestType, requestParams, true); + return this.SendRequestAsync(requestType, requestParams, true); } - public async Task SendRequest( + public async Task SendRequestAsync( RequestType requestType, TParams requestParams, bool waitForResponse) @@ -170,7 +170,7 @@ public async Task SendRequest( + await this.protocolChannel.MessageWriter.WriteRequestAsync( requestType, requestParams, this.currentMessageId); @@ -198,7 +198,7 @@ await this.protocolChannel.MessageWriter.WriteRequestThe type of event being sent. /// The event parameters being sent. /// A Task that tracks completion of the send operation. - public Task SendEvent( + public Task SendEventAsync( NotificationType eventType, TParams eventParams) { @@ -221,7 +221,7 @@ public Task SendEvent( this.SynchronizationContext.Post( async (obj) => { - await this.protocolChannel.MessageWriter.WriteEvent( + await this.protocolChannel.MessageWriter.WriteEventAsync( eventType, eventParams); @@ -232,7 +232,7 @@ await this.protocolChannel.MessageWriter.WriteEvent( } else { - return this.protocolChannel.MessageWriter.WriteEvent( + return this.protocolChannel.MessageWriter.WriteEventAsync( eventType, eventParams); } @@ -261,7 +261,7 @@ private void StartMessageLoop() this.messageLoopThread = new AsyncContextThread("Message Dispatcher"); this.messageLoopThread .Run( - () => this.ListenForMessages(this.messageLoopCancellationToken.Token), + () => this.ListenForMessagesAsync(this.messageLoopCancellationToken.Token), this.Logger) .ContinueWith(this.OnListenTaskCompleted); } @@ -311,7 +311,7 @@ private void MessageDispatcher_UnhandledException(object sender, Exception e) #region Private Methods - private async Task ListenForMessages(CancellationToken cancellationToken) + private async Task ListenForMessagesAsync(CancellationToken cancellationToken) { this.SynchronizationContext = SynchronizationContext.Current; @@ -324,7 +324,7 @@ private async Task ListenForMessages(CancellationToken cancellationToken) try { // Read a message from the channel - newMessage = await this.protocolChannel.MessageReader.ReadMessage(); + newMessage = await this.protocolChannel.MessageReader.ReadMessageAsync(); } catch (MessageParseException e) { @@ -376,7 +376,7 @@ private async Task ListenForMessages(CancellationToken cancellationToken) else { // Process the message - await this.messageDispatcher.DispatchMessage( + await this.messageDispatcher.DispatchMessageAsync( newMessage, this.protocolChannel.MessageWriter); } diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/RequestContext.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/RequestContext.cs index 55c473f01..94c9264b7 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/RequestContext.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/RequestContext.cs @@ -19,24 +19,24 @@ public RequestContext(Message requestMessage, MessageWriter messageWriter) this.messageWriter = messageWriter; } - public async Task SendResult(TResult resultDetails) + public async Task SendResultAsync(TResult resultDetails) { - await this.messageWriter.WriteResponse( + await this.messageWriter.WriteResponseAsync( resultDetails, requestMessage.Method, requestMessage.Id); } - public async Task SendEvent(NotificationType eventType, TParams eventParams) + public async Task SendEventAsync(NotificationType eventType, TParams eventParams) { - await this.messageWriter.WriteEvent( + await this.messageWriter.WriteEventAsync( eventType, eventParams); } - public async Task SendError(object errorDetails) + public async Task SendErrorAsync(object errorDetails) { - await this.messageWriter.WriteMessage( + await this.messageWriter.WriteMessageAsync( Message.ResponseError( requestMessage.Id, requestMessage.Method, diff --git a/src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj b/src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj index d33b55ee2..5d663043d 100644 --- a/src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj +++ b/src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj @@ -3,25 +3,19 @@ PowerShell Editor Services Host Protocol Library Provides message types and client/server APIs for the PowerShell Editor Services JSON protocol. - netstandard1.6;net452; + netstandard2.0 Microsoft.PowerShell.EditorServices.Protocol + - - - - - - - - - $(DefineConstants);CoreCLR - - - - + + + diff --git a/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs b/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs index a43f5a7d3..69a720f8e 100644 --- a/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs +++ b/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs @@ -63,33 +63,33 @@ public DebugAdapter( public void Start() { // Register all supported message types - _messageHandlers.SetRequestHandler(InitializeRequest.Type, HandleInitializeRequest); - - _messageHandlers.SetRequestHandler(LaunchRequest.Type, HandleLaunchRequest); - _messageHandlers.SetRequestHandler(AttachRequest.Type, HandleAttachRequest); - _messageHandlers.SetRequestHandler(ConfigurationDoneRequest.Type, HandleConfigurationDoneRequest); - _messageHandlers.SetRequestHandler(DisconnectRequest.Type, HandleDisconnectRequest); - - _messageHandlers.SetRequestHandler(SetBreakpointsRequest.Type, HandleSetBreakpointsRequest); - _messageHandlers.SetRequestHandler(SetExceptionBreakpointsRequest.Type, HandleSetExceptionBreakpointsRequest); - _messageHandlers.SetRequestHandler(SetFunctionBreakpointsRequest.Type, HandleSetFunctionBreakpointsRequest); - - _messageHandlers.SetRequestHandler(ContinueRequest.Type, HandleContinueRequest); - _messageHandlers.SetRequestHandler(NextRequest.Type, HandleNextRequest); - _messageHandlers.SetRequestHandler(StepInRequest.Type, HandleStepInRequest); - _messageHandlers.SetRequestHandler(StepOutRequest.Type, HandleStepOutRequest); - _messageHandlers.SetRequestHandler(PauseRequest.Type, HandlePauseRequest); - - _messageHandlers.SetRequestHandler(ThreadsRequest.Type, HandleThreadsRequest); - _messageHandlers.SetRequestHandler(StackTraceRequest.Type, HandleStackTraceRequest); - _messageHandlers.SetRequestHandler(ScopesRequest.Type, HandleScopesRequest); - _messageHandlers.SetRequestHandler(VariablesRequest.Type, HandleVariablesRequest); - _messageHandlers.SetRequestHandler(SetVariableRequest.Type, HandleSetVariablesRequest); - _messageHandlers.SetRequestHandler(SourceRequest.Type, HandleSourceRequest); - _messageHandlers.SetRequestHandler(EvaluateRequest.Type, HandleEvaluateRequest); + _messageHandlers.SetRequestHandler(InitializeRequest.Type, HandleInitializeRequestAsync); + + _messageHandlers.SetRequestHandler(LaunchRequest.Type, HandleLaunchRequestAsync); + _messageHandlers.SetRequestHandler(AttachRequest.Type, HandleAttachRequestAsync); + _messageHandlers.SetRequestHandler(ConfigurationDoneRequest.Type, HandleConfigurationDoneRequestAsync); + _messageHandlers.SetRequestHandler(DisconnectRequest.Type, HandleDisconnectRequestAsync); + + _messageHandlers.SetRequestHandler(SetBreakpointsRequest.Type, HandleSetBreakpointsRequestAsync); + _messageHandlers.SetRequestHandler(SetExceptionBreakpointsRequest.Type, HandleSetExceptionBreakpointsRequestAsync); + _messageHandlers.SetRequestHandler(SetFunctionBreakpointsRequest.Type, HandleSetFunctionBreakpointsRequestAsync); + + _messageHandlers.SetRequestHandler(ContinueRequest.Type, HandleContinueRequestAsync); + _messageHandlers.SetRequestHandler(NextRequest.Type, HandleNextRequestAsync); + _messageHandlers.SetRequestHandler(StepInRequest.Type, HandleStepInRequestAsync); + _messageHandlers.SetRequestHandler(StepOutRequest.Type, HandleStepOutRequestAsync); + _messageHandlers.SetRequestHandler(PauseRequest.Type, HandlePauseRequestAsync); + + _messageHandlers.SetRequestHandler(ThreadsRequest.Type, HandleThreadsRequestAsync); + _messageHandlers.SetRequestHandler(StackTraceRequest.Type, HandleStackTraceRequestAsync); + _messageHandlers.SetRequestHandler(ScopesRequest.Type, HandleScopesRequestAsync); + _messageHandlers.SetRequestHandler(VariablesRequest.Type, HandleVariablesRequestAsync); + _messageHandlers.SetRequestHandler(SetVariableRequest.Type, HandleSetVariablesRequestAsync); + _messageHandlers.SetRequestHandler(SourceRequest.Type, HandleSourceRequestAsync); + _messageHandlers.SetRequestHandler(EvaluateRequest.Type, HandleEvaluateRequestAsync); } - protected Task LaunchScript(RequestContext requestContext, string scriptToLaunch) + protected Task LaunchScriptAsync(RequestContext requestContext, string scriptToLaunch) { // Is this an untitled script? Task launchTask = null; @@ -99,19 +99,30 @@ protected Task LaunchScript(RequestContext requestContext, string script ScriptFile untitledScript = _editorSession.Workspace.GetFile(scriptToLaunch); launchTask = _editorSession.PowerShellContext - .ExecuteScriptString(untitledScript.Contents, true, true); + .ExecuteScriptStringAsync(untitledScript.Contents, true, true); } else { launchTask = _editorSession.PowerShellContext - .ExecuteScriptWithArgs(scriptToLaunch, _arguments, writeInputToHost: true); + .ExecuteScriptWithArgsAsync(scriptToLaunch, _arguments, writeInputToHost: true); } - return launchTask.ContinueWith(OnExecutionCompleted); + return launchTask.ContinueWith(OnExecutionCompletedAsync); } - private async Task OnExecutionCompleted(Task executeTask) + private async Task OnExecutionCompletedAsync(Task executeTask) { + try + { + await executeTask; + } + catch (Exception e) + { + Logger.Write( + LogLevel.Error, + "Exception occurred while awaiting debug launch task.\n\n" + e.ToString()); + } + Logger.Write(LogLevel.Verbose, "Execution completed, terminating..."); _executionCompleted = true; @@ -125,12 +136,12 @@ private async Task OnExecutionCompleted(Task executeTask) { try { - await _editorSession.PowerShellContext.ExecuteScriptString("Exit-PSHostProcess"); + await _editorSession.PowerShellContext.ExecuteScriptStringAsync("Exit-PSHostProcess"); if (_isRemoteAttach && _editorSession.PowerShellContext.CurrentRunspace.Location == RunspaceLocation.Remote) { - await _editorSession.PowerShellContext.ExecuteScriptString("Exit-PSSession"); + await _editorSession.PowerShellContext.ExecuteScriptStringAsync("Exit-PSSession"); } } catch (Exception e) @@ -145,12 +156,12 @@ private async Task OnExecutionCompleted(Task executeTask) if (_disconnectRequestContext != null) { // Respond to the disconnect request and stop the server - await _disconnectRequestContext.SendResult(null); + await _disconnectRequestContext.SendResultAsync(null); Stop(); } else { - await _messageSender.SendEvent( + await _messageSender.SendEventAsync( TerminatedEvent.Type, new TerminatedEvent()); } @@ -162,9 +173,9 @@ protected void Stop() if (_editorSession != null) { - _editorSession.PowerShellContext.RunspaceChanged -= powerShellContext_RunspaceChanged; - _editorSession.DebugService.DebuggerStopped -= DebugService_DebuggerStopped; - _editorSession.PowerShellContext.DebuggerResumed -= powerShellContext_DebuggerResumed; + _editorSession.PowerShellContext.RunspaceChanged -= powerShellContext_RunspaceChangedAsync; + _editorSession.DebugService.DebuggerStopped -= DebugService_DebuggerStoppedAsync; + _editorSession.PowerShellContext.DebuggerResumed -= powerShellContext_DebuggerResumedAsync; if (_ownsEditorSession) { @@ -179,15 +190,15 @@ protected void Stop() #region Built-in Message Handlers - private async Task HandleInitializeRequest( + private async Task HandleInitializeRequestAsync( object shutdownParams, RequestContext requestContext) { // Clear any existing breakpoints before proceeding - await ClearSessionBreakpoints(); + await ClearSessionBreakpointsAsync(); // Now send the Initialize response to continue setup - await requestContext.SendResult( + await requestContext.SendResultAsync( new InitializeResponseBody { SupportsConfigurationDoneRequest = true, SupportsFunctionBreakpoints = true, @@ -197,7 +208,7 @@ await requestContext.SendResult( }); } - protected async Task HandleConfigurationDoneRequest( + protected async Task HandleConfigurationDoneRequestAsync( object args, RequestContext requestContext) { @@ -208,7 +219,7 @@ protected async Task HandleConfigurationDoneRequest( if (_editorSession.PowerShellContext.SessionState == PowerShellContextState.Ready) { // Configuration is done, launch the script - var nonAwaitedTask = LaunchScript(requestContext, _scriptToLaunch) + var nonAwaitedTask = LaunchScriptAsync(requestContext, _scriptToLaunch) .ConfigureAwait(continueOnCapturedContext: false); } else @@ -219,7 +230,7 @@ protected async Task HandleConfigurationDoneRequest( } } - await requestContext.SendResult(null); + await requestContext.SendResultAsync(null); if (_isInteractiveDebugSession) { @@ -234,14 +245,14 @@ protected async Task HandleConfigurationDoneRequest( { // If this is an interactive session and there's a pending breakpoint, // send that information along to the debugger client - DebugService_DebuggerStopped( + DebugService_DebuggerStoppedAsync( this, _editorSession.DebugService.CurrentDebuggerStoppedEventArgs); } } } - protected async Task HandleLaunchRequest( + protected async Task HandleLaunchRequestAsync( LaunchRequestArguments launchParams, RequestContext requestContext) { @@ -279,12 +290,7 @@ protected async Task HandleLaunchRequest( // pick some reasonable default. if (string.IsNullOrEmpty(workingDir) && launchParams.CreateTemporaryIntegratedConsole) { -#if CoreCLR - //TODO: RKH 2018-06-26 .NET standard 2.0 has added Environment.CurrentDirectory - let's use it. - workingDir = AppContext.BaseDirectory; -#else workingDir = Environment.CurrentDirectory; -#endif } // At this point, we will either have a working dir that should be set to cwd in @@ -292,7 +298,7 @@ protected async Task HandleLaunchRequest( // the working dir should not be changed. if (!string.IsNullOrEmpty(workingDir)) { - await _editorSession.PowerShellContext.SetWorkingDirectory(workingDir, isPathAlreadyEscaped: false); + await _editorSession.PowerShellContext.SetWorkingDirectoryAsync(workingDir, isPathAlreadyEscaped: false); } Logger.Write(LogLevel.Verbose, $"Working dir " + (string.IsNullOrEmpty(workingDir) ? "not set." : $"set to '{workingDir}'")); @@ -323,7 +329,7 @@ protected async Task HandleLaunchRequest( _editorSession.PowerShellContext.CurrentRunspace); } - await requestContext.SendResult(null); + await requestContext.SendResultAsync(null); // If no script is being launched, mark this as an interactive // debugging session @@ -331,12 +337,12 @@ protected async Task HandleLaunchRequest( // Send the InitializedEvent so that the debugger will continue // sending configuration requests - await _messageSender.SendEvent( + await _messageSender.SendEventAsync( InitializedEvent.Type, null); } - protected async Task HandleAttachRequest( + protected async Task HandleAttachRequestAsync( AttachRequestArguments attachParams, RequestContext requestContext) { @@ -354,7 +360,7 @@ protected async Task HandleAttachRequest( LogLevel.Normal, $"Attach request aborted, received {attachParams.ProcessId} for processId."); - await requestContext.SendError( + await requestContext.SendErrorAsync( "User aborted attach to PowerShell host process."); return; @@ -369,26 +375,26 @@ await requestContext.SendError( if (runspaceVersion.Version.Major < 4) { - await requestContext.SendError( + await requestContext.SendErrorAsync( $"Remote sessions are only available with PowerShell 4 and higher (current session is {runspaceVersion.Version})."); return; } else if (_editorSession.PowerShellContext.CurrentRunspace.Location == RunspaceLocation.Remote) { - await requestContext.SendError( + await requestContext.SendErrorAsync( $"Cannot attach to a process in a remote session when already in a remote session."); return; } - await _editorSession.PowerShellContext.ExecuteScriptString( + await _editorSession.PowerShellContext.ExecuteScriptStringAsync( $"Enter-PSSession -ComputerName \"{attachParams.ComputerName}\"", errorMessages); if (errorMessages.Length > 0) { - await requestContext.SendError( + await requestContext.SendErrorAsync( $"Could not establish remote session to computer '{attachParams.ComputerName}'"); return; @@ -404,26 +410,26 @@ await requestContext.SendError( if (runspaceVersion.Version.Major < 5) { - await requestContext.SendError( + await requestContext.SendErrorAsync( $"Attaching to a process is only available with PowerShell 5 and higher (current session is {runspaceVersion.Version})."); return; } - await _editorSession.PowerShellContext.ExecuteScriptString( + await _editorSession.PowerShellContext.ExecuteScriptStringAsync( $"Enter-PSHostProcess -Id {processId}", errorMessages); if (errorMessages.Length > 0) { - await requestContext.SendError( + await requestContext.SendErrorAsync( $"Could not attach to process '{processId}'"); return; } // Clear any existing breakpoints before proceeding - await ClearSessionBreakpoints(); + await ClearSessionBreakpointsAsync(); // Execute the Debug-Runspace command but don't await it because it // will block the debug adapter initialization process. The @@ -433,8 +439,8 @@ await requestContext.SendError( _waitingForAttach = true; Task nonAwaitedTask = _editorSession.PowerShellContext - .ExecuteScriptString($"\nDebug-Runspace -Id {runspaceId}") - .ContinueWith(OnExecutionCompleted); + .ExecuteScriptStringAsync($"\nDebug-Runspace -Id {runspaceId}") + .ContinueWith(OnExecutionCompletedAsync); } else { @@ -442,16 +448,16 @@ await requestContext.SendError( LogLevel.Error, $"Attach request failed, '{attachParams.ProcessId}' is an invalid value for the processId."); - await requestContext.SendError( + await requestContext.SendErrorAsync( "A positive integer must be specified for the processId field."); return; } - await requestContext.SendResult(null); + await requestContext.SendResultAsync(null); } - protected async Task HandleDisconnectRequest( + protected async Task HandleDisconnectRequestAsync( object disconnectParams, RequestContext requestContext) { @@ -462,24 +468,24 @@ protected async Task HandleDisconnectRequest( if (_executionCompleted == false) { _disconnectRequestContext = requestContext; - _editorSession.PowerShellContext.AbortExecution(); + _editorSession.PowerShellContext.AbortExecution(shouldAbortDebugSession: true); if (_isInteractiveDebugSession) { - await OnExecutionCompleted(null); + await OnExecutionCompletedAsync(null); } } else { UnregisterEventHandlers(); - await requestContext.SendResult(null); + await requestContext.SendResultAsync(null); Stop(); } } } - protected async Task HandleSetBreakpointsRequest( + protected async Task HandleSetBreakpointsRequestAsync( SetBreakpointsRequestArguments setBreakpointsParams, RequestContext requestContext) { @@ -498,7 +504,7 @@ protected async Task HandleSetBreakpointsRequest( srcBkpt, setBreakpointsParams.Source.Path, message, verified: _noDebug)); // Return non-verified breakpoint message. - await requestContext.SendResult( + await requestContext.SendResultAsync( new SetBreakpointsResponseBody { Breakpoints = srcBreakpoints.ToArray() }); @@ -521,7 +527,7 @@ await requestContext.SendResult( srcBkpt, setBreakpointsParams.Source.Path, message, verified: _noDebug)); // Return non-verified breakpoint message. - await requestContext.SendResult( + await requestContext.SendResultAsync( new SetBreakpointsResponseBody { Breakpoints = srcBreakpoints.ToArray() @@ -552,7 +558,7 @@ await requestContext.SendResult( try { updatedBreakpointDetails = - await _editorSession.DebugService.SetLineBreakpoints( + await _editorSession.DebugService.SetLineBreakpointsAsync( scriptFile, breakpointDetails); } @@ -567,7 +573,7 @@ await _editorSession.DebugService.SetLineBreakpoints( } } - await requestContext.SendResult( + await requestContext.SendResultAsync( new SetBreakpointsResponseBody { Breakpoints = updatedBreakpointDetails @@ -576,7 +582,7 @@ await requestContext.SendResult( }); } - protected async Task HandleSetFunctionBreakpointsRequest( + protected async Task HandleSetFunctionBreakpointsRequestAsync( SetFunctionBreakpointsRequestArguments setBreakpointsParams, RequestContext requestContext) { @@ -599,7 +605,7 @@ protected async Task HandleSetFunctionBreakpointsRequest( try { updatedBreakpointDetails = - await _editorSession.DebugService.SetCommandBreakpoints( + await _editorSession.DebugService.SetCommandBreakpointsAsync( breakpointDetails); } catch (Exception e) @@ -613,7 +619,7 @@ await _editorSession.DebugService.SetCommandBreakpoints( } } - await requestContext.SendResult( + await requestContext.SendResultAsync( new SetBreakpointsResponseBody { Breakpoints = updatedBreakpointDetails @@ -622,7 +628,7 @@ await requestContext.SendResult( }); } - protected async Task HandleSetExceptionBreakpointsRequest( + protected async Task HandleSetExceptionBreakpointsRequestAsync( SetExceptionBreakpointsRequestArguments setExceptionBreakpointsParams, RequestContext requestContext) { @@ -648,28 +654,28 @@ protected async Task HandleSetExceptionBreakpointsRequest( // } //} - await requestContext.SendResult(null); + await requestContext.SendResultAsync(null); } - protected async Task HandleContinueRequest( + protected async Task HandleContinueRequestAsync( object continueParams, RequestContext requestContext) { _editorSession.DebugService.Continue(); - await requestContext.SendResult(null); + await requestContext.SendResultAsync(null); } - protected async Task HandleNextRequest( + protected async Task HandleNextRequestAsync( object nextParams, RequestContext requestContext) { _editorSession.DebugService.StepOver(); - await requestContext.SendResult(null); + await requestContext.SendResultAsync(null); } - protected Task HandlePauseRequest( + protected Task HandlePauseRequestAsync( object pauseParams, RequestContext requestContext) { @@ -679,36 +685,36 @@ protected Task HandlePauseRequest( } catch (NotSupportedException e) { - return requestContext.SendError(e.Message); + return requestContext.SendErrorAsync(e.Message); } // This request is responded to by sending the "stopped" event return Task.FromResult(true); } - protected async Task HandleStepInRequest( + protected async Task HandleStepInRequestAsync( object stepInParams, RequestContext requestContext) { _editorSession.DebugService.StepIn(); - await requestContext.SendResult(null); + await requestContext.SendResultAsync(null); } - protected async Task HandleStepOutRequest( + protected async Task HandleStepOutRequestAsync( object stepOutParams, RequestContext requestContext) { _editorSession.DebugService.StepOut(); - await requestContext.SendResult(null); + await requestContext.SendResultAsync(null); } - protected async Task HandleThreadsRequest( + protected async Task HandleThreadsRequestAsync( object threadsParams, RequestContext requestContext) { - await requestContext.SendResult( + await requestContext.SendResultAsync( new ThreadsResponseBody { Threads = new Thread[] @@ -723,13 +729,27 @@ await requestContext.SendResult( }); } - protected async Task HandleStackTraceRequest( + protected async Task HandleStackTraceRequestAsync( StackTraceRequestArguments stackTraceParams, RequestContext requestContext) { StackFrameDetails[] stackFrames = _editorSession.DebugService.GetStackFrames(); + // Handle a rare race condition where the adapter requests stack frames before they've + // begun building. + if (stackFrames == null) + { + await requestContext.SendResultAsync( + new StackTraceResponseBody + { + StackFrames = new StackFrame[0], + TotalFrames = 0 + }); + + return; + } + List newStackFrames = new List(); int startFrameIndex = stackTraceParams.StartFrame ?? 0; @@ -753,7 +773,7 @@ protected async Task HandleStackTraceRequest( i)); } - await requestContext.SendResult( + await requestContext.SendResultAsync( new StackTraceResponseBody { StackFrames = newStackFrames.ToArray(), @@ -761,7 +781,7 @@ await requestContext.SendResult( }); } - protected async Task HandleScopesRequest( + protected async Task HandleScopesRequestAsync( ScopesRequestArguments scopesParams, RequestContext requestContext) { @@ -769,7 +789,7 @@ protected async Task HandleScopesRequest( _editorSession.DebugService.GetVariableScopes( scopesParams.FrameId); - await requestContext.SendResult( + await requestContext.SendResultAsync( new ScopesResponseBody { Scopes = @@ -779,7 +799,7 @@ await requestContext.SendResult( }); } - protected async Task HandleVariablesRequest( + protected async Task HandleVariablesRequestAsync( VariablesRequestArguments variablesParams, RequestContext requestContext) { @@ -804,17 +824,17 @@ protected async Task HandleVariablesRequest( // TODO: This shouldn't be so broad } - await requestContext.SendResult(variablesResponse); + await requestContext.SendResultAsync(variablesResponse); } - protected async Task HandleSetVariablesRequest( + protected async Task HandleSetVariablesRequestAsync( SetVariableRequestArguments setVariableParams, RequestContext requestContext) { try { string updatedValue = - await _editorSession.DebugService.SetVariable( + await _editorSession.DebugService.SetVariableAsync( setVariableParams.VariablesReference, setVariableParams.Name, setVariableParams.Value); @@ -824,7 +844,7 @@ await _editorSession.DebugService.SetVariable( Value = updatedValue }; - await requestContext.SendResult(setVariableResponse); + await requestContext.SendResultAsync(setVariableResponse); } catch (Exception ex) when (ex is ArgumentTransformationMetadataException || ex is InvalidPowerShellExpressionException || @@ -832,18 +852,18 @@ ex is InvalidPowerShellExpressionException || { // Catch common, innocuous errors caused by the user supplying a value that can't be converted or the variable is not settable. Logger.Write(LogLevel.Verbose, $"Failed to set variable: {ex.Message}"); - await requestContext.SendError(ex.Message); + await requestContext.SendErrorAsync(ex.Message); } catch (Exception ex) { Logger.Write(LogLevel.Error, $"Unexpected error setting variable: {ex.Message}"); string msg = $"Unexpected error: {ex.GetType().Name} - {ex.Message} Please report this error to the PowerShellEditorServices project on GitHub."; - await requestContext.SendError(msg); + await requestContext.SendErrorAsync(msg); } } - protected Task HandleSourceRequest( + protected Task HandleSourceRequestAsync( SourceRequestArguments sourceParams, RequestContext requestContext) { @@ -853,7 +873,7 @@ protected Task HandleSourceRequest( return Task.FromResult(true); } - protected async Task HandleEvaluateRequest( + protected async Task HandleEvaluateRequestAsync( EvaluateRequestArguments evaluateParams, RequestContext requestContext) { @@ -871,7 +891,7 @@ protected async Task HandleEvaluateRequest( var notAwaited = _editorSession .PowerShellContext - .ExecuteScriptString(evaluateParams.Expression, false, true) + .ExecuteScriptStringAsync(evaluateParams.Expression, false, true) .ConfigureAwait(false); } else @@ -890,7 +910,7 @@ protected async Task HandleEvaluateRequest( if (result == null) { result = - await _editorSession.DebugService.EvaluateExpression( + await _editorSession.DebugService.EvaluateExpressionAsync( evaluateParams.Expression, evaluateParams.FrameId, isFromRepl); @@ -906,7 +926,7 @@ await _editorSession.DebugService.EvaluateExpression( } } - await requestContext.SendResult( + await requestContext.SendResultAsync( new EvaluateResponseBody { Result = valueString, @@ -914,9 +934,9 @@ await requestContext.SendResult( }); } - private async Task WriteUseIntegratedConsoleMessage() + private async Task WriteUseIntegratedConsoleMessageAsync() { - await _messageSender.SendEvent( + await _messageSender.SendEventAsync( OutputEvent.Type, new OutputEventBody { @@ -927,25 +947,25 @@ await _messageSender.SendEvent( private void RegisterEventHandlers() { - _editorSession.PowerShellContext.RunspaceChanged += powerShellContext_RunspaceChanged; - _editorSession.DebugService.BreakpointUpdated += DebugService_BreakpointUpdated; - _editorSession.DebugService.DebuggerStopped += DebugService_DebuggerStopped; - _editorSession.PowerShellContext.DebuggerResumed += powerShellContext_DebuggerResumed; + _editorSession.PowerShellContext.RunspaceChanged += powerShellContext_RunspaceChangedAsync; + _editorSession.DebugService.BreakpointUpdated += DebugService_BreakpointUpdatedAsync; + _editorSession.DebugService.DebuggerStopped += DebugService_DebuggerStoppedAsync; + _editorSession.PowerShellContext.DebuggerResumed += powerShellContext_DebuggerResumedAsync; } private void UnregisterEventHandlers() { - _editorSession.PowerShellContext.RunspaceChanged -= powerShellContext_RunspaceChanged; - _editorSession.DebugService.BreakpointUpdated -= DebugService_BreakpointUpdated; - _editorSession.DebugService.DebuggerStopped -= DebugService_DebuggerStopped; - _editorSession.PowerShellContext.DebuggerResumed -= powerShellContext_DebuggerResumed; + _editorSession.PowerShellContext.RunspaceChanged -= powerShellContext_RunspaceChangedAsync; + _editorSession.DebugService.BreakpointUpdated -= DebugService_BreakpointUpdatedAsync; + _editorSession.DebugService.DebuggerStopped -= DebugService_DebuggerStoppedAsync; + _editorSession.PowerShellContext.DebuggerResumed -= powerShellContext_DebuggerResumedAsync; } - private async Task ClearSessionBreakpoints() + private async Task ClearSessionBreakpointsAsync() { try { - await _editorSession.DebugService.ClearAllBreakpoints(); + await _editorSession.DebugService.ClearAllBreakpointsAsync(); } catch (Exception e) { @@ -957,7 +977,7 @@ private async Task ClearSessionBreakpoints() #region Event Handlers - async void DebugService_DebuggerStopped(object sender, DebuggerStoppedEventArgs e) + async void DebugService_DebuggerStoppedAsync(object sender, DebuggerStoppedEventArgs e) { // Provide the reason for why the debugger has stopped script execution. // See https://github.com/Microsoft/vscode/issues/3648 @@ -974,7 +994,7 @@ e.OriginalEvent.Breakpoints[0] is CommandBreakpoint : "breakpoint"; } - await _messageSender.SendEvent( + await _messageSender.SendEventAsync( StoppedEvent.Type, new StoppedEventBody { @@ -987,7 +1007,7 @@ await _messageSender.SendEvent( }); } - async void powerShellContext_RunspaceChanged(object sender, RunspaceChangedEventArgs e) + async void powerShellContext_RunspaceChangedAsync(object sender, RunspaceChangedEventArgs e) { if (_waitingForAttach && e.ChangeAction == RunspaceChangeAction.Enter && @@ -996,7 +1016,7 @@ async void powerShellContext_RunspaceChanged(object sender, RunspaceChangedEvent // Send the InitializedEvent so that the debugger will continue // sending configuration requests _waitingForAttach = false; - await _messageSender.SendEvent(InitializedEvent.Type, null); + await _messageSender.SendEventAsync(InitializedEvent.Type, null); } else if ( e.ChangeAction == RunspaceChangeAction.Exit && @@ -1006,7 +1026,7 @@ async void powerShellContext_RunspaceChanged(object sender, RunspaceChangedEvent // Exited the session while the debugger is stopped, // send a ContinuedEvent so that the client changes the // UI to appear to be running again - await _messageSender.SendEvent( + await _messageSender.SendEventAsync( ContinuedEvent.Type, new ContinuedEvent { @@ -1016,9 +1036,9 @@ await _messageSender.SendEvent( } } - private async void powerShellContext_DebuggerResumed(object sender, DebuggerResumeAction e) + private async void powerShellContext_DebuggerResumedAsync(object sender, DebuggerResumeAction e) { - await _messageSender.SendEvent( + await _messageSender.SendEventAsync( ContinuedEvent.Type, new ContinuedEvent { @@ -1027,7 +1047,7 @@ await _messageSender.SendEvent( }); } - private async void DebugService_BreakpointUpdated(object sender, BreakpointUpdatedEventArgs e) + private async void DebugService_BreakpointUpdatedAsync(object sender, BreakpointUpdatedEventArgs e) { string reason = "changed"; @@ -1068,7 +1088,7 @@ private async void DebugService_BreakpointUpdated(object sender, BreakpointUpdat breakpoint.Verified = e.UpdateType != BreakpointUpdateType.Disabled; - await _messageSender.SendEvent( + await _messageSender.SendEventAsync( BreakpointEvent.Type, new BreakpointEvent { diff --git a/src/PowerShellEditorServices.Protocol/Server/IMessageDispatcher.cs b/src/PowerShellEditorServices.Protocol/Server/IMessageDispatcher.cs index 45342a21b..156acb9b2 100644 --- a/src/PowerShellEditorServices.Protocol/Server/IMessageDispatcher.cs +++ b/src/PowerShellEditorServices.Protocol/Server/IMessageDispatcher.cs @@ -10,7 +10,7 @@ namespace Microsoft.PowerShell.EditorServices.Protocol { public interface IMessageDispatcher { - Task DispatchMessage( + Task DispatchMessageAsync( Message messageToDispatch, MessageWriter messageWriter); } diff --git a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs index 0432fa2ee..d325b2d65 100644 --- a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs +++ b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs @@ -80,13 +80,13 @@ public LanguageServer( this.editorSession = editorSession; this.serverCompletedTask = serverCompletedTask; // Attach to the underlying PowerShell context to listen for changes in the runspace or execution status - this.editorSession.PowerShellContext.RunspaceChanged += PowerShellContext_RunspaceChanged; - this.editorSession.PowerShellContext.ExecutionStatusChanged += PowerShellContext_ExecutionStatusChanged; + this.editorSession.PowerShellContext.RunspaceChanged += PowerShellContext_RunspaceChangedAsync; + this.editorSession.PowerShellContext.ExecutionStatusChanged += PowerShellContext_ExecutionStatusChangedAsync; // Attach to ExtensionService events - this.editorSession.ExtensionService.CommandAdded += ExtensionService_ExtensionAdded; - this.editorSession.ExtensionService.CommandUpdated += ExtensionService_ExtensionUpdated; - this.editorSession.ExtensionService.CommandRemoved += ExtensionService_ExtensionRemoved; + this.editorSession.ExtensionService.CommandAdded += ExtensionService_ExtensionAddedAsync; + this.editorSession.ExtensionService.CommandUpdated += ExtensionService_ExtensionUpdatedAsync; + this.editorSession.ExtensionService.CommandRemoved += ExtensionService_ExtensionRemovedAsync; this.messageSender = messageSender; this.messageHandlers = messageHandlers; @@ -98,7 +98,7 @@ public LanguageServer( this.messageSender); this.editorSession.StartDebugService(this.editorOperations); - this.editorSession.DebugService.DebuggerStopped += DebugService_DebuggerStopped; + this.editorSession.DebugService.DebuggerStopped += DebugService_DebuggerStoppedAsync; } /// @@ -109,62 +109,61 @@ public void Start() { // Register all supported message types - this.messageHandlers.SetRequestHandler(ShutdownRequest.Type, this.HandleShutdownRequest); - this.messageHandlers.SetEventHandler(ExitNotification.Type, this.HandleExitNotification); - - this.messageHandlers.SetRequestHandler(InitializeRequest.Type, this.HandleInitializeRequest); - this.messageHandlers.SetEventHandler(InitializedNotification.Type, this.HandleInitializedNotification); - - this.messageHandlers.SetEventHandler(DidOpenTextDocumentNotification.Type, this.HandleDidOpenTextDocumentNotification); - this.messageHandlers.SetEventHandler(DidCloseTextDocumentNotification.Type, this.HandleDidCloseTextDocumentNotification); - this.messageHandlers.SetEventHandler(DidSaveTextDocumentNotification.Type, this.HandleDidSaveTextDocumentNotification); - this.messageHandlers.SetEventHandler(DidChangeTextDocumentNotification.Type, this.HandleDidChangeTextDocumentNotification); - this.messageHandlers.SetEventHandler(DidChangeConfigurationNotification.Type, this.HandleDidChangeConfigurationNotification); - - this.messageHandlers.SetRequestHandler(DefinitionRequest.Type, this.HandleDefinitionRequest); - this.messageHandlers.SetRequestHandler(ReferencesRequest.Type, this.HandleReferencesRequest); - this.messageHandlers.SetRequestHandler(CompletionRequest.Type, this.HandleCompletionRequest); - this.messageHandlers.SetRequestHandler(CompletionResolveRequest.Type, this.HandleCompletionResolveRequest); - this.messageHandlers.SetRequestHandler(SignatureHelpRequest.Type, this.HandleSignatureHelpRequest); - this.messageHandlers.SetRequestHandler(DocumentHighlightRequest.Type, this.HandleDocumentHighlightRequest); - this.messageHandlers.SetRequestHandler(HoverRequest.Type, this.HandleHoverRequest); - this.messageHandlers.SetRequestHandler(WorkspaceSymbolRequest.Type, this.HandleWorkspaceSymbolRequest); - this.messageHandlers.SetRequestHandler(CodeActionRequest.Type, this.HandleCodeActionRequest); - this.messageHandlers.SetRequestHandler(DocumentFormattingRequest.Type, this.HandleDocumentFormattingRequest); + this.messageHandlers.SetRequestHandler(ShutdownRequest.Type, this.HandleShutdownRequestAsync); + this.messageHandlers.SetEventHandler(ExitNotification.Type, this.HandleExitNotificationAsync); + + this.messageHandlers.SetRequestHandler(InitializeRequest.Type, this.HandleInitializeRequestAsync); + this.messageHandlers.SetEventHandler(InitializedNotification.Type, this.HandleInitializedNotificationAsync); + + this.messageHandlers.SetEventHandler(DidOpenTextDocumentNotification.Type, this.HandleDidOpenTextDocumentNotificationAsync); + this.messageHandlers.SetEventHandler(DidCloseTextDocumentNotification.Type, this.HandleDidCloseTextDocumentNotificationAsync); + this.messageHandlers.SetEventHandler(DidSaveTextDocumentNotification.Type, this.HandleDidSaveTextDocumentNotificationAsync); + this.messageHandlers.SetEventHandler(DidChangeTextDocumentNotification.Type, this.HandleDidChangeTextDocumentNotificationAsync); + this.messageHandlers.SetEventHandler(DidChangeConfigurationNotification.Type, this.HandleDidChangeConfigurationNotificationAsync); + + this.messageHandlers.SetRequestHandler(DefinitionRequest.Type, this.HandleDefinitionRequestAsync); + this.messageHandlers.SetRequestHandler(ReferencesRequest.Type, this.HandleReferencesRequestAsync); + this.messageHandlers.SetRequestHandler(CompletionRequest.Type, this.HandleCompletionRequestAsync); + this.messageHandlers.SetRequestHandler(CompletionResolveRequest.Type, this.HandleCompletionResolveRequestAsync); + this.messageHandlers.SetRequestHandler(SignatureHelpRequest.Type, this.HandleSignatureHelpRequestAsync); + this.messageHandlers.SetRequestHandler(DocumentHighlightRequest.Type, this.HandleDocumentHighlightRequestAsync); + this.messageHandlers.SetRequestHandler(HoverRequest.Type, this.HandleHoverRequestAsync); + this.messageHandlers.SetRequestHandler(WorkspaceSymbolRequest.Type, this.HandleWorkspaceSymbolRequestAsync); + this.messageHandlers.SetRequestHandler(CodeActionRequest.Type, this.HandleCodeActionRequestAsync); + this.messageHandlers.SetRequestHandler(DocumentFormattingRequest.Type, this.HandleDocumentFormattingRequestAsync); this.messageHandlers.SetRequestHandler( DocumentRangeFormattingRequest.Type, - this.HandleDocumentRangeFormattingRequest); + this.HandleDocumentRangeFormattingRequestAsync); this.messageHandlers.SetRequestHandler(FoldingRangeRequest.Type, this.HandleFoldingRangeRequestAsync); - this.messageHandlers.SetRequestHandler(ShowOnlineHelpRequest.Type, this.HandleShowOnlineHelpRequest); - this.messageHandlers.SetRequestHandler(ShowHelpRequest.Type, this.HandleShowHelpRequest); + this.messageHandlers.SetRequestHandler(ShowHelpRequest.Type, this.HandleShowHelpRequestAsync); - this.messageHandlers.SetRequestHandler(ExpandAliasRequest.Type, this.HandleExpandAliasRequest); + this.messageHandlers.SetRequestHandler(ExpandAliasRequest.Type, this.HandleExpandAliasRequestAsync); this.messageHandlers.SetRequestHandler(GetCommandRequest.Type, this.HandleGetCommandRequestAsync); - this.messageHandlers.SetRequestHandler(FindModuleRequest.Type, this.HandleFindModuleRequest); - this.messageHandlers.SetRequestHandler(InstallModuleRequest.Type, this.HandleInstallModuleRequest); + this.messageHandlers.SetRequestHandler(FindModuleRequest.Type, this.HandleFindModuleRequestAsync); + this.messageHandlers.SetRequestHandler(InstallModuleRequest.Type, this.HandleInstallModuleRequestAsync); - this.messageHandlers.SetRequestHandler(InvokeExtensionCommandRequest.Type, this.HandleInvokeExtensionCommandRequest); + this.messageHandlers.SetRequestHandler(InvokeExtensionCommandRequest.Type, this.HandleInvokeExtensionCommandRequestAsync); - this.messageHandlers.SetRequestHandler(PowerShellVersionRequest.Type, this.HandlePowerShellVersionRequest); + this.messageHandlers.SetRequestHandler(PowerShellVersionRequest.Type, this.HandlePowerShellVersionRequestAsync); - this.messageHandlers.SetRequestHandler(NewProjectFromTemplateRequest.Type, this.HandleNewProjectFromTemplateRequest); - this.messageHandlers.SetRequestHandler(GetProjectTemplatesRequest.Type, this.HandleGetProjectTemplatesRequest); + this.messageHandlers.SetRequestHandler(NewProjectFromTemplateRequest.Type, this.HandleNewProjectFromTemplateRequestAsync); + this.messageHandlers.SetRequestHandler(GetProjectTemplatesRequest.Type, this.HandleGetProjectTemplatesRequestAsync); - this.messageHandlers.SetRequestHandler(DebugAdapterMessages.EvaluateRequest.Type, this.HandleEvaluateRequest); + this.messageHandlers.SetRequestHandler(DebugAdapterMessages.EvaluateRequest.Type, this.HandleEvaluateRequestAsync); - this.messageHandlers.SetRequestHandler(GetPSSARulesRequest.Type, this.HandleGetPSSARulesRequest); - this.messageHandlers.SetRequestHandler(SetPSSARulesRequest.Type, this.HandleSetPSSARulesRequest); + this.messageHandlers.SetRequestHandler(GetPSSARulesRequest.Type, this.HandleGetPSSARulesRequestAsync); + this.messageHandlers.SetRequestHandler(SetPSSARulesRequest.Type, this.HandleSetPSSARulesRequestAsync); - this.messageHandlers.SetRequestHandler(ScriptRegionRequest.Type, this.HandleGetFormatScriptRegionRequest); + this.messageHandlers.SetRequestHandler(ScriptRegionRequest.Type, this.HandleGetFormatScriptRegionRequestAsync); - this.messageHandlers.SetRequestHandler(GetPSHostProcessesRequest.Type, this.HandleGetPSHostProcessesRequest); - this.messageHandlers.SetRequestHandler(CommentHelpRequest.Type, this.HandleCommentHelpRequest); + this.messageHandlers.SetRequestHandler(GetPSHostProcessesRequest.Type, this.HandleGetPSHostProcessesRequestAsync); + this.messageHandlers.SetRequestHandler(CommentHelpRequest.Type, this.HandleCommentHelpRequestAsync); // Initialize the extension service // TODO: This should be made awaited once Initialize is async! - this.editorSession.ExtensionService.Initialize( + this.editorSession.ExtensionService.InitializeAsync( this.editorOperations, this.editorSession.Components).Wait(); } @@ -181,15 +180,15 @@ protected Task Stop() #region Built-in Message Handlers - private async Task HandleShutdownRequest( + private async Task HandleShutdownRequestAsync( RequestContext requestContext) { // Allow the implementor to shut down gracefully - await requestContext.SendResult(new object()); + await requestContext.SendResultAsync(new object()); } - private async Task HandleExitNotification( + private async Task HandleExitNotificationAsync( object exitParams, EventContext eventContext) { @@ -197,14 +196,14 @@ private async Task HandleExitNotification( await this.Stop(); } - private Task HandleInitializedNotification(InitializedParams initializedParams, + private Task HandleInitializedNotificationAsync(InitializedParams initializedParams, EventContext eventContext) { // Can do dynamic registration of capabilities in this notification handler return Task.FromResult(true); } - protected async Task HandleInitializeRequest( + protected async Task HandleInitializeRequestAsync( InitializeParams initializeParams, RequestContext requestContext) { @@ -214,12 +213,12 @@ protected async Task HandleInitializeRequest( // Set the working directory of the PowerShell session to the workspace path if (editorSession.Workspace.WorkspacePath != null) { - await editorSession.PowerShellContext.SetWorkingDirectory( + await editorSession.PowerShellContext.SetWorkingDirectoryAsync( editorSession.Workspace.WorkspacePath, isPathAlreadyEscaped: false); } - await requestContext.SendResult( + await requestContext.SendResultAsync( new InitializeResult { Capabilities = new ServerCapabilities @@ -250,7 +249,7 @@ await requestContext.SendResult( }); } - protected async Task HandleShowHelpRequest( + protected async Task HandleShowHelpRequestAsync( string helpParams, RequestContext requestContext) { @@ -293,24 +292,11 @@ protected async Task HandleShowHelpRequest( // TODO: Rather than print the help in the console, we should send the string back // to VSCode to display in a help pop-up (or similar) - await editorSession.PowerShellContext.ExecuteCommand(checkHelpPSCommand, sendOutputToHost: true); - await requestContext.SendResult(null); + await editorSession.PowerShellContext.ExecuteCommandAsync(checkHelpPSCommand, sendOutputToHost: true); + await requestContext.SendResultAsync(null); } - protected async Task HandleShowOnlineHelpRequest( - string helpParams, - RequestContext requestContext - ) - { - PSCommand commandDeprecated = new PSCommand() - .AddCommand("Microsoft.PowerShell.Utility\\Write-Verbose") - .AddParameter("Message", "'powerShell/showOnlineHelp' has been deprecated. Use 'powerShell/showHelp' instead."); - - await editorSession.PowerShellContext.ExecuteCommand(commandDeprecated, sendOutputToHost: true); - await this.HandleShowHelpRequest(helpParams, requestContext); - } - - private async Task HandleSetPSSARulesRequest( + private async Task HandleSetPSSARulesRequestAsync( object param, RequestContext requestContext) { @@ -330,16 +316,16 @@ private async Task HandleSetPSSARulesRequest( editorSession.AnalysisService.ActiveRules = activeRules.ToArray(); } - var sendresult = requestContext.SendResult(null); + var sendresult = requestContext.SendResultAsync(null); var scripFile = editorSession.Workspace.GetFile((string)dynParams.filepath); - await RunScriptDiagnostics( + await RunScriptDiagnosticsAsync( new ScriptFile[] { scripFile }, editorSession, - this.messageSender.SendEvent); + this.messageSender.SendEventAsync); await sendresult; } - private async Task HandleGetFormatScriptRegionRequest( + private async Task HandleGetFormatScriptRegionRequestAsync( ScriptRegionRequestParams requestParams, RequestContext requestContext) { @@ -375,13 +361,13 @@ private async Task HandleGetFormatScriptRegionRequest( break; } - await requestContext.SendResult(new ScriptRegionRequestResult + await requestContext.SendResultAsync(new ScriptRegionRequestResult { scriptRegion = scriptRegion }); } - private async Task HandleGetPSSARulesRequest( + private async Task HandleGetPSSARulesRequestAsync( object param, RequestContext requestContext) { @@ -398,10 +384,10 @@ private async Task HandleGetPSSARulesRequest( } } - await requestContext.SendResult(rules); + await requestContext.SendResultAsync(rules); } - private async Task HandleInstallModuleRequest( + private async Task HandleInstallModuleRequestAsync( string moduleName, RequestContext requestContext ) @@ -409,15 +395,15 @@ RequestContext requestContext var script = string.Format("Install-Module -Name {0} -Scope CurrentUser", moduleName); var executeTask = - editorSession.PowerShellContext.ExecuteScriptString( + editorSession.PowerShellContext.ExecuteScriptStringAsync( script, true, true).ConfigureAwait(false); - await requestContext.SendResult(null); + await requestContext.SendResultAsync(null); } - private Task HandleInvokeExtensionCommandRequest( + private Task HandleInvokeExtensionCommandRequestAsync( InvokeExtensionCommandRequest commandDetails, RequestContext requestContext) { @@ -430,29 +416,29 @@ private Task HandleInvokeExtensionCommandRequest( commandDetails.Context); Task commandTask = - this.editorSession.ExtensionService.InvokeCommand( + this.editorSession.ExtensionService.InvokeCommandAsync( commandDetails.Name, editorContext); commandTask.ContinueWith(t => { - return requestContext.SendResult(null); + return requestContext.SendResultAsync(null); }); return Task.FromResult(true); } - private Task HandleNewProjectFromTemplateRequest( + private Task HandleNewProjectFromTemplateRequestAsync( NewProjectFromTemplateRequest newProjectArgs, RequestContext requestContext) { // Don't await the Task here so that we don't block the session this.editorSession.TemplateService - .CreateFromTemplate(newProjectArgs.TemplatePath, newProjectArgs.DestinationPath) + .CreateFromTemplateAsync(newProjectArgs.TemplatePath, newProjectArgs.DestinationPath) .ContinueWith( async task => { - await requestContext.SendResult( + await requestContext.SendResultAsync( new NewProjectFromTemplateResponse { CreationSuccessful = task.Result @@ -462,19 +448,19 @@ await requestContext.SendResult( return Task.FromResult(true); } - private async Task HandleGetProjectTemplatesRequest( + private async Task HandleGetProjectTemplatesRequestAsync( GetProjectTemplatesRequest requestArgs, RequestContext requestContext) { - bool plasterInstalled = await this.editorSession.TemplateService.ImportPlasterIfInstalled(); + bool plasterInstalled = await this.editorSession.TemplateService.ImportPlasterIfInstalledAsync(); if (plasterInstalled) { var availableTemplates = - await this.editorSession.TemplateService.GetAvailableTemplates( + await this.editorSession.TemplateService.GetAvailableTemplatesAsync( requestArgs.IncludeInstalledModules); - await requestContext.SendResult( + await requestContext.SendResultAsync( new GetProjectTemplatesResponse { Templates = availableTemplates @@ -482,7 +468,7 @@ await requestContext.SendResult( } else { - await requestContext.SendResult( + await requestContext.SendResultAsync( new GetProjectTemplatesResponse { NeedsModuleInstall = true, @@ -491,7 +477,7 @@ await requestContext.SendResult( } } - private async Task HandleExpandAliasRequest( + private async Task HandleExpandAliasRequestAsync( string content, RequestContext requestContext) { @@ -520,13 +506,13 @@ function __Expand-Alias { }"; var psCommand = new PSCommand(); psCommand.AddScript(script); - await this.editorSession.PowerShellContext.ExecuteCommand(psCommand); + await this.editorSession.PowerShellContext.ExecuteCommandAsync(psCommand); psCommand = new PSCommand(); psCommand.AddCommand("__Expand-Alias").AddArgument(content); - var result = await this.editorSession.PowerShellContext.ExecuteCommand(psCommand); + var result = await this.editorSession.PowerShellContext.ExecuteCommandAsync(psCommand); - await requestContext.SendResult(result.First().ToString()); + await requestContext.SendResultAsync(result.First().ToString()); } private async Task HandleGetCommandRequestAsync( @@ -550,9 +536,10 @@ private async Task HandleGetCommandRequestAsync( .AddCommand("Microsoft.PowerShell.Utility\\Sort-Object") .AddParameter("Property", "Name"); } - IEnumerable result = await this.editorSession.PowerShellContext.ExecuteCommand(psCommand); - var commandList = new List(); + IEnumerable result = await this.editorSession.PowerShellContext.ExecuteCommandAsync(psCommand); + + var commandList = new List(); if (result != null) { foreach (dynamic command in result) @@ -568,17 +555,17 @@ private async Task HandleGetCommandRequestAsync( } } - await requestContext.SendResult(commandList); + await requestContext.SendResultAsync(commandList); } - private async Task HandleFindModuleRequest( + private async Task HandleFindModuleRequestAsync( object param, RequestContext requestContext) { var psCommand = new PSCommand(); psCommand.AddScript("Find-Module | Select Name, Description"); - var modules = await editorSession.PowerShellContext.ExecuteCommand(psCommand); + var modules = await editorSession.PowerShellContext.ExecuteCommandAsync(psCommand); var moduleList = new List(); @@ -590,10 +577,10 @@ private async Task HandleFindModuleRequest( } } - await requestContext.SendResult(moduleList); + await requestContext.SendResultAsync(moduleList); } - protected Task HandleDidOpenTextDocumentNotification( + protected Task HandleDidOpenTextDocumentNotificationAsync( DidOpenTextDocumentParams openParams, EventContext eventContext) { @@ -603,7 +590,7 @@ protected Task HandleDidOpenTextDocumentNotification( openParams.TextDocument.Text); // TODO: Get all recently edited files in the workspace - this.RunScriptDiagnostics( + this.RunScriptDiagnosticsAsync( new ScriptFile[] { openedFile }, editorSession, eventContext); @@ -613,7 +600,7 @@ protected Task HandleDidOpenTextDocumentNotification( return Task.FromResult(true); } - protected async Task HandleDidCloseTextDocumentNotification( + protected async Task HandleDidCloseTextDocumentNotificationAsync( DidCloseTextDocumentParams closeParams, EventContext eventContext) { @@ -623,12 +610,12 @@ protected async Task HandleDidCloseTextDocumentNotification( if (fileToClose != null) { editorSession.Workspace.CloseFile(fileToClose); - await ClearMarkers(fileToClose, eventContext); + await ClearMarkersAsync(fileToClose, eventContext); } Logger.Write(LogLevel.Verbose, "Finished closing document."); } - protected async Task HandleDidSaveTextDocumentNotification( + protected async Task HandleDidSaveTextDocumentNotificationAsync( DidSaveTextDocumentParams saveParams, EventContext eventContext) { @@ -640,13 +627,13 @@ protected async Task HandleDidSaveTextDocumentNotification( { if (this.editorSession.RemoteFileManager.IsUnderRemoteTempPath(savedFile.FilePath)) { - await this.editorSession.RemoteFileManager.SaveRemoteFile( + await this.editorSession.RemoteFileManager.SaveRemoteFileAsync( savedFile.FilePath); } } } - protected Task HandleDidChangeTextDocumentNotification( + protected Task HandleDidChangeTextDocumentNotificationAsync( DidChangeTextDocumentParams textChangeParams, EventContext eventContext) { @@ -666,7 +653,7 @@ protected Task HandleDidChangeTextDocumentNotification( } // TODO: Get all recently edited files in the workspace - this.RunScriptDiagnostics( + this.RunScriptDiagnosticsAsync( changedFiles.ToArray(), editorSession, eventContext); @@ -674,7 +661,7 @@ protected Task HandleDidChangeTextDocumentNotification( return Task.FromResult(true); } - protected async Task HandleDidChangeConfigurationNotification( + protected async Task HandleDidChangeConfigurationNotificationAsync( DidChangeConfigurationParams configChangeParams, EventContext eventContext) { @@ -693,7 +680,7 @@ protected async Task HandleDidChangeConfigurationNotification( this.currentSettings.EnableProfileLoading && oldLoadProfiles != this.currentSettings.EnableProfileLoading) { - await this.editorSession.PowerShellContext.LoadHostProfiles(); + await this.editorSession.PowerShellContext.LoadHostProfilesAsync(); this.profilesLoaded = true; } @@ -727,18 +714,18 @@ protected async Task HandleDidChangeConfigurationNotification( { foreach (var scriptFile in editorSession.Workspace.GetOpenedFiles()) { - await ClearMarkers(scriptFile, eventContext); + await ClearMarkersAsync(scriptFile, eventContext); } } - await this.RunScriptDiagnostics( + await this.RunScriptDiagnosticsAsync( this.editorSession.Workspace.GetOpenedFiles(), this.editorSession, eventContext); } } - protected async Task HandleDefinitionRequest( + protected async Task HandleDefinitionRequestAsync( TextDocumentPositionParams textDocumentPosition, RequestContext requestContext) { @@ -758,7 +745,7 @@ protected async Task HandleDefinitionRequest( if (foundSymbol != null) { definition = - await editorSession.LanguageService.GetDefinitionOfSymbol( + await editorSession.LanguageService.GetDefinitionOfSymbolAsync( scriptFile, foundSymbol, editorSession.Workspace); @@ -774,10 +761,10 @@ await editorSession.LanguageService.GetDefinitionOfSymbol( } } - await requestContext.SendResult(definitionLocations.ToArray()); + await requestContext.SendResultAsync(definitionLocations.ToArray()); } - protected async Task HandleReferencesRequest( + protected async Task HandleReferencesRequestAsync( ReferencesParams referencesParams, RequestContext requestContext) { @@ -792,7 +779,7 @@ protected async Task HandleReferencesRequest( referencesParams.Position.Character + 1); FindReferencesResult referencesResult = - await editorSession.LanguageService.FindReferencesOfSymbol( + await editorSession.LanguageService.FindReferencesOfSymbolAsync( foundSymbol, editorSession.Workspace.ExpandScriptReferences(scriptFile), editorSession.Workspace); @@ -813,10 +800,10 @@ await editorSession.LanguageService.FindReferencesOfSymbol( referenceLocations = locations.ToArray(); } - await requestContext.SendResult(referenceLocations); + await requestContext.SendResultAsync(referenceLocations); } - protected async Task HandleCompletionRequest( + protected async Task HandleCompletionRequestAsync( TextDocumentPositionParams textDocumentPositionParams, RequestContext requestContext) { @@ -828,7 +815,7 @@ protected async Task HandleCompletionRequest( textDocumentPositionParams.TextDocument.Uri); CompletionResults completionResults = - await editorSession.LanguageService.GetCompletionsInFile( + await editorSession.LanguageService.GetCompletionsInFileAsync( scriptFile, cursorLine, cursorColumn); @@ -844,10 +831,10 @@ await editorSession.LanguageService.GetCompletionsInFile( } } - await requestContext.SendResult(completionItems); + await requestContext.SendResultAsync(completionItems); } - protected async Task HandleCompletionResolveRequest( + protected async Task HandleCompletionResolveRequestAsync( CompletionItem completionItem, RequestContext requestContext) { @@ -855,24 +842,24 @@ protected async Task HandleCompletionResolveRequest( { // Get the documentation for the function CommandInfo commandInfo = - await CommandHelpers.GetCommandInfo( + await CommandHelpers.GetCommandInfoAsync( completionItem.Label, this.editorSession.PowerShellContext); if (commandInfo != null) { completionItem.Documentation = - await CommandHelpers.GetCommandSynopsis( + await CommandHelpers.GetCommandSynopsisAsync( commandInfo, this.editorSession.PowerShellContext); } } // Send back the updated CompletionItem - await requestContext.SendResult(completionItem); + await requestContext.SendResultAsync(completionItem); } - protected async Task HandleSignatureHelpRequest( + protected async Task HandleSignatureHelpRequestAsync( TextDocumentPositionParams textDocumentPositionParams, RequestContext requestContext) { @@ -881,7 +868,7 @@ protected async Task HandleSignatureHelpRequest( textDocumentPositionParams.TextDocument.Uri); ParameterSetSignatures parameterSets = - await editorSession.LanguageService.FindParameterSetsInFile( + await editorSession.LanguageService.FindParameterSetsInFileAsync( scriptFile, textDocumentPositionParams.Position.Line + 1, textDocumentPositionParams.Position.Character + 1); @@ -910,7 +897,7 @@ await editorSession.LanguageService.FindParameterSetsInFile( } } - await requestContext.SendResult( + await requestContext.SendResultAsync( new SignatureHelp { Signatures = signatures, @@ -919,7 +906,7 @@ await requestContext.SendResult( }); } - protected async Task HandleDocumentHighlightRequest( + protected async Task HandleDocumentHighlightRequestAsync( TextDocumentPositionParams textDocumentPositionParams, RequestContext requestContext) { @@ -949,10 +936,10 @@ protected async Task HandleDocumentHighlightRequest( documentHighlights = highlights.ToArray(); } - await requestContext.SendResult(documentHighlights); + await requestContext.SendResultAsync(documentHighlights); } - protected async Task HandleHoverRequest( + protected async Task HandleHoverRequestAsync( TextDocumentPositionParams textDocumentPositionParams, RequestContext requestContext) { @@ -963,7 +950,7 @@ protected async Task HandleHoverRequest( SymbolDetails symbolDetails = await editorSession .LanguageService - .FindSymbolDetailsAtLocation( + .FindSymbolDetailsAtLocationAsync( scriptFile, textDocumentPositionParams.Position.Line + 1, textDocumentPositionParams.Position.Character + 1); @@ -993,7 +980,7 @@ await editorSession symbolRange = GetRangeFromScriptRegion(symbolDetails.SymbolReference.ScriptRegion); } - await requestContext.SendResult( + await requestContext.SendResultAsync( new Hover { Contents = symbolInfo.ToArray(), @@ -1001,7 +988,7 @@ await requestContext.SendResult( }); } - protected async Task HandleDocumentSymbolRequest( + protected async Task HandleDocumentSymbolRequestAsync( DocumentSymbolParams documentSymbolParams, RequestContext requestContext) { @@ -1038,7 +1025,7 @@ protected async Task HandleDocumentSymbolRequest( symbols = symbolAcc.ToArray(); } - await requestContext.SendResult(symbols); + await requestContext.SendResultAsync(symbols); } public static SymbolKind GetSymbolKind(SymbolType symbolType) @@ -1069,7 +1056,7 @@ public static string GetDecoratedSymbolName(SymbolReference symbolReference) return name; } - protected async Task HandleWorkspaceSymbolRequest( + protected async Task HandleWorkspaceSymbolRequestAsync( WorkspaceSymbolParams workspaceSymbolParams, RequestContext requestContext) { @@ -1110,19 +1097,19 @@ protected async Task HandleWorkspaceSymbolRequest( } } - await requestContext.SendResult(symbols.ToArray()); + await requestContext.SendResultAsync(symbols.ToArray()); } - protected async Task HandlePowerShellVersionRequest( + protected async Task HandlePowerShellVersionRequestAsync( object noParams, RequestContext requestContext) { - await requestContext.SendResult( + await requestContext.SendResultAsync( new PowerShellVersion( this.editorSession.PowerShellContext.LocalPowerShellVersion)); } - protected async Task HandleGetPSHostProcessesRequest( + protected async Task HandleGetPSHostProcessesRequestAsync( object noParams, RequestContext requestContext) { @@ -1138,7 +1125,7 @@ protected async Task HandleGetPSHostProcessesRequest( .AddParameter("NE") .AddParameter("Value", processId.ToString()); - var processes = await editorSession.PowerShellContext.ExecuteCommand(psCommand); + var processes = await editorSession.PowerShellContext.ExecuteCommandAsync(psCommand); if (processes != null) { foreach (dynamic p in processes) @@ -1155,10 +1142,10 @@ protected async Task HandleGetPSHostProcessesRequest( } } - await requestContext.SendResult(psHostProcesses.ToArray()); + await requestContext.SendResultAsync(psHostProcesses.ToArray()); } - protected async Task HandleCommentHelpRequest( + protected async Task HandleCommentHelpRequestAsync( CommentHelpRequestParams requestParams, RequestContext requestContext) { @@ -1167,7 +1154,7 @@ protected async Task HandleCommentHelpRequest( ScriptFile scriptFile; if (!this.editorSession.Workspace.TryGetFile(requestParams.DocumentUri, out scriptFile)) { - await requestContext.SendResult(result); + await requestContext.SendResultAsync(result); return; } @@ -1181,7 +1168,7 @@ protected async Task HandleCommentHelpRequest( if (functionDefinitionAst == null) { - await requestContext.SendResult(result); + await requestContext.SendResultAsync(result); return; } @@ -1214,7 +1201,7 @@ protected async Task HandleCommentHelpRequest( if (helpText == null) { - await requestContext.SendResult(result); + await requestContext.SendResultAsync(result); return; } @@ -1228,7 +1215,7 @@ protected async Task HandleCommentHelpRequest( result.Content = result.Content.Skip(1).ToArray(); } - await requestContext.SendResult(result); + await requestContext.SendResultAsync(result); } private bool IsQueryMatch(string query, string symbolName) @@ -1237,7 +1224,7 @@ private bool IsQueryMatch(string query, string symbolName) } // https://microsoft.github.io/language-server-protocol/specification#textDocument_codeAction - protected async Task HandleCodeActionRequest( + protected async Task HandleCodeActionRequestAsync( CodeActionParams codeActionParams, RequestContext requestContext) { @@ -1296,19 +1283,20 @@ protected async Task HandleCodeActionRequest( } } - await requestContext.SendResult(codeActionCommands.ToArray()); + await requestContext.SendResultAsync( + codeActionCommands.ToArray()); } - protected async Task HandleDocumentFormattingRequest( + protected async Task HandleDocumentFormattingRequestAsync( DocumentFormattingParams formattingParams, RequestContext requestContext) { - var result = await Format( + var result = await FormatAsync( formattingParams.TextDocument.Uri, formattingParams.options, null); - await requestContext.SendResult(new TextEdit[1] + await requestContext.SendResultAsync(new TextEdit[1] { new TextEdit { @@ -1318,16 +1306,16 @@ await requestContext.SendResult(new TextEdit[1] }); } - protected async Task HandleDocumentRangeFormattingRequest( + protected async Task HandleDocumentRangeFormattingRequestAsync( DocumentRangeFormattingParams formattingParams, RequestContext requestContext) { - var result = await Format( + var result = await FormatAsync( formattingParams.TextDocument.Uri, formattingParams.Options, formattingParams.Range); - await requestContext.SendResult(new TextEdit[1] + await requestContext.SendResultAsync(new TextEdit[1] { new TextEdit { @@ -1341,10 +1329,10 @@ protected async Task HandleFoldingRangeRequestAsync( FoldingRangeParams foldingParams, RequestContext requestContext) { - await requestContext.SendResult(Fold(foldingParams.TextDocument.Uri)); + await requestContext.SendResultAsync(Fold(foldingParams.TextDocument.Uri)); } - protected Task HandleEvaluateRequest( + protected Task HandleEvaluateRequestAsync( DebugAdapterMessages.EvaluateRequestArguments evaluateParams, RequestContext requestContext) { @@ -1353,7 +1341,7 @@ protected Task HandleEvaluateRequest( // is executing. This important in cases where the pipeline thread // gets blocked by something in the script like a prompt to the user. var executeTask = - this.editorSession.PowerShellContext.ExecuteScriptString( + this.editorSession.PowerShellContext.ExecuteScriptStringAsync( evaluateParams.Expression, writeInputToHost: true, writeOutputToHost: true, @@ -1367,7 +1355,7 @@ protected Task HandleEvaluateRequest( // Return an empty result since the result value is irrelevant // for this request in the LanguageServer return - requestContext.SendResult( + requestContext.SendResultAsync( new DebugAdapterMessages.EvaluateResponseBody { Result = "", @@ -1414,7 +1402,7 @@ private FoldingRange[] Fold(string documentUri) return result.ToArray(); } - private async Task> Format( + private async Task> FormatAsync( string documentUri, FormattingOptions options, Range range) @@ -1449,7 +1437,7 @@ private async Task> Format( } }; - formattedScript = await editorSession.AnalysisService.Format( + formattedScript = await editorSession.AnalysisService.FormatAsync( scriptFile.Contents, pssaSettings, rangeList); @@ -1457,9 +1445,9 @@ private async Task> Format( return Tuple.Create(formattedScript, editRange); } - private async void PowerShellContext_RunspaceChanged(object sender, Session.RunspaceChangedEventArgs e) + private async void PowerShellContext_RunspaceChangedAsync(object sender, Session.RunspaceChangedEventArgs e) { - await this.messageSender.SendEvent( + await this.messageSender.SendEventAsync( RunspaceChangedEvent.Type, new Protocol.LanguageServer.RunspaceDetails(e.NewRunspace)); } @@ -1469,16 +1457,16 @@ await this.messageSender.SendEvent( /// /// the PowerShell context sending the execution event /// details of the execution status change - private async void PowerShellContext_ExecutionStatusChanged(object sender, ExecutionStatusChangedEventArgs e) + private async void PowerShellContext_ExecutionStatusChangedAsync(object sender, ExecutionStatusChangedEventArgs e) { - await this.messageSender.SendEvent( + await this.messageSender.SendEventAsync( ExecutionStatusChangedEvent.Type, e); } - private async void ExtensionService_ExtensionAdded(object sender, EditorCommand e) + private async void ExtensionService_ExtensionAddedAsync(object sender, EditorCommand e) { - await this.messageSender.SendEvent( + await this.messageSender.SendEventAsync( ExtensionCommandAddedNotification.Type, new ExtensionCommandAddedNotification { @@ -1487,9 +1475,9 @@ await this.messageSender.SendEvent( }); } - private async void ExtensionService_ExtensionUpdated(object sender, EditorCommand e) + private async void ExtensionService_ExtensionUpdatedAsync(object sender, EditorCommand e) { - await this.messageSender.SendEvent( + await this.messageSender.SendEventAsync( ExtensionCommandUpdatedNotification.Type, new ExtensionCommandUpdatedNotification { @@ -1497,9 +1485,9 @@ await this.messageSender.SendEvent( }); } - private async void ExtensionService_ExtensionRemoved(object sender, EditorCommand e) + private async void ExtensionService_ExtensionRemovedAsync(object sender, EditorCommand e) { - await this.messageSender.SendEvent( + await this.messageSender.SendEventAsync( ExtensionCommandRemovedNotification.Type, new ExtensionCommandRemovedNotification { @@ -1507,11 +1495,11 @@ await this.messageSender.SendEvent( }); } - private async void DebugService_DebuggerStopped(object sender, DebuggerStoppedEventArgs e) + private async void DebugService_DebuggerStoppedAsync(object sender, DebuggerStoppedEventArgs e) { if (!this.editorSession.DebugService.IsClientAttached) { - await this.messageSender.SendEvent( + await this.messageSender.SendEventAsync( StartDebuggerEvent.Type, new StartDebuggerEvent()); } @@ -1564,15 +1552,15 @@ private static FileChange GetFileChangeDetails(Range changeRange, string insertS }; } - private Task RunScriptDiagnostics( + private Task RunScriptDiagnosticsAsync( ScriptFile[] filesToAnalyze, EditorSession editorSession, EventContext eventContext) { - return RunScriptDiagnostics(filesToAnalyze, editorSession, this.messageSender.SendEvent); + return RunScriptDiagnosticsAsync(filesToAnalyze, editorSession, this.messageSender.SendEventAsync); } - private Task RunScriptDiagnostics( + private Task RunScriptDiagnosticsAsync( ScriptFile[] filesToAnalyze, EditorSession editorSession, Func, PublishDiagnosticsNotification, Task> eventSender) @@ -1618,7 +1606,7 @@ private Task RunScriptDiagnostics( s_existingRequestCancellation = new CancellationTokenSource(); Task.Factory.StartNew( () => - DelayThenInvokeDiagnostics( + DelayThenInvokeDiagnosticsAsync( 750, filesToAnalyze, this.currentSettings.ScriptAnalysis?.Enable.Value ?? false, @@ -1634,7 +1622,29 @@ private Task RunScriptDiagnostics( return Task.FromResult(true); } - private static async Task DelayThenInvokeDiagnostics( + private static async Task DelayThenInvokeDiagnosticsAsync( + int delayMilliseconds, + ScriptFile[] filesToAnalyze, + bool isScriptAnalysisEnabled, + Dictionary> correctionIndex, + EditorSession editorSession, + EventContext eventContext, + ILogger Logger, + CancellationToken cancellationToken) + { + await DelayThenInvokeDiagnosticsAsync( + delayMilliseconds, + filesToAnalyze, + isScriptAnalysisEnabled, + correctionIndex, + editorSession, + eventContext.SendEventAsync, + Logger, + cancellationToken); + } + + + private static async Task DelayThenInvokeDiagnosticsAsync( int delayMilliseconds, ScriptFile[] filesToAnalyze, bool isScriptAnalysisEnabled, @@ -1653,6 +1663,15 @@ private static async Task DelayThenInvokeDiagnostics( catch (TaskCanceledException) { // If the task is cancelled, exit directly + foreach (var script in filesToAnalyze) + { + await PublishScriptDiagnosticsAsync( + script, + script.SyntaxMarkers, + correctionIndex, + eventSender); + } + return; } @@ -1681,7 +1700,7 @@ private static async Task DelayThenInvokeDiagnostics( semanticMarkers = new ScriptFileMarker[0]; } - await PublishScriptDiagnostics( + await PublishScriptDiagnosticsAsync( scriptFile, // Concat script analysis errors to any existing parse errors scriptFile.SyntaxMarkers.Concat(semanticMarkers).ToArray(), @@ -1690,30 +1709,30 @@ await PublishScriptDiagnostics( } } - private async Task ClearMarkers(ScriptFile scriptFile, EventContext eventContext) + private async Task ClearMarkersAsync(ScriptFile scriptFile, EventContext eventContext) { // send empty diagnostic markers to clear any markers associated with the given file - await PublishScriptDiagnostics( + await PublishScriptDiagnosticsAsync( scriptFile, new ScriptFileMarker[0], this.codeActionsPerFile, eventContext); } - private static async Task PublishScriptDiagnostics( + private static async Task PublishScriptDiagnosticsAsync( ScriptFile scriptFile, ScriptFileMarker[] markers, Dictionary> correctionIndex, EventContext eventContext) { - await PublishScriptDiagnostics( + await PublishScriptDiagnosticsAsync( scriptFile, markers, correctionIndex, - eventContext.SendEvent); + eventContext.SendEventAsync); } - private static async Task PublishScriptDiagnostics( + private static async Task PublishScriptDiagnosticsAsync( ScriptFile scriptFile, ScriptFileMarker[] markers, Dictionary> correctionIndex, diff --git a/src/PowerShellEditorServices.Protocol/Server/LanguageServerEditorOperations.cs b/src/PowerShellEditorServices.Protocol/Server/LanguageServerEditorOperations.cs index 8639f5ee3..5df247bd5 100644 --- a/src/PowerShellEditorServices.Protocol/Server/LanguageServerEditorOperations.cs +++ b/src/PowerShellEditorServices.Protocol/Server/LanguageServerEditorOperations.cs @@ -26,10 +26,10 @@ public LanguageServerEditorOperations( this.messageSender = messageSender; } - public async Task GetEditorContext() + public async Task GetEditorContextAsync() { ClientEditorContext clientContext = - await this.messageSender.SendRequest( + await this.messageSender.SendRequestAsync( GetEditorContextRequest.Type, new GetEditorContextRequest(), true); @@ -37,9 +37,9 @@ await this.messageSender.SendRequest( return this.ConvertClientEditorContext(clientContext); } - public async Task InsertText(string filePath, string text, BufferRange insertRange) + public async Task InsertTextAsync(string filePath, string text, BufferRange insertRange) { - await this.messageSender.SendRequest( + await this.messageSender.SendRequestAsync( InsertTextRequest.Type, new InsertTextRequest { @@ -64,9 +64,9 @@ await this.messageSender.SendRequest( // TODO: Set the last param back to true! } - public Task SetSelection(BufferRange selectionRange) + public Task SetSelectionAsync(BufferRange selectionRange) { - return this.messageSender.SendRequest( + return this.messageSender.SendRequestAsync( SetSelectionRequest.Type, new SetSelectionRequest { @@ -109,19 +109,19 @@ public EditorContext ConvertClientEditorContext( clientContext.CurrentFileLanguage); } - public Task NewFile() + public Task NewFileAsync() { return - this.messageSender.SendRequest( + this.messageSender.SendRequestAsync( NewFileRequest.Type, null, true); } - public Task OpenFile(string filePath) + public Task OpenFileAsync(string filePath) { return - this.messageSender.SendRequest( + this.messageSender.SendRequestAsync( OpenFileRequest.Type, new OpenFileDetails { @@ -131,10 +131,10 @@ public Task OpenFile(string filePath) true); } - public Task OpenFile(string filePath, bool preview) + public Task OpenFileAsync(string filePath, bool preview) { return - this.messageSender.SendRequest( + this.messageSender.SendRequestAsync( OpenFileRequest.Type, new OpenFileDetails { @@ -144,24 +144,24 @@ public Task OpenFile(string filePath, bool preview) true); } - public Task CloseFile(string filePath) + public Task CloseFileAsync(string filePath) { return - this.messageSender.SendRequest( + this.messageSender.SendRequestAsync( CloseFileRequest.Type, filePath, true); } - public Task SaveFile(string filePath) + public Task SaveFileAsync(string filePath) { - return SaveFile(filePath, null); + return SaveFileAsync(filePath, null); } - public Task SaveFile(string currentPath, string newSavePath) + public Task SaveFileAsync(string currentPath, string newSavePath) { return - this.messageSender.SendRequest( + this.messageSender.SendRequestAsync( SaveFileRequest.Type, new SaveFileDetails { @@ -181,37 +181,37 @@ public string GetWorkspaceRelativePath(string filePath) return this.editorSession.Workspace.GetRelativePath(filePath); } - public Task ShowInformationMessage(string message) + public Task ShowInformationMessageAsync(string message) { return - this.messageSender.SendRequest( + this.messageSender.SendRequestAsync( ShowInformationMessageRequest.Type, message, true); } - public Task ShowErrorMessage(string message) + public Task ShowErrorMessageAsync(string message) { return - this.messageSender.SendRequest( + this.messageSender.SendRequestAsync( ShowErrorMessageRequest.Type, message, true); } - public Task ShowWarningMessage(string message) + public Task ShowWarningMessageAsync(string message) { return - this.messageSender.SendRequest( + this.messageSender.SendRequestAsync( ShowWarningMessageRequest.Type, message, true); } - public Task SetStatusBarMessage(string message, int? timeout) + public Task SetStatusBarMessageAsync(string message, int? timeout) { return - this.messageSender.SendRequest( + this.messageSender.SendRequestAsync( SetStatusBarMessageRequest.Type, new StatusBarMessageDetails { diff --git a/src/PowerShellEditorServices.Protocol/Server/OutputDebouncer.cs b/src/PowerShellEditorServices.Protocol/Server/OutputDebouncer.cs index 20866655a..079183a26 100644 --- a/src/PowerShellEditorServices.Protocol/Server/OutputDebouncer.cs +++ b/src/PowerShellEditorServices.Protocol/Server/OutputDebouncer.cs @@ -47,7 +47,7 @@ public OutputDebouncer(IMessageSender messageSender) #region Private Methods - protected override async Task OnInvoke(OutputWrittenEventArgs output) + protected override async Task OnInvokeAsync(OutputWrittenEventArgs output) { bool outputIsError = output.OutputType == OutputType.Error; @@ -56,7 +56,7 @@ protected override async Task OnInvoke(OutputWrittenEventArgs output) if (this.currentOutputString != null) { // Flush the output - await this.OnFlush(); + await this.OnFlushAsync(); } this.currentOutputString = string.Empty; @@ -77,13 +77,13 @@ protected override async Task OnInvoke(OutputWrittenEventArgs output) string.Empty); } - protected override async Task OnFlush() + protected override async Task OnFlushAsync() { // Only flush output if there is some to flush if (this.currentOutputString != null) { // Send an event for the current output - await this.messageSender.SendEvent( + await this.messageSender.SendEventAsync( OutputEvent.Type, new OutputEventBody { diff --git a/src/PowerShellEditorServices.VSCode/CustomViews/CustomViewBase.cs b/src/PowerShellEditorServices.VSCode/CustomViews/CustomViewBase.cs index f212ee968..1fdb6ddf0 100644 --- a/src/PowerShellEditorServices.VSCode/CustomViews/CustomViewBase.cs +++ b/src/PowerShellEditorServices.VSCode/CustomViews/CustomViewBase.cs @@ -34,10 +34,10 @@ public CustomViewBase( this.logger = logger; } - internal Task Create() + internal Task CreateAsync() { return - this.messageSender.SendRequest( + this.messageSender.SendRequestAsync( NewCustomViewRequest.Type, new NewCustomViewRequest { @@ -50,7 +50,7 @@ internal Task Create() public Task Show(ViewColumn viewColumn) { return - this.messageSender.SendRequest( + this.messageSender.SendRequestAsync( ShowCustomViewRequest.Type, new ShowCustomViewRequest { @@ -62,7 +62,7 @@ public Task Show(ViewColumn viewColumn) public Task Close() { return - this.messageSender.SendRequest( + this.messageSender.SendRequestAsync( CloseCustomViewRequest.Type, new CloseCustomViewRequest { diff --git a/src/PowerShellEditorServices.VSCode/CustomViews/HtmlContentView.cs b/src/PowerShellEditorServices.VSCode/CustomViews/HtmlContentView.cs index 1ab72e04b..c7a553707 100644 --- a/src/PowerShellEditorServices.VSCode/CustomViews/HtmlContentView.cs +++ b/src/PowerShellEditorServices.VSCode/CustomViews/HtmlContentView.cs @@ -26,10 +26,10 @@ public HtmlContentView( { } - public Task SetContent(string htmlBodyContent) + public Task SetContentAsync(string htmlBodyContent) { return - this.messageSender.SendRequest( + this.messageSender.SendRequestAsync( SetHtmlContentViewRequest.Type, new SetHtmlContentViewRequest { @@ -38,7 +38,7 @@ public Task SetContent(string htmlBodyContent) }, true); } - public Task SetContent(HtmlContent htmlContent) + public Task SetContentAsync(HtmlContent htmlContent) { HtmlContent validatedContent = new HtmlContent() @@ -49,7 +49,7 @@ public Task SetContent(HtmlContent htmlContent) }; return - this.messageSender.SendRequest( + this.messageSender.SendRequestAsync( SetHtmlContentViewRequest.Type, new SetHtmlContentViewRequest { @@ -58,10 +58,10 @@ public Task SetContent(HtmlContent htmlContent) }, true); } - public Task AppendContent(string appendedHtmlBodyContent) + public Task AppendContentAsync(string appendedHtmlBodyContent) { return - this.messageSender.SendRequest( + this.messageSender.SendRequestAsync( AppendHtmlContentViewRequest.Type, new AppendHtmlContentViewRequest { diff --git a/src/PowerShellEditorServices.VSCode/CustomViews/HtmlContentViewsFeature.cs b/src/PowerShellEditorServices.VSCode/CustomViews/HtmlContentViewsFeature.cs index 4ae093c2e..a520fa610 100644 --- a/src/PowerShellEditorServices.VSCode/CustomViews/HtmlContentViewsFeature.cs +++ b/src/PowerShellEditorServices.VSCode/CustomViews/HtmlContentViewsFeature.cs @@ -19,7 +19,7 @@ public HtmlContentViewsFeature( { } - public async Task CreateHtmlContentView(string viewTitle) + public async Task CreateHtmlContentViewAsync(string viewTitle) { HtmlContentView htmlView = new HtmlContentView( @@ -27,7 +27,7 @@ public async Task CreateHtmlContentView(string viewTitle) this.messageSender, this.logger); - await htmlView.Create(); + await htmlView.CreateAsync(); this.AddView(htmlView); return htmlView; diff --git a/src/PowerShellEditorServices.VSCode/CustomViews/IHtmlContentView.cs b/src/PowerShellEditorServices.VSCode/CustomViews/IHtmlContentView.cs index 0d58f2719..c12bc531b 100644 --- a/src/PowerShellEditorServices.VSCode/CustomViews/IHtmlContentView.cs +++ b/src/PowerShellEditorServices.VSCode/CustomViews/IHtmlContentView.cs @@ -21,7 +21,7 @@ public interface IHtmlContentView : ICustomView /// The HTML content that is placed inside of the page's body tag. /// /// A Task which can be awaited for completion. - Task SetContent(string htmlBodyContent); + Task SetContentAsync(string htmlBodyContent); /// /// Sets the HTML content of the view. @@ -30,7 +30,7 @@ public interface IHtmlContentView : ICustomView /// The HTML content that is placed inside of the page's body tag. /// /// A Task which can be awaited for completion. - Task SetContent(HtmlContent htmlContent); + Task SetContentAsync(HtmlContent htmlContent); /// /// Appends HTML body content to the view. @@ -39,6 +39,6 @@ public interface IHtmlContentView : ICustomView /// The HTML fragment to be appended to the output stream. /// /// A Task which can be awaited for completion. - Task AppendContent(string appendedHtmlBodyContent); + Task AppendContentAsync(string appendedHtmlBodyContent); } } diff --git a/src/PowerShellEditorServices.VSCode/CustomViews/IHtmlContentViews.cs b/src/PowerShellEditorServices.VSCode/CustomViews/IHtmlContentViews.cs index 5b42b4540..396ddae4c 100644 --- a/src/PowerShellEditorServices.VSCode/CustomViews/IHtmlContentViews.cs +++ b/src/PowerShellEditorServices.VSCode/CustomViews/IHtmlContentViews.cs @@ -21,6 +21,6 @@ public interface IHtmlContentViews /// /// A Task to await for completion, returns the IHtmlContentView instance. /// - Task CreateHtmlContentView(string viewTitle); + Task CreateHtmlContentViewAsync(string viewTitle); } } diff --git a/src/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.csproj b/src/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.csproj index e3726be1f..c50e535ea 100644 --- a/src/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.csproj +++ b/src/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.csproj @@ -4,12 +4,12 @@ PowerShell Editor Services, Visual Studio Code Extensions Provides added functionality to PowerShell Editor Services for the Visual Studio Code editor. - netstandard1.6;net452 + netstandard2.0 Microsoft.PowerShell.EditorServices.VSCode - + 1591,1573,1572 bin\$(TargetFramework)\$(Configuration)\Microsoft.PowerShell.EditorServices.VSCode.xml @@ -20,12 +20,4 @@ - - $(DefineConstants);CoreCLR - - - - - - diff --git a/src/PowerShellEditorServices/Analysis/AnalysisService.cs b/src/PowerShellEditorServices/Analysis/AnalysisService.cs index 40b6327e5..d87226377 100644 --- a/src/PowerShellEditorServices/Analysis/AnalysisService.cs +++ b/src/PowerShellEditorServices/Analysis/AnalysisService.cs @@ -323,7 +323,7 @@ public IEnumerable GetPSScriptAnalyzerRules() /// ScriptAnalyzer settings /// The range within which formatting should be applied. /// The formatted script text. - public async Task Format( + public async Task FormatAsync( string scriptDefinition, Hashtable settings, int[] rangeList) @@ -611,7 +611,12 @@ private static RunspacePool CreatePssaRunspacePool(out PSModuleInfo pssaModuleIn } // Create a base session state with PSScriptAnalyzer loaded - InitialSessionState sessionState = InitialSessionState.CreateDefault2(); + InitialSessionState sessionState; + if (Environment.GetEnvironmentVariable("PSES_TEST_USE_CREATE_DEFAULT") == "1") { + sessionState = InitialSessionState.CreateDefault(); + } else { + sessionState = InitialSessionState.CreateDefault2(); + } sessionState.ImportPSModule(new [] { pssaModuleInfo.ModuleBase }); // RunspacePool takes care of queuing commands for us so we do not diff --git a/src/PowerShellEditorServices/Console/ChoicePromptHandler.cs b/src/PowerShellEditorServices/Console/ChoicePromptHandler.cs index 3511d4483..579d5b708 100644 --- a/src/PowerShellEditorServices/Console/ChoicePromptHandler.cs +++ b/src/PowerShellEditorServices/Console/ChoicePromptHandler.cs @@ -110,7 +110,7 @@ public ChoicePromptHandler(ILogger logger) : base(logger) /// A Task instance that can be monitored for completion to get /// the user's choice. /// - public async Task PromptForChoice( + public async Task PromptForChoiceAsync( string promptCaption, string promptMessage, ChoiceDetails[] choices, @@ -132,8 +132,8 @@ public async Task PromptForChoice( cancellationToken.Register(this.CancelPrompt, true); // Convert the int[] result to int - return await this.WaitForTask( - this.StartPromptLoop(this.promptCancellationTokenSource.Token) + return await this.WaitForTaskAsync( + this.StartPromptLoopAsync(this.promptCancellationTokenSource.Token) .ContinueWith( task => { @@ -173,7 +173,7 @@ public async Task PromptForChoice( /// A Task instance that can be monitored for completion to get /// the user's choices. /// - public async Task PromptForChoice( + public async Task PromptForChoiceAsync( string promptCaption, string promptMessage, ChoiceDetails[] choices, @@ -191,12 +191,12 @@ public async Task PromptForChoice( // Cancel the TaskCompletionSource if the caller cancels the task cancellationToken.Register(this.CancelPrompt, true); - return await this.WaitForTask( - this.StartPromptLoop( + return await this.WaitForTaskAsync( + this.StartPromptLoopAsync( this.promptCancellationTokenSource.Token)); } - private async Task WaitForTask(Task taskToWait) + private async Task WaitForTaskAsync(Task taskToWait) { Task finishedTask = await Task.WhenAny( @@ -211,7 +211,7 @@ await Task.WhenAny( return taskToWait.Result; } - private async Task StartPromptLoop( + private async Task StartPromptLoopAsync( CancellationToken cancellationToken) { int[] choiceIndexes = null; @@ -221,7 +221,7 @@ private async Task StartPromptLoop( while (!cancellationToken.IsCancellationRequested) { - string responseString = await this.ReadInputString(cancellationToken); + string responseString = await this.ReadInputStringAsync(cancellationToken); if (responseString == null) { // If the response string is null, the prompt has been cancelled @@ -334,7 +334,7 @@ protected override void OnPromptCancelled() /// A Task instance that can be monitored for completion to get /// the user's input. /// - protected abstract Task ReadInputString(CancellationToken cancellationToken); + protected abstract Task ReadInputStringAsync(CancellationToken cancellationToken); #endregion diff --git a/src/PowerShellEditorServices/Console/ConsoleProxy.cs b/src/PowerShellEditorServices/Console/ConsoleProxy.cs new file mode 100644 index 000000000..3956f6df9 --- /dev/null +++ b/src/PowerShellEditorServices/Console/ConsoleProxy.cs @@ -0,0 +1,90 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + /// + /// Provides asynchronous implementations of the API's as well as + /// synchronous implementations that work around platform specific issues. + /// + internal static class ConsoleProxy + { + private static IConsoleOperations s_consoleProxy; + + static ConsoleProxy() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + s_consoleProxy = new WindowsConsoleOperations(); + return; + } + + s_consoleProxy = new UnixConsoleOperations(); + } + + public static Task ReadKeyAsync(CancellationToken cancellationToken) => + s_consoleProxy.ReadKeyAsync(cancellationToken); + + public static int GetCursorLeft() => + s_consoleProxy.GetCursorLeft(); + + public static int GetCursorLeft(CancellationToken cancellationToken) => + s_consoleProxy.GetCursorLeft(cancellationToken); + + public static Task GetCursorLeftAsync() => + s_consoleProxy.GetCursorLeftAsync(); + + public static Task GetCursorLeftAsync(CancellationToken cancellationToken) => + s_consoleProxy.GetCursorLeftAsync(cancellationToken); + + public static int GetCursorTop() => + s_consoleProxy.GetCursorTop(); + + public static int GetCursorTop(CancellationToken cancellationToken) => + s_consoleProxy.GetCursorTop(cancellationToken); + + public static Task GetCursorTopAsync() => + s_consoleProxy.GetCursorTopAsync(); + + public static Task GetCursorTopAsync(CancellationToken cancellationToken) => + s_consoleProxy.GetCursorTopAsync(cancellationToken); + + /// + /// On Unix platforms this method is sent to PSReadLine as a work around for issues + /// with the System.Console implementation for that platform. Functionally it is the + /// same as System.Console.ReadKey, with the exception that it will not lock the + /// standard input stream. + /// + /// + /// Determines whether to display the pressed key in the console window. + /// true to not display the pressed key; otherwise, false. + /// + /// + /// The that can be used to cancel the request. + /// + /// + /// An object that describes the ConsoleKey constant and Unicode character, if any, + /// that correspond to the pressed console key. The ConsoleKeyInfo object also describes, + /// in a bitwise combination of ConsoleModifiers values, whether one or more Shift, Alt, + /// or Ctrl modifier keys was pressed simultaneously with the console key. + /// + internal static ConsoleKeyInfo UnixReadKey(bool intercept, CancellationToken cancellationToken) + { + try + { + return ((UnixConsoleOperations)s_consoleProxy).ReadKey(intercept, cancellationToken); + } + catch (OperationCanceledException) + { + return default(ConsoleKeyInfo); + } + } + } +} diff --git a/src/PowerShellEditorServices/Console/ConsoleReadLine.cs b/src/PowerShellEditorServices/Console/ConsoleReadLine.cs index 7c518a718..e2824d635 100644 --- a/src/PowerShellEditorServices/Console/ConsoleReadLine.cs +++ b/src/PowerShellEditorServices/Console/ConsoleReadLine.cs @@ -6,7 +6,6 @@ using System.Collections.ObjectModel; using System.Linq; using System.Text; -using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -20,23 +19,11 @@ namespace Microsoft.PowerShell.EditorServices.Console internal class ConsoleReadLine { #region Private Field - private static IConsoleOperations s_consoleProxy; - private PowerShellContext powerShellContext; #endregion #region Constructors - static ConsoleReadLine() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - s_consoleProxy = new WindowsConsoleOperations(); - return; - } - - s_consoleProxy = new UnixConsoleOperations(); - } public ConsoleReadLine(PowerShellContext powerShellContext) { @@ -47,22 +34,22 @@ public ConsoleReadLine(PowerShellContext powerShellContext) #region Public Methods - public Task ReadCommandLine(CancellationToken cancellationToken) + public Task ReadCommandLineAsync(CancellationToken cancellationToken) { - return this.ReadLine(true, cancellationToken); + return this.ReadLineAsync(true, cancellationToken); } - public Task ReadSimpleLine(CancellationToken cancellationToken) + public Task ReadSimpleLineAsync(CancellationToken cancellationToken) { - return this.ReadLine(false, cancellationToken); + return this.ReadLineAsync(false, cancellationToken); } - public async Task ReadSecureLine(CancellationToken cancellationToken) + public async Task ReadSecureLineAsync(CancellationToken cancellationToken) { SecureString secureString = new SecureString(); - int initialPromptRow = Console.CursorTop; - int initialPromptCol = Console.CursorLeft; + int initialPromptRow = await ConsoleProxy.GetCursorTopAsync(cancellationToken); + int initialPromptCol = await ConsoleProxy.GetCursorLeftAsync(cancellationToken); int previousInputLength = 0; Console.TreatControlCAsInput = true; @@ -109,7 +96,8 @@ public async Task ReadSecureLine(CancellationToken cancellationTok } else if (previousInputLength > 0 && currentInputLength < previousInputLength) { - int row = Console.CursorTop, col = Console.CursorLeft; + int row = await ConsoleProxy.GetCursorTopAsync(cancellationToken); + int col = await ConsoleProxy.GetCursorLeftAsync(cancellationToken); // Back up the cursor before clearing the character col--; @@ -141,10 +129,30 @@ public async Task ReadSecureLine(CancellationToken cancellationTok private static async Task ReadKeyAsync(CancellationToken cancellationToken) { - return await s_consoleProxy.ReadKeyAsync(cancellationToken); + return await ConsoleProxy.ReadKeyAsync(cancellationToken); + } + + private async Task ReadLineAsync(bool isCommandLine, CancellationToken cancellationToken) + { + return await this.powerShellContext.InvokeReadLineAsync(isCommandLine, cancellationToken); } - private async Task ReadLine(bool isCommandLine, CancellationToken cancellationToken) + /// + /// Invokes a custom ReadLine method that is similar to but more basic than PSReadLine. + /// This method should be used when PSReadLine is disabled, either by user settings or + /// unsupported PowerShell versions. + /// + /// + /// Indicates whether ReadLine should act like a command line. + /// + /// + /// The cancellation token that will be checked prior to completing the returned task. + /// + /// + /// A task object representing the asynchronus operation. The Result property on + /// the task object returns the user input string. + /// + internal async Task InvokeLegacyReadLineAsync(bool isCommandLine, CancellationToken cancellationToken) { string inputBeforeCompletion = null; string inputAfterCompletion = null; @@ -155,8 +163,8 @@ private async Task ReadLine(bool isCommandLine, CancellationToken cancel StringBuilder inputLine = new StringBuilder(); - int initialCursorCol = Console.CursorLeft; - int initialCursorRow = Console.CursorTop; + int initialCursorCol = await ConsoleProxy.GetCursorLeftAsync(cancellationToken); + int initialCursorRow = await ConsoleProxy.GetCursorTopAsync(cancellationToken); int initialWindowLeft = Console.WindowLeft; int initialWindowTop = Console.WindowTop; @@ -200,13 +208,13 @@ private async Task ReadLine(bool isCommandLine, CancellationToken cancel command.AddParameter("Options", null); var results = - await this.powerShellContext.ExecuteCommand(command, false, false); + await this.powerShellContext.ExecuteCommandAsync(command, false, false); currentCompletion = results.FirstOrDefault(); } else { - using (RunspaceHandle runspaceHandle = await this.powerShellContext.GetRunspaceHandle()) + using (RunspaceHandle runspaceHandle = await this.powerShellContext.GetRunspaceHandleAsync()) using (PowerShell powerShell = PowerShell.Create()) { powerShell.Runspace = runspaceHandle.Runspace; @@ -318,7 +326,7 @@ private async Task ReadLine(bool isCommandLine, CancellationToken cancel command.AddCommand("Get-History"); currentHistory = - await this.powerShellContext.ExecuteCommand( + await this.powerShellContext.ExecuteCommandAsync( command, false, false) as Collection; @@ -487,8 +495,8 @@ private int CalculateIndexFromCursor( int consoleWidth) { return - ((Console.CursorTop - promptStartRow) * consoleWidth) + - Console.CursorLeft - promptStartCol; + ((ConsoleProxy.GetCursorTop() - promptStartRow) * consoleWidth) + + ConsoleProxy.GetCursorLeft() - promptStartCol; } private void CalculateCursorFromIndex( diff --git a/src/PowerShellEditorServices/Console/IConsoleOperations.cs b/src/PowerShellEditorServices/Console/IConsoleOperations.cs index 721ae8ff7..a5556eda5 100644 --- a/src/PowerShellEditorServices/Console/IConsoleOperations.cs +++ b/src/PowerShellEditorServices/Console/IConsoleOperations.cs @@ -1,3 +1,8 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using System; using System.Threading; using System.Threading.Tasks; @@ -18,5 +23,97 @@ public interface IConsoleOperations /// A task that will complete with a result of the key pressed by the user. /// Task ReadKeyAsync(CancellationToken cancellationToken); + + /// + /// Obtains the horizontal position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The horizontal position of the console cursor. + int GetCursorLeft(); + + /// + /// Obtains the horizontal position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The to observe. + /// The horizontal position of the console cursor. + int GetCursorLeft(CancellationToken cancellationToken); + + /// + /// Obtains the horizontal position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// + /// A representing the asynchronous operation. The + /// property will return the horizontal position + /// of the console cursor. + /// + Task GetCursorLeftAsync(); + + /// + /// Obtains the horizontal position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The to observe. + /// + /// A representing the asynchronous operation. The + /// property will return the horizontal position + /// of the console cursor. + /// + Task GetCursorLeftAsync(CancellationToken cancellationToken); + + /// + /// Obtains the vertical position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The vertical position of the console cursor. + int GetCursorTop(); + + /// + /// Obtains the vertical position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The to observe. + /// The vertical position of the console cursor. + int GetCursorTop(CancellationToken cancellationToken); + + /// + /// Obtains the vertical position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// + /// A representing the asynchronous operation. The + /// property will return the vertical position + /// of the console cursor. + /// + Task GetCursorTopAsync(); + + /// + /// Obtains the vertical position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The to observe. + /// + /// A representing the asynchronous operation. The + /// property will return the vertical position + /// of the console cursor. + /// + Task GetCursorTopAsync(CancellationToken cancellationToken); } } diff --git a/src/PowerShellEditorServices/Console/InputPromptHandler.cs b/src/PowerShellEditorServices/Console/InputPromptHandler.cs index 0b8e4ff22..7d626c9ab 100644 --- a/src/PowerShellEditorServices/Console/InputPromptHandler.cs +++ b/src/PowerShellEditorServices/Console/InputPromptHandler.cs @@ -58,11 +58,11 @@ public InputPromptHandler(ILogger logger) : base(logger) /// A Task instance that can be monitored for completion to get /// the user's input. /// - public Task PromptForInput( + public Task PromptForInputAsync( CancellationToken cancellationToken) { Task> innerTask = - this.PromptForInput( + this.PromptForInputAsync( null, null, new FieldDetails[] { new FieldDetails("", "", typeof(string), false, "") }, @@ -106,7 +106,7 @@ public Task PromptForInput( /// A Task instance that can be monitored for completion to get /// the user's input. /// - public async Task> PromptForInput( + public async Task> PromptForInputAsync( string promptCaption, string promptMessage, FieldDetails[] fields, @@ -120,7 +120,7 @@ public async Task> PromptForInput( this.ShowPromptMessage(promptCaption, promptMessage); Task> promptTask = - this.StartPromptLoop(this.promptCancellationTokenSource.Token); + this.StartPromptLoopAsync(this.promptCancellationTokenSource.Token); Task finishedTask = await Task.WhenAny( @@ -142,11 +142,11 @@ await Task.WhenAny( /// A Task instance that can be monitored for completion to get /// the user's input. /// - public Task PromptForSecureInput( + public Task PromptForSecureInputAsync( CancellationToken cancellationToken) { Task> innerTask = - this.PromptForInput( + this.PromptForInputAsync( null, null, new FieldDetails[] { new FieldDetails("", "", typeof(SecureString), false, "") }, @@ -209,7 +209,7 @@ protected override void OnPromptCancelled() /// A Task instance that can be monitored for completion to get /// the user's input. /// - protected abstract Task ReadInputString(CancellationToken cancellationToken); + protected abstract Task ReadInputStringAsync(CancellationToken cancellationToken); /// /// Reads a SecureString asynchronously from the console. @@ -221,7 +221,7 @@ protected override void OnPromptCancelled() /// A Task instance that can be monitored for completion to get /// the user's input. /// - protected abstract Task ReadSecureString(CancellationToken cancellationToken); + protected abstract Task ReadSecureStringAsync(CancellationToken cancellationToken); /// /// Called when an error should be displayed, such as when the @@ -237,7 +237,7 @@ protected override void OnPromptCancelled() #region Private Methods - private async Task> StartPromptLoop( + private async Task> StartPromptLoopAsync( CancellationToken cancellationToken) { this.GetNextField(); @@ -255,13 +255,13 @@ private async Task> StartPromptLoop( // Read input depending on field type if (this.currentField.FieldType == typeof(SecureString)) { - SecureString secureString = await this.ReadSecureString(cancellationToken); + SecureString secureString = await this.ReadSecureStringAsync(cancellationToken); responseValue = secureString; enteredValue = secureString != null; } else { - responseString = await this.ReadInputString(cancellationToken); + responseString = await this.ReadInputStringAsync(cancellationToken); responseValue = responseString; enteredValue = responseString != null && responseString.Length > 0; diff --git a/src/PowerShellEditorServices/Console/TerminalChoicePromptHandler.cs b/src/PowerShellEditorServices/Console/TerminalChoicePromptHandler.cs index b99b959cd..1bf5a5cc4 100644 --- a/src/PowerShellEditorServices/Console/TerminalChoicePromptHandler.cs +++ b/src/PowerShellEditorServices/Console/TerminalChoicePromptHandler.cs @@ -52,9 +52,9 @@ public TerminalChoicePromptHandler( /// /// A CancellationToken that can be used to cancel the prompt. /// A Task that can be awaited to get the user's response. - protected override async Task ReadInputString(CancellationToken cancellationToken) + protected override async Task ReadInputStringAsync(CancellationToken cancellationToken) { - string inputString = await this.consoleReadLine.ReadSimpleLine(cancellationToken); + string inputString = await this.consoleReadLine.ReadSimpleLineAsync(cancellationToken); this.hostOutput.WriteOutput(string.Empty); return inputString; diff --git a/src/PowerShellEditorServices/Console/TerminalInputPromptHandler.cs b/src/PowerShellEditorServices/Console/TerminalInputPromptHandler.cs index e77bf2f9a..67b58bc55 100644 --- a/src/PowerShellEditorServices/Console/TerminalInputPromptHandler.cs +++ b/src/PowerShellEditorServices/Console/TerminalInputPromptHandler.cs @@ -54,9 +54,9 @@ public TerminalInputPromptHandler( /// /// A CancellationToken that can be used to cancel the prompt. /// A Task that can be awaited to get the user's response. - protected override async Task ReadInputString(CancellationToken cancellationToken) + protected override async Task ReadInputStringAsync(CancellationToken cancellationToken) { - string inputString = await this.consoleReadLine.ReadSimpleLine(cancellationToken); + string inputString = await this.consoleReadLine.ReadSimpleLineAsync(cancellationToken); this.hostOutput.WriteOutput(string.Empty); return inputString; @@ -67,9 +67,9 @@ protected override async Task ReadInputString(CancellationToken cancella /// /// A CancellationToken that can be used to cancel the prompt. /// A Task that can be awaited to get the user's response. - protected override async Task ReadSecureString(CancellationToken cancellationToken) + protected override async Task ReadSecureStringAsync(CancellationToken cancellationToken) { - SecureString secureString = await this.consoleReadLine.ReadSecureLine(cancellationToken); + SecureString secureString = await this.consoleReadLine.ReadSecureLineAsync(cancellationToken); this.hostOutput.WriteOutput(string.Empty); return secureString; diff --git a/src/PowerShellEditorServices/Console/UnixConsoleOperations.cs b/src/PowerShellEditorServices/Console/UnixConsoleOperations.cs index ab5cccfd6..df5ec2460 100644 --- a/src/PowerShellEditorServices/Console/UnixConsoleOperations.cs +++ b/src/PowerShellEditorServices/Console/UnixConsoleOperations.cs @@ -1,19 +1,33 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using System; using System.Threading; using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Utility; using UnixConsoleEcho; namespace Microsoft.PowerShell.EditorServices.Console { internal class UnixConsoleOperations : IConsoleOperations { - private const int LONG_READ_DELAY = 300; + private const int LongWaitForKeySleepTime = 300; + + private const int ShortWaitForKeyTimeout = 5000; + + private const int ShortWaitForKeySpinUntilSleepTime = 30; + + private static readonly ManualResetEventSlim s_waitHandle = new ManualResetEventSlim(); - private const int SHORT_READ_TIMEOUT = 5000; + private static readonly SemaphoreSlim s_readKeyHandle = AsyncUtils.CreateSimpleLockingSemaphore(); - private static readonly ManualResetEventSlim _waitHandle = new ManualResetEventSlim(); + private static readonly SemaphoreSlim s_stdInHandle = AsyncUtils.CreateSimpleLockingSemaphore(); - private SemaphoreSlim _readKeyHandle = new SemaphoreSlim(1, 1); + private Func WaitForKeyAvailable; + + private Func> WaitForKeyAvailableAsync; internal UnixConsoleOperations() { @@ -21,65 +35,250 @@ internal UnixConsoleOperations() // user has recently (last 5 seconds) pressed a key to avoid preventing // the CPU from entering low power mode. WaitForKeyAvailable = LongWaitForKey; + WaitForKeyAvailableAsync = LongWaitForKeyAsync; + } + + internal ConsoleKeyInfo ReadKey(bool intercept, CancellationToken cancellationToken) + { + s_readKeyHandle.Wait(cancellationToken); + + // On Unix platforms System.Console.ReadKey has an internal lock on stdin. Because + // of this, if a ReadKey call is pending in one thread and in another thread + // Console.CursorLeft is called, both threads block until a key is pressed. + + // To work around this we wait for a key to be pressed before actually calling Console.ReadKey. + // However, any pressed keys during this time will be echoed to the console. To get around + // this we use the UnixConsoleEcho package to disable echo prior to waiting. + InputEcho.Disable(); + try + { + // The WaitForKeyAvailable delegate switches between a long delay between waits and + // a short timeout depending on how recently a key has been pressed. This allows us + // to let the CPU enter low power mode without compromising responsiveness. + while (!WaitForKeyAvailable(cancellationToken)); + } + finally + { + InputEcho.Disable(); + s_readKeyHandle.Release(); + } + + // A key has been pressed, so aquire a lock on our internal stdin handle. This is done + // so any of our calls to cursor position API's do not release ReadKey. + s_stdInHandle.Wait(cancellationToken); + try + { + return System.Console.ReadKey(intercept); + } + finally + { + s_stdInHandle.Release(); + } } public async Task ReadKeyAsync(CancellationToken cancellationToken) { - await _readKeyHandle.WaitAsync(cancellationToken); + await s_readKeyHandle.WaitAsync(cancellationToken); // I tried to replace this library with a call to `stty -echo`, but unfortunately // the library also sets up allowing backspace to trigger `Console.KeyAvailable`. InputEcho.Disable(); try { - while (!await WaitForKeyAvailable(cancellationToken)); + while (!await WaitForKeyAvailableAsync(cancellationToken)); } finally { InputEcho.Enable(); - _readKeyHandle.Release(); + s_readKeyHandle.Release(); + } + + await s_stdInHandle.WaitAsync(cancellationToken); + try + { + return System.Console.ReadKey(intercept: true); + } + finally + { + s_stdInHandle.Release(); } + } - return System.Console.ReadKey(intercept: true); + public int GetCursorLeft() + { + return GetCursorLeft(CancellationToken.None); } - private Func> WaitForKeyAvailable; + public int GetCursorLeft(CancellationToken cancellationToken) + { + s_stdInHandle.Wait(cancellationToken); + try + { + return System.Console.CursorLeft; + } + finally + { + s_stdInHandle.Release(); + } + } + + public async Task GetCursorLeftAsync() + { + return await GetCursorLeftAsync(CancellationToken.None); + } - private async Task LongWaitForKey(CancellationToken cancellationToken) + public async Task GetCursorLeftAsync(CancellationToken cancellationToken) { - while (!System.Console.KeyAvailable) + await s_stdInHandle.WaitAsync(cancellationToken); + try + { + return System.Console.CursorLeft; + } + finally { - await Task.Delay(LONG_READ_DELAY, cancellationToken); + s_stdInHandle.Release(); } + } + + public int GetCursorTop() + { + return GetCursorTop(CancellationToken.None); + } + public int GetCursorTop(CancellationToken cancellationToken) + { + s_stdInHandle.Wait(cancellationToken); + try + { + return System.Console.CursorTop; + } + finally + { + s_stdInHandle.Release(); + } + } + + public async Task GetCursorTopAsync() + { + return await GetCursorTopAsync(CancellationToken.None); + } + + public async Task GetCursorTopAsync(CancellationToken cancellationToken) + { + await s_stdInHandle.WaitAsync(cancellationToken); + try + { + return System.Console.CursorTop; + } + finally + { + s_stdInHandle.Release(); + } + } + + private bool LongWaitForKey(CancellationToken cancellationToken) + { + // Wait for a key to be buffered (in other words, wait for Console.KeyAvailable to become + // true) with a long delay between checks. + while (!IsKeyAvailable(cancellationToken)) + { + s_waitHandle.Wait(LongWaitForKeySleepTime, cancellationToken); + } + + // As soon as a key is buffered, return true and switch the wait logic to be more + // responsive, but also more expensive. WaitForKeyAvailable = ShortWaitForKey; return true; } - private async Task ShortWaitForKey(CancellationToken cancellationToken) + private async Task LongWaitForKeyAsync(CancellationToken cancellationToken) + { + while (!await IsKeyAvailableAsync(cancellationToken)) + { + await Task.Delay(LongWaitForKeySleepTime, cancellationToken); + } + + WaitForKeyAvailableAsync = ShortWaitForKeyAsync; + return true; + } + + private bool ShortWaitForKey(CancellationToken cancellationToken) { - if (await SpinUntilKeyAvailable(SHORT_READ_TIMEOUT, cancellationToken)) + // Check frequently for a new key to be buffered. + if (SpinUntilKeyAvailable(ShortWaitForKeyTimeout, cancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); return true; } + // If the user has not pressed a key before the end of the SpinUntil timeout then + // the user is idle and we can switch back to long delays between KeyAvailable checks. cancellationToken.ThrowIfCancellationRequested(); WaitForKeyAvailable = LongWaitForKey; return false; } - private async Task SpinUntilKeyAvailable(int millisecondsTimeout, CancellationToken cancellationToken) + private async Task ShortWaitForKeyAsync(CancellationToken cancellationToken) + { + if (await SpinUntilKeyAvailableAsync(ShortWaitForKeyTimeout, cancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + return true; + } + + cancellationToken.ThrowIfCancellationRequested(); + WaitForKeyAvailableAsync = LongWaitForKeyAsync; + return false; + } + + private bool SpinUntilKeyAvailable(int millisecondsTimeout, CancellationToken cancellationToken) + { + return SpinWait.SpinUntil( + () => + { + s_waitHandle.Wait(ShortWaitForKeySpinUntilSleepTime, cancellationToken); + return IsKeyAvailable(cancellationToken); + }, + millisecondsTimeout); + } + + private async Task SpinUntilKeyAvailableAsync(int millisecondsTimeout, CancellationToken cancellationToken) { return await Task.Factory.StartNew( () => SpinWait.SpinUntil( () => { // The wait handle is never set, it's just used to enable cancelling the wait. - _waitHandle.Wait(30, cancellationToken); - return System.Console.KeyAvailable || cancellationToken.IsCancellationRequested; + s_waitHandle.Wait(ShortWaitForKeySpinUntilSleepTime, cancellationToken); + return IsKeyAvailable(cancellationToken); }, millisecondsTimeout)); } + + private bool IsKeyAvailable(CancellationToken cancellationToken) + { + s_stdInHandle.Wait(cancellationToken); + try + { + return System.Console.KeyAvailable; + } + finally + { + s_stdInHandle.Release(); + } + } + + private async Task IsKeyAvailableAsync(CancellationToken cancellationToken) + { + await s_stdInHandle.WaitAsync(cancellationToken); + try + { + return System.Console.KeyAvailable; + } + finally + { + s_stdInHandle.Release(); + } + } } } diff --git a/src/PowerShellEditorServices/Console/WindowsConsoleOperations.cs b/src/PowerShellEditorServices/Console/WindowsConsoleOperations.cs index 3158c87c4..86c543123 100644 --- a/src/PowerShellEditorServices/Console/WindowsConsoleOperations.cs +++ b/src/PowerShellEditorServices/Console/WindowsConsoleOperations.cs @@ -1,6 +1,12 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using System; using System.Threading; using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Utility; namespace Microsoft.PowerShell.EditorServices.Console { @@ -8,7 +14,23 @@ internal class WindowsConsoleOperations : IConsoleOperations { private ConsoleKeyInfo? _bufferedKey; - private SemaphoreSlim _readKeyHandle = new SemaphoreSlim(1, 1); + private SemaphoreSlim _readKeyHandle = AsyncUtils.CreateSimpleLockingSemaphore(); + + public int GetCursorLeft() => System.Console.CursorLeft; + + public int GetCursorLeft(CancellationToken cancellationToken) => System.Console.CursorLeft; + + public Task GetCursorLeftAsync() => Task.FromResult(System.Console.CursorLeft); + + public Task GetCursorLeftAsync(CancellationToken cancellationToken) => Task.FromResult(System.Console.CursorLeft); + + public int GetCursorTop() => System.Console.CursorTop; + + public int GetCursorTop(CancellationToken cancellationToken) => System.Console.CursorTop; + + public Task GetCursorTopAsync() => Task.FromResult(System.Console.CursorTop); + + public Task GetCursorTopAsync(CancellationToken cancellationToken) => Task.FromResult(System.Console.CursorTop); public async Task ReadKeyAsync(CancellationToken cancellationToken) { diff --git a/src/PowerShellEditorServices/Debugging/DebugService.cs b/src/PowerShellEditorServices/Debugging/DebugService.cs index 0eae58dd2..98d47d7af 100644 --- a/src/PowerShellEditorServices/Debugging/DebugService.cs +++ b/src/PowerShellEditorServices/Debugging/DebugService.cs @@ -15,6 +15,7 @@ using Microsoft.PowerShell.EditorServices.Utility; using Microsoft.PowerShell.EditorServices.Session; using Microsoft.PowerShell.EditorServices.Session.Capabilities; +using System.Threading; namespace Microsoft.PowerShell.EditorServices { @@ -47,6 +48,7 @@ public class DebugService private static int breakpointHitCounter = 0; + private SemaphoreSlim debugInfoHandle = AsyncUtils.CreateSimpleLockingSemaphore(); #endregion #region Properties @@ -106,7 +108,7 @@ public DebugService( this.logger = logger; this.powerShellContext = powerShellContext; - this.powerShellContext.DebuggerStop += this.OnDebuggerStop; + this.powerShellContext.DebuggerStop += this.OnDebuggerStopAsync; this.powerShellContext.DebuggerResumed += this.OnDebuggerResumed; this.powerShellContext.BreakpointUpdated += this.OnBreakpointUpdated; @@ -131,7 +133,7 @@ public DebugService( /// BreakpointDetails for each breakpoint that will be set. /// If true, causes all existing breakpoints to be cleared before setting new ones. /// An awaitable Task that will provide details about the breakpoints that were set. - public async Task SetLineBreakpoints( + public async Task SetLineBreakpointsAsync( ScriptFile scriptFile, BreakpointDetails[] breakpoints, bool clearExisting = true) @@ -184,7 +186,7 @@ public async Task SetLineBreakpoints( { if (clearExisting) { - await this.ClearBreakpointsInFile(scriptFile); + await this.ClearBreakpointsInFileAsync(scriptFile); } foreach (BreakpointDetails breakpoint in breakpoints) @@ -222,7 +224,7 @@ public async Task SetLineBreakpoints( } IEnumerable configuredBreakpoints = - await this.powerShellContext.ExecuteCommand(psCommand); + await this.powerShellContext.ExecuteCommandAsync(psCommand); // The order in which the breakpoints are returned is significant to the // VSCode client and should match the order in which they are passed in. @@ -233,7 +235,7 @@ public async Task SetLineBreakpoints( else { resultBreakpointDetails = - await dscBreakpoints.SetLineBreakpoints( + await dscBreakpoints.SetLineBreakpointsAsync( this.powerShellContext, escapedScriptPath, breakpoints); @@ -248,7 +250,7 @@ await dscBreakpoints.SetLineBreakpoints( /// CommandBreakpointDetails for each command breakpoint that will be set. /// If true, causes all existing function breakpoints to be cleared before setting new ones. /// An awaitable Task that will provide details about the breakpoints that were set. - public async Task SetCommandBreakpoints( + public async Task SetCommandBreakpointsAsync( CommandBreakpointDetails[] breakpoints, bool clearExisting = true) { @@ -256,7 +258,7 @@ public async Task SetCommandBreakpoints( if (clearExisting) { - await this.ClearCommandBreakpoints(); + await this.ClearCommandBreakpointsAsync(); } if (breakpoints.Length > 0) @@ -285,7 +287,7 @@ public async Task SetCommandBreakpoints( } IEnumerable configuredBreakpoints = - await this.powerShellContext.ExecuteCommand(psCommand); + await this.powerShellContext.ExecuteCommandAsync(psCommand); // The order in which the breakpoints are returned is significant to the // VSCode client and should match the order in which they are passed in. @@ -350,7 +352,7 @@ public void Break() /// public void Abort() { - this.powerShellContext.AbortExecution(); + this.powerShellContext.AbortExecution(shouldAbortDebugSession: true); } /// @@ -362,33 +364,40 @@ public void Abort() public VariableDetailsBase[] GetVariables(int variableReferenceId) { VariableDetailsBase[] childVariables; - - if ((variableReferenceId < 0) || (variableReferenceId >= this.variables.Count)) + this.debugInfoHandle.Wait(); + try { - logger.Write(LogLevel.Warning, $"Received request for variableReferenceId {variableReferenceId} that is out of range of valid indices."); - return new VariableDetailsBase[0]; - } + if ((variableReferenceId < 0) || (variableReferenceId >= this.variables.Count)) + { + logger.Write(LogLevel.Warning, $"Received request for variableReferenceId {variableReferenceId} that is out of range of valid indices."); + return new VariableDetailsBase[0]; + } - VariableDetailsBase parentVariable = this.variables[variableReferenceId]; - if (parentVariable.IsExpandable) - { - childVariables = parentVariable.GetChildren(this.logger); - foreach (var child in childVariables) + VariableDetailsBase parentVariable = this.variables[variableReferenceId]; + if (parentVariable.IsExpandable) { - // Only add child if it hasn't already been added. - if (child.Id < 0) + childVariables = parentVariable.GetChildren(this.logger); + foreach (var child in childVariables) { - child.Id = this.nextVariableId++; - this.variables.Add(child); + // Only add child if it hasn't already been added. + if (child.Id < 0) + { + child.Id = this.nextVariableId++; + this.variables.Add(child); + } } } + else + { + childVariables = new VariableDetailsBase[0]; + } + + return childVariables; } - else + finally { - childVariables = new VariableDetailsBase[0]; + this.debugInfoHandle.Release(); } - - return childVariables; } /// @@ -410,7 +419,18 @@ public VariableDetailsBase GetVariableFromExpression(string variableExpression, string[] variablePathParts = variableExpression.Split('.'); VariableDetailsBase resolvedVariable = null; - IEnumerable variableList = this.variables; + IEnumerable variableList; + + // Ensure debug info isn't currently being built. + this.debugInfoHandle.Wait(); + try + { + variableList = this.variables; + } + finally + { + this.debugInfoHandle.Release(); + } foreach (var variableName in variablePathParts) { @@ -450,7 +470,7 @@ public VariableDetailsBase GetVariableFromExpression(string variableExpression, /// The new string value. This value must not be null. If you want to set the variable to $null /// pass in the string "$null". /// The string representation of the value the variable was set to. - public async Task SetVariable(int variableContainerReferenceId, string name, string value) + public async Task SetVariableAsync(int variableContainerReferenceId, string name, string value) { Validate.IsNotNull(nameof(name), name); Validate.IsNotNull(nameof(value), value); @@ -468,7 +488,7 @@ public async Task SetVariable(int variableContainerReferenceId, string n psCommand.AddScript(value); var errorMessages = new StringBuilder(); var results = - await this.powerShellContext.ExecuteCommand( + await this.powerShellContext.ExecuteCommandAsync( psCommand, errorMessages, false, @@ -491,9 +511,18 @@ await this.powerShellContext.ExecuteCommand( // OK, now we have a PS object from the supplied value string (expression) to assign to a variable. // Get the variable referenced by variableContainerReferenceId and variable name. - VariableContainerDetails variableContainer = (VariableContainerDetails)this.variables[variableContainerReferenceId]; - VariableDetailsBase variable = variableContainer.Children[name]; + VariableContainerDetails variableContainer = null; + await this.debugInfoHandle.WaitAsync(); + try + { + variableContainer = (VariableContainerDetails)this.variables[variableContainerReferenceId]; + } + finally + { + this.debugInfoHandle.Release(); + } + VariableDetailsBase variable = variableContainer.Children[name]; // Determine scope in which the variable lives. This is required later for the call to Get-Variable -Scope. string scope = null; if (variableContainerReferenceId == this.scriptScopeVariables.Id) @@ -507,9 +536,10 @@ await this.powerShellContext.ExecuteCommand( else { // Determine which stackframe's local scope the variable is in. - for (int i = 0; i < this.stackFrameDetails.Length; i++) + StackFrameDetails[] stackFrames = await this.GetStackFramesAsync(); + for (int i = 0; i < stackFrames.Length; i++) { - var stackFrame = this.stackFrameDetails[i]; + var stackFrame = stackFrames[i]; if (stackFrame.LocalVariables.ContainsVariable(variable.Id)) { scope = i.ToString(); @@ -531,7 +561,7 @@ await this.powerShellContext.ExecuteCommand( psCommand.AddParameter("Name", name.TrimStart('$')); psCommand.AddParameter("Scope", scope); - IEnumerable result = await this.powerShellContext.ExecuteCommand(psCommand, sendErrorToHost: false); + IEnumerable result = await this.powerShellContext.ExecuteCommandAsync(psCommand, sendErrorToHost: false); PSVariable psVariable = result.FirstOrDefault(); if (psVariable == null) { @@ -560,7 +590,7 @@ await this.powerShellContext.ExecuteCommand( errorMessages.Clear(); var getExecContextResults = - await this.powerShellContext.ExecuteCommand( + await this.powerShellContext.ExecuteCommandAsync( psCommand, errorMessages, sendErrorToHost: false); @@ -598,13 +628,13 @@ await this.powerShellContext.ExecuteCommand( /// If true, writes the expression result as host output rather than returning the results. /// In this case, the return value of this function will be null. /// A VariableDetails object containing the result. - public async Task EvaluateExpression( + public async Task EvaluateExpressionAsync( string expressionString, int stackFrameId, bool writeResultAsOutput) { var results = - await this.powerShellContext.ExecuteScriptString( + await this.powerShellContext.ExecuteScriptStringAsync( expressionString, false, writeResultAsOutput); @@ -637,7 +667,54 @@ await this.powerShellContext.ExecuteScriptString( /// public StackFrameDetails[] GetStackFrames() { - return this.stackFrameDetails; + this.debugInfoHandle.Wait(); + try + { + return this.stackFrameDetails; + } + finally + { + this.debugInfoHandle.Release(); + } + } + + internal StackFrameDetails[] GetStackFrames(CancellationToken cancellationToken) + { + this.debugInfoHandle.Wait(cancellationToken); + try + { + return this.stackFrameDetails; + } + finally + { + this.debugInfoHandle.Release(); + } + } + + internal async Task GetStackFramesAsync() + { + await this.debugInfoHandle.WaitAsync(); + try + { + return this.stackFrameDetails; + } + finally + { + this.debugInfoHandle.Release(); + } + } + + internal async Task GetStackFramesAsync(CancellationToken cancellationToken) + { + await this.debugInfoHandle.WaitAsync(cancellationToken); + try + { + return this.stackFrameDetails; + } + finally + { + this.debugInfoHandle.Release(); + } } /// @@ -648,8 +725,9 @@ public StackFrameDetails[] GetStackFrames() /// The list of VariableScope instances which describe the available variable scopes. public VariableScope[] GetVariableScopes(int stackFrameId) { - int localStackFrameVariableId = this.stackFrameDetails[stackFrameId].LocalVariables.Id; - int autoVariablesId = this.stackFrameDetails[stackFrameId].AutoVariables.Id; + var stackFrames = this.GetStackFrames(); + int localStackFrameVariableId = stackFrames[stackFrameId].LocalVariables.Id; + int autoVariablesId = stackFrames[stackFrameId].AutoVariables.Id; return new VariableScope[] { @@ -663,20 +741,20 @@ public VariableScope[] GetVariableScopes(int stackFrameId) /// /// Clears all breakpoints in the current session. /// - public async Task ClearAllBreakpoints() + public async Task ClearAllBreakpointsAsync() { PSCommand psCommand = new PSCommand(); psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Get-PSBreakpoint"); psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Remove-PSBreakpoint"); - await this.powerShellContext.ExecuteCommand(psCommand); + await this.powerShellContext.ExecuteCommandAsync(psCommand); } #endregion #region Private Methods - private async Task ClearBreakpointsInFile(ScriptFile scriptFile) + private async Task ClearBreakpointsInFileAsync(ScriptFile scriptFile) { List breakpoints = null; @@ -689,7 +767,7 @@ private async Task ClearBreakpointsInFile(ScriptFile scriptFile) psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Remove-PSBreakpoint"); psCommand.AddParameter("Id", breakpoints.Select(b => b.Id).ToArray()); - await this.powerShellContext.ExecuteCommand(psCommand); + await this.powerShellContext.ExecuteCommandAsync(psCommand); // Clear the existing breakpoints list for the file breakpoints.Clear(); @@ -697,41 +775,49 @@ private async Task ClearBreakpointsInFile(ScriptFile scriptFile) } } - private async Task ClearCommandBreakpoints() + private async Task ClearCommandBreakpointsAsync() { PSCommand psCommand = new PSCommand(); psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Get-PSBreakpoint"); psCommand.AddParameter("Type", "Command"); psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Remove-PSBreakpoint"); - await this.powerShellContext.ExecuteCommand(psCommand); + await this.powerShellContext.ExecuteCommandAsync(psCommand); } - private async Task FetchStackFramesAndVariables(string scriptNameOverride) + private async Task FetchStackFramesAndVariablesAsync(string scriptNameOverride) { - this.nextVariableId = VariableDetailsBase.FirstVariableId; - this.variables = new List(); + await this.debugInfoHandle.WaitAsync(); + try + { + this.nextVariableId = VariableDetailsBase.FirstVariableId; + this.variables = new List(); - // Create a dummy variable for index 0, should never see this. - this.variables.Add(new VariableDetails("Dummy", null)); + // Create a dummy variable for index 0, should never see this. + this.variables.Add(new VariableDetails("Dummy", null)); - // Must retrieve global/script variales before stack frame variables - // as we check stack frame variables against globals. - await FetchGlobalAndScriptVariables(); - await FetchStackFrames(scriptNameOverride); + // Must retrieve global/script variales before stack frame variables + // as we check stack frame variables against globals. + await FetchGlobalAndScriptVariablesAsync(); + await FetchStackFramesAsync(scriptNameOverride); + } + finally + { + this.debugInfoHandle.Release(); + } } - private async Task FetchGlobalAndScriptVariables() + private async Task FetchGlobalAndScriptVariablesAsync() { // Retrieve globals first as script variable retrieval needs to search globals. this.globalScopeVariables = - await FetchVariableContainer(VariableContainerDetails.GlobalScopeName, null); + await FetchVariableContainerAsync(VariableContainerDetails.GlobalScopeName, null); this.scriptScopeVariables = - await FetchVariableContainer(VariableContainerDetails.ScriptScopeName, null); + await FetchVariableContainerAsync(VariableContainerDetails.ScriptScopeName, null); } - private async Task FetchVariableContainer( + private async Task FetchVariableContainerAsync( string scope, VariableContainerDetails autoVariables) { @@ -743,7 +829,7 @@ private async Task FetchVariableContainer( new VariableContainerDetails(this.nextVariableId++, "Scope: " + scope); this.variables.Add(scopeVariableContainer); - var results = await this.powerShellContext.ExecuteCommand(psCommand, sendErrorToHost: false); + var results = await this.powerShellContext.ExecuteCommandAsync(psCommand, sendErrorToHost: false); if (results != null) { foreach (PSObject psVariableObject in results) @@ -837,7 +923,7 @@ private bool AddToAutoVariables(PSObject psvariable, string scope) return true; } - private async Task FetchStackFrames(string scriptNameOverride) + private async Task FetchStackFramesAsync(string scriptNameOverride) { PSCommand psCommand = new PSCommand(); @@ -848,9 +934,10 @@ private async Task FetchStackFrames(string scriptNameOverride) var callStackVarName = $"$global:{PsesGlobalVariableNamePrefix}CallStack"; psCommand.AddScript($"{callStackVarName} = Get-PSCallStack; {callStackVarName}"); - var results = await this.powerShellContext.ExecuteCommand(psCommand); + var results = await this.powerShellContext.ExecuteCommandAsync(psCommand); var callStackFrames = results.ToArray(); + this.stackFrameDetails = new StackFrameDetails[callStackFrames.Length]; for (int i = 0; i < callStackFrames.Length; i++) @@ -863,7 +950,7 @@ private async Task FetchStackFrames(string scriptNameOverride) this.variables.Add(autoVariables); VariableContainerDetails localVariables = - await FetchVariableContainer(i.ToString(), autoVariables); + await FetchVariableContainerAsync(i.ToString(), autoVariables); // When debugging, this is the best way I can find to get what is likely the workspace root. // This is controlled by the "cwd:" setting in the launch config. @@ -1103,7 +1190,7 @@ private string TrimScriptListingLine(PSObject scriptLineObj, ref int prefixLengt /// public event EventHandler DebuggerStopped; - private async void OnDebuggerStop(object sender, DebuggerStopEventArgs e) + private async void OnDebuggerStopAsync(object sender, DebuggerStopEventArgs e) { bool noScriptName = false; string localScriptPath = e.InvocationInfo.ScriptName; @@ -1116,7 +1203,7 @@ private async void OnDebuggerStop(object sender, DebuggerStopEventArgs e) command.AddScript($"list 1 {int.MaxValue}"); IEnumerable scriptListingLines = - await this.powerShellContext.ExecuteCommand( + await this.powerShellContext.ExecuteCommandAsync( command, false, false); if (scriptListingLines != null) @@ -1151,7 +1238,7 @@ await this.powerShellContext.ExecuteCommand( } // Get call stack and variables. - await this.FetchStackFramesAndVariables( + await this.FetchStackFramesAndVariablesAsync( noScriptName ? localScriptPath : null); // If this is a remote connection and the debugger stopped at a line @@ -1161,7 +1248,7 @@ await this.FetchStackFramesAndVariables( !noScriptName) { localScriptPath = - await this.remoteFileManager.FetchRemoteFile( + await this.remoteFileManager.FetchRemoteFileAsync( e.InvocationInfo.ScriptName, this.powerShellContext.CurrentRunspace); } diff --git a/src/PowerShellEditorServices/Extensions/EditorContext.cs b/src/PowerShellEditorServices/Extensions/EditorContext.cs index c29c60fcc..c669acd19 100644 --- a/src/PowerShellEditorServices/Extensions/EditorContext.cs +++ b/src/PowerShellEditorServices/Extensions/EditorContext.cs @@ -107,7 +107,7 @@ public void SetSelection( public void SetSelection(BufferRange selectionRange) { this.editorOperations - .SetSelection(selectionRange) + .SetSelectionAsync(selectionRange) .Wait(); } diff --git a/src/PowerShellEditorServices/Extensions/EditorObject.cs b/src/PowerShellEditorServices/Extensions/EditorObject.cs index e8f50d589..0013da684 100644 --- a/src/PowerShellEditorServices/Extensions/EditorObject.cs +++ b/src/PowerShellEditorServices/Extensions/EditorObject.cs @@ -105,7 +105,7 @@ public EditorCommand[] GetCommands() /// A instance of the EditorContext class. public EditorContext GetEditorContext() { - return this.editorOperations.GetEditorContext().Result; + return this.editorOperations.GetEditorContextAsync().Result; } } } diff --git a/src/PowerShellEditorServices/Extensions/EditorWindow.cs b/src/PowerShellEditorServices/Extensions/EditorWindow.cs index 0ffd125f0..8d241559a 100644 --- a/src/PowerShellEditorServices/Extensions/EditorWindow.cs +++ b/src/PowerShellEditorServices/Extensions/EditorWindow.cs @@ -28,7 +28,7 @@ internal EditorWindow(IEditorOperations editorOperations) this.editorOperations = editorOperations; } - #endregion + #endregion #region Public Methods @@ -38,7 +38,7 @@ internal EditorWindow(IEditorOperations editorOperations) /// The message to be shown. public void ShowInformationMessage(string message) { - this.editorOperations.ShowInformationMessage(message).Wait(); + this.editorOperations.ShowInformationMessageAsync(message).Wait(); } /// @@ -47,7 +47,7 @@ public void ShowInformationMessage(string message) /// The message to be shown. public void ShowErrorMessage(string message) { - this.editorOperations.ShowErrorMessage(message).Wait(); + this.editorOperations.ShowErrorMessageAsync(message).Wait(); } /// @@ -56,7 +56,7 @@ public void ShowErrorMessage(string message) /// The message to be shown. public void ShowWarningMessage(string message) { - this.editorOperations.ShowWarningMessage(message).Wait(); + this.editorOperations.ShowWarningMessageAsync(message).Wait(); } /// @@ -65,7 +65,7 @@ public void ShowWarningMessage(string message) /// The message to be shown. public void SetStatusBarMessage(string message) { - this.editorOperations.SetStatusBarMessage(message, null).Wait(); + this.editorOperations.SetStatusBarMessageAsync(message, null).Wait(); } /// @@ -75,9 +75,9 @@ public void SetStatusBarMessage(string message) /// A timeout in milliseconds for how long the message should remain visible. public void SetStatusBarMessage(string message, int timeout) { - this.editorOperations.SetStatusBarMessage(message, timeout).Wait(); + this.editorOperations.SetStatusBarMessageAsync(message, timeout).Wait(); } - #endregion + #endregion } } diff --git a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs index 9bdd2c043..ad679f371 100644 --- a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs +++ b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs @@ -45,7 +45,7 @@ internal EditorWorkspace(IEditorOperations editorOperations) /// public void NewFile() { - this.editorOperations.NewFile().Wait(); + this.editorOperations.NewFileAsync().Wait(); } /// @@ -55,7 +55,7 @@ public void NewFile() /// The path to the file to be opened. public void OpenFile(string filePath) { - this.editorOperations.OpenFile(filePath).Wait(); + this.editorOperations.OpenFileAsync(filePath).Wait(); } /// @@ -67,7 +67,7 @@ public void OpenFile(string filePath) /// Determines wether the file is opened as a preview or as a durable editor. public void OpenFile(string filePath, bool preview) { - this.editorOperations.OpenFile(filePath, preview).Wait(); + this.editorOperations.OpenFileAsync(filePath, preview).Wait(); } #endregion diff --git a/src/PowerShellEditorServices/Extensions/ExtensionService.cs b/src/PowerShellEditorServices/Extensions/ExtensionService.cs index d9defe6a3..cc4e01af4 100644 --- a/src/PowerShellEditorServices/Extensions/ExtensionService.cs +++ b/src/PowerShellEditorServices/Extensions/ExtensionService.cs @@ -71,7 +71,7 @@ public ExtensionService(PowerShellContext powerShellContext) /// An IEditorOperations implementation. /// An IComponentRegistry instance which provides components in the session. /// A Task that can be awaited for completion. - public async Task Initialize( + public async Task InitializeAsync( IEditorOperations editorOperations, IComponentRegistry componentRegistry) { @@ -83,7 +83,7 @@ public async Task Initialize( // Register the editor object in the runspace PSCommand variableCommand = new PSCommand(); - using (RunspaceHandle handle = await this.PowerShellContext.GetRunspaceHandle()) + using (RunspaceHandle handle = await this.PowerShellContext.GetRunspaceHandleAsync()) { handle.Runspace.SessionStateProxy.PSVariable.Set( "psEditor", @@ -97,7 +97,7 @@ public async Task Initialize( /// The unique name of the command to be invoked. /// The context in which the command is being invoked. /// A Task that can be awaited for completion. - public async Task InvokeCommand(string commandName, EditorContext editorContext) + public async Task InvokeCommandAsync(string commandName, EditorContext editorContext) { EditorCommand editorCommand; @@ -108,7 +108,7 @@ public async Task InvokeCommand(string commandName, EditorContext editorContext) executeCommand.AddParameter("ScriptBlock", editorCommand.ScriptBlock); executeCommand.AddParameter("ArgumentList", new object[] { editorContext }); - await this.PowerShellContext.ExecuteCommand( + await this.PowerShellContext.ExecuteCommandAsync( executeCommand, !editorCommand.SuppressOutput, true); diff --git a/src/PowerShellEditorServices/Extensions/FileContext.cs b/src/PowerShellEditorServices/Extensions/FileContext.cs index 1b563dcb2..3fc5ac120 100644 --- a/src/PowerShellEditorServices/Extensions/FileContext.cs +++ b/src/PowerShellEditorServices/Extensions/FileContext.cs @@ -237,7 +237,7 @@ public void InsertText( public void InsertText(string textToInsert, BufferRange insertRange) { this.editorOperations - .InsertText(this.scriptFile.ClientFilePath, textToInsert, insertRange) + .InsertTextAsync(this.scriptFile.ClientFilePath, textToInsert, insertRange) .Wait(); } @@ -250,7 +250,7 @@ public void InsertText(string textToInsert, BufferRange insertRange) /// public void Save() { - this.editorOperations.SaveFile(this.scriptFile.FilePath); + this.editorOperations.SaveFileAsync(this.scriptFile.FilePath); } /// @@ -272,7 +272,7 @@ public void SaveAs(string newFilePath) throw new IOException(String.Format("The file '{0}' already exists", absolutePath)); } - this.editorOperations.SaveFile(this.scriptFile.FilePath, newFilePath); + this.editorOperations.SaveFileAsync(this.scriptFile.FilePath, newFilePath); } #endregion diff --git a/src/PowerShellEditorServices/Extensions/IEditorOperations.cs b/src/PowerShellEditorServices/Extensions/IEditorOperations.cs index 51d0ac5b7..7ef6bdf0e 100644 --- a/src/PowerShellEditorServices/Extensions/IEditorOperations.cs +++ b/src/PowerShellEditorServices/Extensions/IEditorOperations.cs @@ -18,7 +18,7 @@ public interface IEditorOperations /// Gets the EditorContext for the editor's current state. /// /// A new EditorContext object. - Task GetEditorContext(); + Task GetEditorContextAsync(); /// /// Gets the path to the editor's active workspace. @@ -37,7 +37,7 @@ public interface IEditorOperations /// Causes a new untitled file to be created in the editor. /// /// A task that can be awaited for completion. - Task NewFile(); + Task NewFileAsync(); /// /// Causes a file to be opened in the editor. If the file is @@ -45,7 +45,7 @@ public interface IEditorOperations /// /// The path of the file to be opened. /// A Task that can be tracked for completion. - Task OpenFile(string filePath); + Task OpenFileAsync(string filePath); /// /// Causes a file to be opened in the editor. If the file is @@ -55,21 +55,21 @@ public interface IEditorOperations /// The path of the file to be opened. /// Determines wether the file is opened as a preview or as a durable editor. /// A Task that can be tracked for completion. - Task OpenFile(string filePath, bool preview); + Task OpenFileAsync(string filePath, bool preview); /// /// Causes a file to be closed in the editor. /// /// The path of the file to be closed. /// A Task that can be tracked for completion. - Task CloseFile(string filePath); + Task CloseFileAsync(string filePath); /// /// Causes a file to be saved in the editor. /// /// The path of the file to be saved. /// A Task that can be tracked for completion. - Task SaveFile(string filePath); + Task SaveFileAsync(string filePath); /// /// Causes a file to be saved as a new file in a new editor window. @@ -77,7 +77,7 @@ public interface IEditorOperations /// the path of the current file being saved /// the path of the new file where the current window content will be saved /// - Task SaveFile(string oldFilePath, string newFilePath); + Task SaveFileAsync(string oldFilePath, string newFilePath); /// /// Inserts text into the specified range for the file at the specified path. @@ -86,35 +86,35 @@ public interface IEditorOperations /// The text to insert into the file. /// The range in the file to be replaced. /// A Task that can be tracked for completion. - Task InsertText(string filePath, string insertText, BufferRange insertRange); + Task InsertTextAsync(string filePath, string insertText, BufferRange insertRange); /// /// Causes the selection to be changed in the editor's active file buffer. /// /// The range over which the selection will be made. /// A Task that can be tracked for completion. - Task SetSelection(BufferRange selectionRange); + Task SetSelectionAsync(BufferRange selectionRange); /// /// Shows an informational message to the user. /// /// The message to be shown. /// A Task that can be tracked for completion. - Task ShowInformationMessage(string message); + Task ShowInformationMessageAsync(string message); /// /// Shows an error message to the user. /// /// The message to be shown. /// A Task that can be tracked for completion. - Task ShowErrorMessage(string message); + Task ShowErrorMessageAsync(string message); /// /// Shows a warning message to the user. /// /// The message to be shown. /// A Task that can be tracked for completion. - Task ShowWarningMessage(string message); + Task ShowWarningMessageAsync(string message); /// /// Sets the status bar message in the editor UI (if applicable). @@ -122,7 +122,7 @@ public interface IEditorOperations /// The message to be shown. /// If non-null, a timeout in milliseconds for how long the message should remain visible. /// A Task that can be tracked for completion. - Task SetStatusBarMessage(string message, int? timeout); + Task SetStatusBarMessageAsync(string message, int? timeout); } } diff --git a/src/PowerShellEditorServices/Language/AstOperations.cs b/src/PowerShellEditorServices/Language/AstOperations.cs index 9076b1cb9..354e5f188 100644 --- a/src/PowerShellEditorServices/Language/AstOperations.cs +++ b/src/PowerShellEditorServices/Language/AstOperations.cs @@ -17,6 +17,7 @@ namespace Microsoft.PowerShell.EditorServices { using System.Management.Automation; + using System.Management.Automation.Language; /// /// Provides common operations for the syntax tree of a parsed script. @@ -29,6 +30,8 @@ internal static class AstOperations .GetType("System.Management.Automation.Language.InternalScriptPosition") .GetMethod("CloneWithNewOffset", BindingFlags.Instance | BindingFlags.NonPublic); + private static readonly SemaphoreSlim s_completionHandle = AsyncUtils.CreateSimpleLockingSemaphore(); + /// /// Gets completions for the symbol found in the Ast at /// the given file offset. @@ -53,7 +56,7 @@ internal static class AstOperations /// A CommandCompletion instance that contains completions for the /// symbol at the given offset. /// - static public async Task GetCompletions( + static public async Task GetCompletionsAsync( Ast scriptAst, Token[] currentTokens, int fileOffset, @@ -61,74 +64,80 @@ static public async Task GetCompletions( ILogger logger, CancellationToken cancellationToken) { + if (!s_completionHandle.Wait(0)) + { + return null; + } - IScriptPosition cursorPosition = (IScriptPosition)s_extentCloneWithNewOffset.Invoke( + try + { + IScriptPosition cursorPosition = (IScriptPosition)s_extentCloneWithNewOffset.Invoke( scriptAst.Extent.StartScriptPosition, new object[] { fileOffset }); - logger.Write( - LogLevel.Verbose, - string.Format( - "Getting completions at offset {0} (line: {1}, column: {2})", - fileOffset, - cursorPosition.LineNumber, - cursorPosition.ColumnNumber)); + logger.Write( + LogLevel.Verbose, + string.Format( + "Getting completions at offset {0} (line: {1}, column: {2})", + fileOffset, + cursorPosition.LineNumber, + cursorPosition.ColumnNumber)); - CommandCompletion commandCompletion = null; - if (powerShellContext.IsDebuggerStopped) - { - PSCommand command = new PSCommand(); - command.AddCommand("TabExpansion2"); - command.AddParameter("Ast", scriptAst); - command.AddParameter("Tokens", currentTokens); - command.AddParameter("PositionOfCursor", cursorPosition); - command.AddParameter("Options", null); - - PSObject outputObject = - (await powerShellContext.ExecuteCommand(command, false, false)) - .FirstOrDefault(); - - if (outputObject != null) + if (!powerShellContext.IsAvailable) { - ErrorRecord errorRecord = outputObject.BaseObject as ErrorRecord; - if (errorRecord != null) - { - logger.WriteException( - "Encountered an error while invoking TabExpansion2 in the debugger", - errorRecord.Exception); - } - else + return null; + } + + var stopwatch = new Stopwatch(); + + // If the current runspace is out of process we can use + // CommandCompletion.CompleteInput because PSReadLine won't be taking up the + // main runspace. + if (powerShellContext.IsCurrentRunspaceOutOfProcess()) + { + using (RunspaceHandle runspaceHandle = await powerShellContext.GetRunspaceHandleAsync(cancellationToken)) + using (PowerShell powerShell = PowerShell.Create()) { - commandCompletion = outputObject.BaseObject as CommandCompletion; + powerShell.Runspace = runspaceHandle.Runspace; + stopwatch.Start(); + try + { + return CommandCompletion.CompleteInput( + scriptAst, + currentTokens, + cursorPosition, + options: null, + powershell: powerShell); + } + finally + { + stopwatch.Stop(); + logger.Write(LogLevel.Verbose, $"IntelliSense completed in {stopwatch.ElapsedMilliseconds}ms."); + } } } - } - else if (powerShellContext.CurrentRunspace.Runspace.RunspaceAvailability == - RunspaceAvailability.Available) - { - using (RunspaceHandle runspaceHandle = await powerShellContext.GetRunspaceHandle(cancellationToken)) - using (PowerShell powerShell = PowerShell.Create()) - { - powerShell.Runspace = runspaceHandle.Runspace; - Stopwatch stopwatch = new Stopwatch(); - stopwatch.Start(); - - commandCompletion = - CommandCompletion.CompleteInput( + CommandCompletion commandCompletion = null; + await powerShellContext.InvokeOnPipelineThreadAsync( + pwsh => + { + stopwatch.Start(); + commandCompletion = CommandCompletion.CompleteInput( scriptAst, currentTokens, cursorPosition, - null, - powerShell); + options: null, + powershell: pwsh); + }); + stopwatch.Stop(); + logger.Write(LogLevel.Verbose, $"IntelliSense completed in {stopwatch.ElapsedMilliseconds}ms."); - stopwatch.Stop(); - - logger.Write(LogLevel.Verbose, $"IntelliSense completed in {stopwatch.ElapsedMilliseconds}ms."); - } + return commandCompletion; + } + finally + { + s_completionHandle.Release(); } - - return commandCompletion; } /// diff --git a/src/PowerShellEditorServices/Language/CommandHelpers.cs b/src/PowerShellEditorServices/Language/CommandHelpers.cs index 1a834c410..9c72e0eef 100644 --- a/src/PowerShellEditorServices/Language/CommandHelpers.cs +++ b/src/PowerShellEditorServices/Language/CommandHelpers.cs @@ -36,7 +36,7 @@ public class CommandHelpers /// The name of the command. /// The PowerShellContext to use for running Get-Command. /// A CommandInfo object with details about the specified command. - public static async Task GetCommandInfo( + public static async Task GetCommandInfoAsync( string commandName, PowerShellContext powerShellContext) { @@ -59,7 +59,7 @@ public static async Task GetCommandInfo( return (await powerShellContext - .ExecuteCommand(command, false, false)) + .ExecuteCommandAsync(command, false, false)) .Select(o => o.BaseObject) .OfType() .FirstOrDefault(); @@ -71,7 +71,7 @@ public static async Task GetCommandInfo( /// The CommandInfo instance for the command. /// The PowerShellContext to use for getting command documentation. /// - public static async Task GetCommandSynopsis( + public static async Task GetCommandSynopsisAsync( CommandInfo commandInfo, PowerShellContext powerShellContext) { @@ -89,7 +89,7 @@ public static async Task GetCommandSynopsis( command.AddArgument(commandInfo); command.AddParameter("ErrorAction", "Ignore"); - var results = await powerShellContext.ExecuteCommand(command, false, false); + var results = await powerShellContext.ExecuteCommandAsync(command, false, false); helpObject = results.FirstOrDefault(); if (helpObject != null) diff --git a/src/PowerShellEditorServices/Language/CompletionResults.cs b/src/PowerShellEditorServices/Language/CompletionResults.cs index b41394146..fc8d2eb00 100644 --- a/src/PowerShellEditorServices/Language/CompletionResults.cs +++ b/src/PowerShellEditorServices/Language/CompletionResults.cs @@ -305,10 +305,8 @@ private static CompletionType ConvertCompletionResultType( case CompletionResultType.Type: return CompletionType.Type; -#if !PowerShellv3 case CompletionResultType.Keyword: return CompletionType.Keyword; -#endif case CompletionResultType.ProviderContainer: return CompletionType.Folder; diff --git a/src/PowerShellEditorServices/Language/FindSymbolsVisitor2.cs b/src/PowerShellEditorServices/Language/FindSymbolsVisitor2.cs index 226b96035..03628ee3e 100644 --- a/src/PowerShellEditorServices/Language/FindSymbolsVisitor2.cs +++ b/src/PowerShellEditorServices/Language/FindSymbolsVisitor2.cs @@ -8,8 +8,6 @@ namespace Microsoft.PowerShell.EditorServices { -#if PowerShellv5 - // TODO: Restore this when we figure out how to support multiple // PS versions in the new PSES-as-a-module world (issue #276) @@ -78,6 +76,5 @@ namespace Microsoft.PowerShell.EditorServices // return AstVisitAction.Continue; // } //} -#endif } diff --git a/src/PowerShellEditorServices/Language/LanguageService.cs b/src/PowerShellEditorServices/Language/LanguageService.cs index d5c58ea83..486306b58 100644 --- a/src/PowerShellEditorServices/Language/LanguageService.cs +++ b/src/PowerShellEditorServices/Language/LanguageService.cs @@ -40,6 +40,8 @@ public class LanguageService private readonly IDocumentSymbolProvider[] _documentSymbolProviders; + private readonly SemaphoreSlim _aliasHandle = AsyncUtils.CreateSimpleLockingSemaphore(); + private bool _areAliasesLoaded; private CompletionResults _mostRecentCompletions; @@ -101,7 +103,7 @@ public LanguageService( /// /// A CommandCompletion instance completions for the identified statement. /// - public async Task GetCompletionsInFile( + public async Task GetCompletionsInFileAsync( ScriptFile scriptFile, int lineNumber, int columnNumber) @@ -116,7 +118,7 @@ public async Task GetCompletionsInFile( columnNumber); CommandCompletion commandCompletion = - await AstOperations.GetCompletions( + await AstOperations.GetCompletionsAsync( scriptFile.ScriptAst, scriptFile.ScriptTokens, fileOffset, @@ -247,7 +249,7 @@ public SymbolReference FindFunctionDefinitionAtLocation( /// The line number at which the symbol can be located. /// The column number at which the symbol can be located. /// - public async Task FindSymbolDetailsAtLocation( + public async Task FindSymbolDetailsAtLocationAsync( ScriptFile scriptFile, int lineNumber, int columnNumber) @@ -267,7 +269,7 @@ public async Task FindSymbolDetailsAtLocation( symbolReference.FilePath = scriptFile.FilePath; symbolDetails = - await SymbolDetails.Create( + await SymbolDetails.CreateAsync( symbolReference, _powerShellContext); @@ -307,7 +309,7 @@ public FindOccurrencesResult FindSymbolsInFile(ScriptFile scriptFile) /// An array of scriptFiles too search for references in /// The workspace that will be searched for symbols /// FindReferencesResult - public async Task FindReferencesOfSymbol( + public async Task FindReferencesOfSymbolAsync( SymbolReference foundSymbol, ScriptFile[] referencedFiles, Workspace workspace) @@ -322,7 +324,7 @@ public async Task FindReferencesOfSymbol( foundSymbol.ScriptRegion.StartColumnNumber); // Make sure aliases have been loaded - await GetAliases(); + await GetAliasesAsync(); // We want to look for references first in referenced files, hence we use ordered dictionary // TODO: File system case-sensitivity is based on filesystem not OS, but OS is a much cheaper heuristic @@ -353,26 +355,34 @@ public async Task FindReferencesOfSymbol( foreach (object fileName in fileMap.Keys) { var file = (ScriptFile)fileMap[fileName]; + await _aliasHandle.WaitAsync(); + try + { - IEnumerable references = AstOperations.FindReferencesOfSymbol( - file.ScriptAst, - foundSymbol, - _cmdletToAliasDictionary, - _aliasToCmdletDictionary); + IEnumerable references = AstOperations.FindReferencesOfSymbol( + file.ScriptAst, + foundSymbol, + _cmdletToAliasDictionary, + _aliasToCmdletDictionary); - foreach (SymbolReference reference in references) - { - try - { - reference.SourceLine = file.GetLine(reference.ScriptRegion.StartLineNumber); - } - catch (ArgumentOutOfRangeException e) + foreach (SymbolReference reference in references) { - reference.SourceLine = string.Empty; - _logger.WriteException("Found reference is out of range in script file", e); + try + { + reference.SourceLine = file.GetLine(reference.ScriptRegion.StartLineNumber); + } + catch (ArgumentOutOfRangeException e) + { + reference.SourceLine = string.Empty; + _logger.WriteException("Found reference is out of range in script file", e); + } + reference.FilePath = file.FilePath; + symbolReferences.Add(reference); } - reference.FilePath = file.FilePath; - symbolReferences.Add(reference); + } + finally + { + _aliasHandle.Release(); } } @@ -392,7 +402,7 @@ public async Task FindReferencesOfSymbol( /// The symbol for which a definition will be found. /// The Workspace to which the ScriptFile belongs. /// The resulting GetDefinitionResult for the symbol's definition. - public async Task GetDefinitionOfSymbol( + public async Task GetDefinitionOfSymbolAsync( ScriptFile sourceFile, SymbolReference foundSymbol, Workspace workspace) @@ -471,7 +481,7 @@ public async Task GetDefinitionOfSymbol( if (foundDefinition == null) { CommandInfo cmdInfo = - await CommandHelpers.GetCommandInfo( + await CommandHelpers.GetCommandInfoAsync( foundSymbol.SymbolName, _powerShellContext); @@ -546,7 +556,7 @@ public FindOccurrencesResult FindOccurrencesInFile( /// The line number of the cursor for the given script /// The coulumn number of the cursor for the given script /// ParameterSetSignatures - public async Task FindParameterSetsInFile( + public async Task FindParameterSetsInFileAsync( ScriptFile file, int lineNumber, int columnNumber) @@ -563,7 +573,7 @@ public async Task FindParameterSetsInFile( } CommandInfo commandInfo = - await CommandHelpers.GetCommandInfo( + await CommandHelpers.GetCommandInfoAsync( foundSymbol.SymbolName, _powerShellContext); @@ -717,26 +727,38 @@ public FunctionDefinitionAst GetFunctionDefinitionForHelpComment( /// /// Gets all aliases found in the runspace /// - private async Task GetAliases() + private async Task GetAliasesAsync() { if (_areAliasesLoaded) { return; } + await _aliasHandle.WaitAsync(); try { - RunspaceHandle runspaceHandle = - await _powerShellContext.GetRunspaceHandle( - new CancellationTokenSource(DefaultWaitTimeoutMilliseconds).Token); - - CommandInvocationIntrinsics invokeCommand = runspaceHandle.Runspace.SessionStateProxy.InvokeCommand; - IEnumerable aliases = invokeCommand.GetCommands("*", CommandTypes.Alias, true); + if (_powerShellContext.IsCurrentRunspaceOutOfProcess()) + { + _areAliasesLoaded = true; + return; + } - runspaceHandle.Dispose(); + var aliases = await _powerShellContext.ExecuteCommandAsync( + new PSCommand() + .AddCommand("Microsoft.PowerShell.Core\\Get-Command") + .AddParameter("CommandType", CommandTypes.Alias), + sendOutputToHost: false, + sendErrorToHost: false); foreach (AliasInfo aliasInfo in aliases) { + // Using Get-Command will obtain aliases from modules not yet loaded, + // these aliases will not have a definition. + if (string.IsNullOrEmpty(aliasInfo.Definition)) + { + continue; + } + if (!_cmdletToAliasDictionary.ContainsKey(aliasInfo.Definition)) { _cmdletToAliasDictionary.Add(aliasInfo.Definition, new List { aliasInfo.Name }); @@ -764,6 +786,10 @@ await _powerShellContext.GetRunspaceHandle( { // The wait for a RunspaceHandle has timed out, skip aliases for now } + finally + { + _aliasHandle.Release(); + } } private ScriptFile[] GetBuiltinCommandScriptFiles( diff --git a/src/PowerShellEditorServices/Language/SymbolDetails.cs b/src/PowerShellEditorServices/Language/SymbolDetails.cs index 02f027415..e0971c2ec 100644 --- a/src/PowerShellEditorServices/Language/SymbolDetails.cs +++ b/src/PowerShellEditorServices/Language/SymbolDetails.cs @@ -38,8 +38,8 @@ public class SymbolDetails #region Constructors - static internal async Task Create( - SymbolReference symbolReference, + static internal async Task CreateAsync( + SymbolReference symbolReference, PowerShellContext powerShellContext) { SymbolDetails symbolDetails = new SymbolDetails(); @@ -49,14 +49,14 @@ static internal async Task Create( if (symbolReference.SymbolType == SymbolType.Function) { CommandInfo commandInfo = - await CommandHelpers.GetCommandInfo( + await CommandHelpers.GetCommandInfoAsync( symbolReference.SymbolName, powerShellContext); if (commandInfo != null) { symbolDetails.Documentation = - await CommandHelpers.GetCommandSynopsis( + await CommandHelpers.GetCommandSynopsisAsync( commandInfo, powerShellContext); diff --git a/src/PowerShellEditorServices/PowerShellEditorServices.csproj b/src/PowerShellEditorServices/PowerShellEditorServices.csproj index d7b44185b..7de370b8c 100644 --- a/src/PowerShellEditorServices/PowerShellEditorServices.csproj +++ b/src/PowerShellEditorServices/PowerShellEditorServices.csproj @@ -3,63 +3,17 @@ PowerShell Editor Services Provides common PowerShell editor capabilities as a .NET library. - netstandard1.6;net452 + netstandard2.0 Microsoft.PowerShell.EditorServices - $(PackageTargetFallback);dnxcore50;portable-net45+win8 - - - - - - - - - $(DefineConstants);PowerShellv3 - $(DefineConstants);PowerShellv4 - $(DefineConstants);PowerShellv5r1;PowerShellv5 - $(DefineConstants);PowerShellv5r2;PowerShellv5 - - - + 1591,1573,1572 bin\$(TargetFramework)\$(Configuration)\Microsoft.PowerShell.EditorServices.xml - - - - - - - - - - - - - - - - - - - - 6.0.0-alpha13 - - - - - - - - - - - - - + + @@ -68,11 +22,6 @@ - - - $(DefineConstants);CoreCLR - - $(DefineConstants);RELEASE diff --git a/src/PowerShellEditorServices/Session/Capabilities/DscBreakpointCapability.cs b/src/PowerShellEditorServices/Session/Capabilities/DscBreakpointCapability.cs index a5907b0d5..998b58a79 100644 --- a/src/PowerShellEditorServices/Session/Capabilities/DscBreakpointCapability.cs +++ b/src/PowerShellEditorServices/Session/Capabilities/DscBreakpointCapability.cs @@ -21,7 +21,7 @@ internal class DscBreakpointCapability : IRunspaceCapability private Dictionary breakpointsPerFile = new Dictionary(); - public async Task> SetLineBreakpoints( + public async Task> SetLineBreakpointsAsync( PowerShellContext powerShellContext, string scriptPath, BreakpointDetails[] breakpoints) @@ -52,7 +52,7 @@ public async Task> SetLineBreakpoints( // Run Enable-DscDebug as a script because running it as a PSCommand // causes an error which states that the Breakpoint parameter has not // been passed. - await powerShellContext.ExecuteScriptString( + await powerShellContext.ExecuteScriptStringAsync( hashtableString.Length > 0 ? $"Enable-DscDebug -Breakpoint {hashtableString}" : "Disable-DscDebug", diff --git a/src/PowerShellEditorServices/Session/ExecutionOptions.cs b/src/PowerShellEditorServices/Session/ExecutionOptions.cs index 3372c7556..a1071606f 100644 --- a/src/PowerShellEditorServices/Session/ExecutionOptions.cs +++ b/src/PowerShellEditorServices/Session/ExecutionOptions.cs @@ -10,6 +10,8 @@ namespace Microsoft.PowerShell.EditorServices /// public class ExecutionOptions { + private bool? _shouldExecuteInOriginalRunspace; + #region Properties /// @@ -38,6 +40,36 @@ public class ExecutionOptions /// public bool InterruptCommandPrompt { get; set; } + /// + /// Gets or sets a value indicating whether the text of the command + /// should be written to the host as if it was ran interactively. + /// + public bool WriteInputToHost { get; set; } + + /// + /// Gets or sets a value indicating whether the command to + /// be executed is a console input prompt, such as the + /// PSConsoleHostReadLine function. + /// + internal bool IsReadLine { get; set; } + + /// + /// Gets or sets a value indicating whether the command should + /// be invoked in the original runspace. In the majority of cases + /// this should remain unset. + /// + internal bool ShouldExecuteInOriginalRunspace + { + get + { + return _shouldExecuteInOriginalRunspace ?? IsReadLine; + } + set + { + _shouldExecuteInOriginalRunspace = value; + } + } + #endregion #region Constructors @@ -50,6 +82,7 @@ public ExecutionOptions() { this.WriteOutputToHost = true; this.WriteErrorsToHost = true; + this.WriteInputToHost = false; this.AddToHistory = false; this.InterruptCommandPrompt = false; } diff --git a/src/PowerShellEditorServices/Session/ExecutionTarget.cs b/src/PowerShellEditorServices/Session/ExecutionTarget.cs new file mode 100644 index 000000000..70ec3cb6f --- /dev/null +++ b/src/PowerShellEditorServices/Session/ExecutionTarget.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.PowerShell.EditorServices.Session +{ + /// + /// Represents the different API's available for executing commands. + /// + internal enum ExecutionTarget + { + /// + /// Indicates that the command should be invoked through the PowerShell debugger. + /// + Debugger, + + /// + /// Indicates that the command should be invoked via an instance of the PowerShell class. + /// + PowerShell, + + /// + /// Indicates that the command should be invoked through the PowerShell engine's event manager. + /// + InvocationEvent + } +} diff --git a/src/PowerShellEditorServices/Session/Host/EditorServicesPSHost.cs b/src/PowerShellEditorServices/Session/Host/EditorServicesPSHost.cs index 33925f044..7c32acdb3 100644 --- a/src/PowerShellEditorServices/Session/Host/EditorServicesPSHost.cs +++ b/src/PowerShellEditorServices/Session/Host/EditorServicesPSHost.cs @@ -26,6 +26,7 @@ public class EditorServicesPSHost : PSHost, IHostSupportsInteractiveSession private Guid instanceId = Guid.NewGuid(); private EditorServicesPSHostUserInterface hostUserInterface; private IHostSupportsInteractiveSession hostSupportsInteractiveSession; + private PowerShellContext powerShellContext; #endregion @@ -55,6 +56,7 @@ public EditorServicesPSHost( this.hostDetails = hostDetails; this.hostUserInterface = hostUserInterface; this.hostSupportsInteractiveSession = powerShellContext; + this.powerShellContext = powerShellContext; } #endregion @@ -251,7 +253,7 @@ public override PSHostUserInterface UI /// public override void EnterNestedPrompt() { - Logger.Write(LogLevel.Verbose, "EnterNestedPrompt() called."); + this.powerShellContext.EnterNestedPrompt(); } /// @@ -259,7 +261,7 @@ public override void EnterNestedPrompt() /// public override void ExitNestedPrompt() { - Logger.Write(LogLevel.Verbose, "ExitNestedPrompt() called."); + this.powerShellContext.ExitNestedPrompt(); } /// diff --git a/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs b/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs index 2aceaaf10..a1d17547f 100644 --- a/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs +++ b/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs @@ -112,7 +112,10 @@ public EditorServicesPSHostUserInterface( #region Public Methods - void IHostInput.StartCommandLoop() + /// + /// Starts the host's interactive command loop. + /// + public void StartCommandLoop() { if (!this.IsCommandLoopRunning) { @@ -121,7 +124,10 @@ void IHostInput.StartCommandLoop() } } - void IHostInput.StopCommandLoop() + /// + /// Stops the host's interactive command loop. + /// + public void StopCommandLoop() { if (this.IsCommandLoopRunning) { @@ -140,7 +146,7 @@ private void ShowCommandPrompt() Task.Factory.StartNew( async () => { - await this.StartReplLoop(this.commandLoopCancellationToken.Token); + await this.StartReplLoopAsync(this.commandLoopCancellationToken.Token); }); } else @@ -192,7 +198,7 @@ public void SendControlC() /// A CancellationToken used to cancel the command line request. /// /// A Task that can be awaited for the resulting input string. - protected abstract Task ReadCommandLine(CancellationToken cancellationToken); + protected abstract Task ReadCommandLineAsync(CancellationToken cancellationToken); /// /// Creates an InputPrompt handle to use for displaying input @@ -272,7 +278,7 @@ public override Dictionary Prompt( CancellationTokenSource cancellationToken = new CancellationTokenSource(); Task> promptTask = this.CreateInputPromptHandler() - .PromptForInput( + .PromptForInputAsync( promptCaption, promptMessage, fields, @@ -327,7 +333,7 @@ public override int PromptForChoice( CancellationTokenSource cancellationToken = new CancellationTokenSource(); Task promptTask = this.CreateChoicePromptHandler() - .PromptForChoice( + .PromptForChoiceAsync( promptCaption, promptMessage, choices, @@ -366,7 +372,7 @@ public override PSCredential PromptForCredential( Task> promptTask = this.CreateInputPromptHandler() - .PromptForInput( + .PromptForInputAsync( promptCaption, promptMessage, new FieldDetails[] { new CredentialFieldDetails("Credential", "Credential", userName) }, @@ -440,7 +446,7 @@ public override string ReadLine() Task promptTask = this.CreateInputPromptHandler() - .PromptForInput(cancellationToken.Token); + .PromptForInputAsync(cancellationToken.Token); // Run the prompt task and wait for it to return this.WaitForPromptCompletion( @@ -461,7 +467,7 @@ public override SecureString ReadLineAsSecureString() Task promptTask = this.CreateInputPromptHandler() - .PromptForSecureInput(cancellationToken.Token); + .PromptForSecureInputAsync(cancellationToken.Token); // Run the prompt task and wait for it to return this.WaitForPromptCompletion( @@ -615,7 +621,7 @@ public Collection PromptForChoice( CancellationTokenSource cancellationToken = new CancellationTokenSource(); Task promptTask = this.CreateChoicePromptHandler() - .PromptForChoice( + .PromptForChoiceAsync( promptCaption, promptMessage, choices, @@ -636,12 +642,30 @@ public Collection PromptForChoice( #region Private Methods - private async Task WritePromptStringToHost() + private Coordinates lastPromptLocation; + + private async Task WritePromptStringToHostAsync(CancellationToken cancellationToken) { + try + { + if (this.lastPromptLocation != null && + this.lastPromptLocation.X == await ConsoleProxy.GetCursorLeftAsync(cancellationToken) && + this.lastPromptLocation.Y == await ConsoleProxy.GetCursorTopAsync(cancellationToken)) + { + return; + } + } + // When output is redirected (like when running tests) attempting to get + // the cursor position will throw. + catch (System.IO.IOException) + { + } + PSCommand promptCommand = new PSCommand().AddScript("prompt"); + cancellationToken.ThrowIfCancellationRequested(); string promptString = - (await this.powerShellContext.ExecuteCommand(promptCommand, false, false)) + (await this.powerShellContext.ExecuteCommandAsync(promptCommand, false, false)) .Select(pso => pso.BaseObject) .OfType() .FirstOrDefault() ?? "PS> "; @@ -669,8 +693,13 @@ private async Task WritePromptStringToHost() promptString); } + cancellationToken.ThrowIfCancellationRequested(); + // Write the prompt string this.WriteOutput(promptString, false); + this.lastPromptLocation = new Coordinates( + await ConsoleProxy.GetCursorLeftAsync(cancellationToken), + await ConsoleProxy.GetCursorTopAsync(cancellationToken)); } private void WriteDebuggerBanner(DebuggerStopEventArgs eventArgs) @@ -705,17 +734,26 @@ private void WriteDebuggerBanner(DebuggerStopEventArgs eventArgs) internal ConsoleColor ProgressForegroundColor { get; set; } = ConsoleColor.Yellow; internal ConsoleColor ProgressBackgroundColor { get; set; } = ConsoleColor.DarkCyan; - private async Task StartReplLoop(CancellationToken cancellationToken) + private async Task StartReplLoopAsync(CancellationToken cancellationToken) { - do + while (!cancellationToken.IsCancellationRequested) { string commandString = null; + int originalCursorTop = 0; - await this.WritePromptStringToHost(); + try + { + await this.WritePromptStringToHostAsync(cancellationToken); + } + catch (OperationCanceledException) + { + break; + } try { - commandString = await this.ReadCommandLine(cancellationToken); + originalCursorTop = await ConsoleProxy.GetCursorTopAsync(cancellationToken); + commandString = await this.ReadCommandLineAsync(cancellationToken); } catch (PipelineStoppedException) { @@ -739,29 +777,29 @@ private async Task StartReplLoop(CancellationToken cancellationToken) Logger.WriteException("Caught exception while reading command line", e); } - - if (commandString != null) + finally { - if (!string.IsNullOrWhiteSpace(commandString)) - { - var unusedTask = - this.powerShellContext - .ExecuteScriptString( - commandString, - false, - true, - true) - .ConfigureAwait(false); - - break; - } - else + if (!cancellationToken.IsCancellationRequested && + originalCursorTop == await ConsoleProxy.GetCursorTopAsync(cancellationToken)) { - this.WriteOutput(string.Empty); + this.WriteLine(); } } + + if (!string.IsNullOrWhiteSpace(commandString)) + { + var unusedTask = + this.powerShellContext + .ExecuteScriptStringAsync( + commandString, + writeInputToHost: false, + writeOutputToHost: true, + addToHistory: true) + .ConfigureAwait(continueOnCapturedContext: false); + + break; + } } - while (!cancellationToken.IsCancellationRequested); } private InputPromptHandler CreateInputPromptHandler() @@ -856,6 +894,12 @@ private void WaitForPromptCompletion( private void PowerShellContext_DebuggerStop(object sender, System.Management.Automation.DebuggerStopEventArgs e) { + if (!this.IsCommandLoopRunning) + { + StartCommandLoop(); + return; + } + // Cancel any existing prompt first this.CancelCommandPrompt(); @@ -871,45 +915,42 @@ private void PowerShellContext_DebuggerResumed(object sender, System.Management. private void PowerShellContext_ExecutionStatusChanged(object sender, ExecutionStatusChangedEventArgs eventArgs) { // The command loop should only be manipulated if it's already started - if (this.IsCommandLoopRunning) + if (eventArgs.ExecutionStatus == ExecutionStatus.Aborted) { - if (eventArgs.ExecutionStatus == ExecutionStatus.Aborted) + // When aborted, cancel any lingering prompts + if (this.activePromptHandler != null) { - // When aborted, cancel any lingering prompts - if (this.activePromptHandler != null) - { - this.activePromptHandler.CancelPrompt(); - this.WriteOutput(string.Empty); - } + this.activePromptHandler.CancelPrompt(); + this.WriteOutput(string.Empty); } - else if ( - eventArgs.ExecutionOptions.WriteOutputToHost || - eventArgs.ExecutionOptions.InterruptCommandPrompt) + } + else if ( + eventArgs.ExecutionOptions.WriteOutputToHost || + eventArgs.ExecutionOptions.InterruptCommandPrompt) + { + // Any command which writes output to the host will affect + // the display of the prompt + if (eventArgs.ExecutionStatus != ExecutionStatus.Running) { - // Any command which writes output to the host will affect - // the display of the prompt - if (eventArgs.ExecutionStatus != ExecutionStatus.Running) - { - // Execution has completed, start the input prompt - this.ShowCommandPrompt(); - } - else - { - // A new command was started, cancel the input prompt - this.CancelCommandPrompt(); - this.WriteOutput(string.Empty); - } + // Execution has completed, start the input prompt + this.ShowCommandPrompt(); + StartCommandLoop(); } - else if ( - eventArgs.ExecutionOptions.WriteErrorsToHost && - (eventArgs.ExecutionStatus == ExecutionStatus.Failed || - eventArgs.HadErrors)) + else { + // A new command was started, cancel the input prompt + StopCommandLoop(); this.CancelCommandPrompt(); - this.WriteOutput(string.Empty); - this.ShowCommandPrompt(); } } + else if ( + eventArgs.ExecutionOptions.WriteErrorsToHost && + (eventArgs.ExecutionStatus == ExecutionStatus.Failed || + eventArgs.HadErrors)) + { + this.WriteOutput(string.Empty, true); + var unusedTask = this.WritePromptStringToHostAsync(CancellationToken.None); + } } #endregion diff --git a/src/PowerShellEditorServices/Session/Host/TerminalPSHostUserInterface.cs b/src/PowerShellEditorServices/Session/Host/TerminalPSHostUserInterface.cs index 3752010f6..0d2e839ca 100644 --- a/src/PowerShellEditorServices/Session/Host/TerminalPSHostUserInterface.cs +++ b/src/PowerShellEditorServices/Session/Host/TerminalPSHostUserInterface.cs @@ -69,9 +69,9 @@ public TerminalPSHostUserInterface( /// A CancellationToken used to cancel the command line request. /// /// A Task that can be awaited for the resulting input string. - protected override Task ReadCommandLine(CancellationToken cancellationToken) + protected override Task ReadCommandLineAsync(CancellationToken cancellationToken) { - return this.consoleReadLine.ReadCommandLine(cancellationToken); + return this.consoleReadLine.ReadCommandLineAsync(cancellationToken); } /// diff --git a/src/PowerShellEditorServices/Session/IPromptContext.cs b/src/PowerShellEditorServices/Session/IPromptContext.cs new file mode 100644 index 000000000..157715e7d --- /dev/null +++ b/src/PowerShellEditorServices/Session/IPromptContext.cs @@ -0,0 +1,67 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + /// + /// Provides methods for interacting with implementations of ReadLine. + /// + public interface IPromptContext + { + /// + /// Read a string that has been input by the user. + /// + /// Indicates if ReadLine should act like a command REPL. + /// + /// The cancellation token can be used to cancel reading user input. + /// + /// + /// A task object that represents the completion of reading input. The Result property will + /// return the input string. + /// + Task InvokeReadLineAsync(bool isCommandLine, CancellationToken cancellationToken); + + /// + /// Performs any additional actions required to cancel the current ReadLine invocation. + /// + void AbortReadLine(); + + /// + /// Creates a task that completes when the current ReadLine invocation has been aborted. + /// + /// + /// A task object that represents the abortion of the current ReadLine invocation. + /// + Task AbortReadLineAsync(); + + /// + /// Blocks until the current ReadLine invocation has exited. + /// + void WaitForReadLineExit(); + + /// + /// Creates a task that completes when the current ReadLine invocation has exited. + /// + /// + /// A task object that represents the exit of the current ReadLine invocation. + /// + Task WaitForReadLineExitAsync(); + + /// + /// Adds the specified command to the history managed by the ReadLine implementation. + /// + /// The command to record. + void AddToHistory(string command); + + /// + /// Forces the prompt handler to trigger PowerShell event handling, reliquishing control + /// of the pipeline thread during event processing. + /// + void ForcePSEventHandling(); + } +} diff --git a/src/PowerShellEditorServices/Session/IVersionSpecificOperations.cs b/src/PowerShellEditorServices/Session/IVersionSpecificOperations.cs index d47264478..55540ba9d 100644 --- a/src/PowerShellEditorServices/Session/IVersionSpecificOperations.cs +++ b/src/PowerShellEditorServices/Session/IVersionSpecificOperations.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Management.Automation; +using System.Management.Automation.Host; using System.Management.Automation.Runspaces; namespace Microsoft.PowerShell.EditorServices.Session @@ -21,6 +22,12 @@ IEnumerable ExecuteCommandInDebugger( PSCommand psCommand, bool sendOutputToHost, out DebuggerResumeAction? debuggerResumeAction); + + void StopCommandInDebugger(PowerShellContext powerShellContext); + + bool IsDebuggerStopped(PromptNest promptNest, Runspace runspace); + + void ExitNestedPrompt(PSHost host); } } diff --git a/src/PowerShellEditorServices/Session/InvocationEventQueue.cs b/src/PowerShellEditorServices/Session/InvocationEventQueue.cs new file mode 100644 index 000000000..b6e9a9c0b --- /dev/null +++ b/src/PowerShellEditorServices/Session/InvocationEventQueue.cs @@ -0,0 +1,263 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Management.Automation.Runspaces; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Threading; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + using System.Management.Automation; + + /// + /// Provides the ability to take over the current pipeline in a runspace. + /// + internal class InvocationEventQueue + { + private const string ShouldProcessInExecutionThreadPropertyName = "ShouldProcessInExecutionThread"; + + private static readonly PropertyInfo s_shouldProcessInExecutionThreadProperty = + typeof(PSEventSubscriber) + .GetProperty( + ShouldProcessInExecutionThreadPropertyName, + BindingFlags.Instance | BindingFlags.NonPublic); + + private readonly PromptNest _promptNest; + + private readonly Runspace _runspace; + + private readonly PowerShellContext _powerShellContext; + + private InvocationRequest _invocationRequest; + + private SemaphoreSlim _lock = AsyncUtils.CreateSimpleLockingSemaphore(); + + private InvocationEventQueue(PowerShellContext powerShellContext, PromptNest promptNest) + { + _promptNest = promptNest; + _powerShellContext = powerShellContext; + _runspace = powerShellContext.CurrentRunspace.Runspace; + } + + internal static InvocationEventQueue Create(PowerShellContext powerShellContext, PromptNest promptNest) + { + var eventQueue = new InvocationEventQueue(powerShellContext, promptNest); + eventQueue.CreateInvocationSubscriber(); + return eventQueue; + } + + /// + /// Executes a command on the main pipeline thread through + /// eventing. A event subscriber will + /// be created that creates a nested PowerShell instance for + /// to utilize. + /// + /// + /// Avoid using this method directly if possible. + /// will route commands + /// through this method if required. + /// + /// The expected result type. + /// The to be executed. + /// + /// Error messages from PowerShell will be written to the . + /// + /// Specifies options to be used when executing this command. + /// + /// An awaitable which will provide results once the command + /// execution completes. + /// + internal async Task> ExecuteCommandOnIdleAsync( + PSCommand psCommand, + StringBuilder errorMessages, + ExecutionOptions executionOptions) + { + var request = new PipelineExecutionRequest( + _powerShellContext, + psCommand, + errorMessages, + executionOptions); + + await SetInvocationRequestAsync( + new InvocationRequest( + pwsh => request.ExecuteAsync().GetAwaiter().GetResult())); + + try + { + return await request.Results; + } + finally + { + await SetInvocationRequestAsync(request: null); + } + } + + /// + /// Marshals a to run on the pipeline thread. A new + /// will be created for the invocation. + /// + /// + /// The to invoke on the pipeline thread. The nested + /// instance for the created + /// will be passed as an argument. + /// + /// + /// An awaitable that the caller can use to know when execution completes. + /// + internal async Task InvokeOnPipelineThreadAsync(Action invocationAction) + { + var request = new InvocationRequest(pwsh => + { + using (_promptNest.GetRunspaceHandle(CancellationToken.None, isReadLine: false)) + { + pwsh.Runspace = _runspace; + invocationAction(pwsh); + } + }); + + await SetInvocationRequestAsync(request); + try + { + await request.Task; + } + finally + { + await SetInvocationRequestAsync(null); + } + } + + private async Task WaitForExistingRequestAsync() + { + InvocationRequest existingRequest; + await _lock.WaitAsync(); + try + { + existingRequest = _invocationRequest; + if (existingRequest == null || existingRequest.Task.IsCompleted) + { + return; + } + } + finally + { + _lock.Release(); + } + + await existingRequest.Task; + } + + private async Task SetInvocationRequestAsync(InvocationRequest request) + { + await WaitForExistingRequestAsync(); + await _lock.WaitAsync(); + try + { + _invocationRequest = request; + } + finally + { + _lock.Release(); + } + + _powerShellContext.ForcePSEventHandling(); + } + + private void OnPowerShellIdle(object sender, EventArgs e) + { + if (!_lock.Wait(0)) + { + return; + } + + InvocationRequest currentRequest = null; + try + { + if (_invocationRequest == null) + { + return; + } + + currentRequest = _invocationRequest; + } + finally + { + _lock.Release(); + } + + _promptNest.PushPromptContext(); + try + { + currentRequest.Invoke(_promptNest.GetPowerShell()); + } + finally + { + _promptNest.PopPromptContext(); + } + } + + private PSEventSubscriber CreateInvocationSubscriber() + { + PSEventSubscriber subscriber = _runspace.Events.SubscribeEvent( + source: null, + eventName: PSEngineEvent.OnIdle, + sourceIdentifier: PSEngineEvent.OnIdle, + data: null, + handlerDelegate: OnPowerShellIdle, + supportEvent: true, + forwardEvent: false); + + SetSubscriberExecutionThreadWithReflection(subscriber); + + subscriber.Unsubscribed += OnInvokerUnsubscribed; + + return subscriber; + } + + private void OnInvokerUnsubscribed(object sender, PSEventUnsubscribedEventArgs e) + { + CreateInvocationSubscriber(); + } + + private void SetSubscriberExecutionThreadWithReflection(PSEventSubscriber subscriber) + { + // We need to create the PowerShell object in the same thread so we can get a nested + // PowerShell. This is the only way to consistently take control of the pipeline. The + // alternative is to make the subscriber a script block and have that create and process + // the PowerShell object, but that puts us in a different SessionState and is a lot slower. + s_shouldProcessInExecutionThreadProperty.SetValue(subscriber, true); + } + + private class InvocationRequest : TaskCompletionSource + { + private readonly Action _invocationAction; + + internal InvocationRequest(Action invocationAction) + { + _invocationAction = invocationAction; + } + + internal void Invoke(PowerShell pwsh) + { + try + { + _invocationAction(pwsh); + + // Ensure the result is set in another thread otherwise the caller + // may take over the pipeline thread. + System.Threading.Tasks.Task.Run(() => SetResult(true)); + } + catch (Exception e) + { + System.Threading.Tasks.Task.Run(() => SetException(e)); + } + } + } + } +} diff --git a/src/PowerShellEditorServices/Session/LegacyReadLineContext.cs b/src/PowerShellEditorServices/Session/LegacyReadLineContext.cs new file mode 100644 index 000000000..ad68d0512 --- /dev/null +++ b/src/PowerShellEditorServices/Session/LegacyReadLineContext.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Console; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + internal class LegacyReadLineContext : IPromptContext + { + private readonly ConsoleReadLine _legacyReadLine; + + internal LegacyReadLineContext(PowerShellContext powerShellContext) + { + _legacyReadLine = new ConsoleReadLine(powerShellContext); + } + + public Task AbortReadLineAsync() + { + return Task.FromResult(true); + } + + public async Task InvokeReadLineAsync(bool isCommandLine, CancellationToken cancellationToken) + { + return await _legacyReadLine.InvokeLegacyReadLineAsync(isCommandLine, cancellationToken); + } + + public Task WaitForReadLineExitAsync() + { + return Task.FromResult(true); + } + + public void AddToHistory(string command) + { + // Do nothing, history is managed completely by the PowerShell engine in legacy ReadLine. + } + + public void AbortReadLine() + { + // Do nothing, no additional actions are needed to cancel ReadLine. + } + + public void WaitForReadLineExit() + { + // Do nothing, ReadLine cancellation is instant or not appliciable. + } + + public void ForcePSEventHandling() + { + // Do nothing, the pipeline thread is not occupied by legacy ReadLine. + } + } +} diff --git a/src/PowerShellEditorServices/Session/PSReadLinePromptContext.cs b/src/PowerShellEditorServices/Session/PSReadLinePromptContext.cs new file mode 100644 index 000000000..0b59e230b --- /dev/null +++ b/src/PowerShellEditorServices/Session/PSReadLinePromptContext.cs @@ -0,0 +1,203 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using System; +using System.Management.Automation.Runspaces; +using Microsoft.PowerShell.EditorServices.Console; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Session { + using System.Management.Automation; + + internal class PSReadLinePromptContext : IPromptContext { + private const string ReadLineScript = @" + [System.Diagnostics.DebuggerHidden()] + [System.Diagnostics.DebuggerStepThrough()] + param() + return [Microsoft.PowerShell.PSConsoleReadLine, Microsoft.PowerShell.PSReadLine2, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null]::ReadLine( + $Host.Runspace, + $ExecutionContext, + $args[0])"; + + private const string ReadLineInitScript = @" + [System.Diagnostics.DebuggerHidden()] + [System.Diagnostics.DebuggerStepThrough()] + param() + end { + $module = Get-Module -ListAvailable PSReadLine | + Where-Object Version -eq '2.0.0' | + Where-Object { $_.PrivateData.PSData.Prerelease -notin 'beta1','beta2' } | + Sort-Object -Descending Version | + Select-Object -First 1 + if (-not $module) { + return + } + + Import-Module -ModuleInfo $module + return [Microsoft.PowerShell.PSConsoleReadLine, Microsoft.PowerShell.PSReadLine2, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null] + }"; + + private readonly PowerShellContext _powerShellContext; + + private PromptNest _promptNest; + + private InvocationEventQueue _invocationEventQueue; + + private ConsoleReadLine _consoleReadLine; + + private CancellationTokenSource _readLineCancellationSource; + + private PSReadLineProxy _readLineProxy; + + internal PSReadLinePromptContext( + PowerShellContext powerShellContext, + PromptNest promptNest, + InvocationEventQueue invocationEventQueue, + PSReadLineProxy readLineProxy) + { + _promptNest = promptNest; + _powerShellContext = powerShellContext; + _invocationEventQueue = invocationEventQueue; + _consoleReadLine = new ConsoleReadLine(powerShellContext); + _readLineProxy = readLineProxy; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return; + } + + _readLineProxy.OverrideReadKey( + intercept => ConsoleProxy.UnixReadKey( + intercept, + _readLineCancellationSource.Token)); + } + + internal static bool TryGetPSReadLineProxy( + ILogger logger, + Runspace runspace, + out PSReadLineProxy readLineProxy) + { + readLineProxy = null; + using (var pwsh = PowerShell.Create()) + { + pwsh.Runspace = runspace; + var psReadLineType = pwsh + .AddScript(ReadLineInitScript) + .Invoke() + .FirstOrDefault(); + + if (psReadLineType == null) + { + return false; + } + + try + { + readLineProxy = new PSReadLineProxy(psReadLineType, logger); + } + catch (InvalidOperationException) + { + // The Type we got back from PowerShell doesn't have the members we expected. + // Could be an older version, a custom build, or something a newer version with + // breaking changes. + return false; + } + } + + return true; + } + + public async Task InvokeReadLineAsync(bool isCommandLine, CancellationToken cancellationToken) + { + _readLineCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var localTokenSource = _readLineCancellationSource; + if (localTokenSource.Token.IsCancellationRequested) + { + throw new TaskCanceledException(); + } + + try + { + if (!isCommandLine) + { + return await _consoleReadLine.InvokeLegacyReadLineAsync( + false, + _readLineCancellationSource.Token); + } + + var result = (await _powerShellContext.ExecuteCommandAsync( + new PSCommand() + .AddScript(ReadLineScript) + .AddArgument(_readLineCancellationSource.Token), + null, + new ExecutionOptions() + { + WriteErrorsToHost = false, + WriteOutputToHost = false, + InterruptCommandPrompt = false, + AddToHistory = false, + IsReadLine = isCommandLine + })) + .FirstOrDefault(); + + return cancellationToken.IsCancellationRequested + ? string.Empty + : result; + } + finally + { + _readLineCancellationSource = null; + } + } + + public void AbortReadLine() + { + if (_readLineCancellationSource == null) + { + return; + } + + _readLineCancellationSource.Cancel(); + + WaitForReadLineExit(); + } + + public async Task AbortReadLineAsync() { + if (_readLineCancellationSource == null) + { + return; + } + + _readLineCancellationSource.Cancel(); + + await WaitForReadLineExitAsync(); + } + + public void WaitForReadLineExit() + { + using (_promptNest.GetRunspaceHandle(CancellationToken.None, isReadLine: true)) + { } + } + + public async Task WaitForReadLineExitAsync() { + using (await _promptNest.GetRunspaceHandleAsync(CancellationToken.None, isReadLine: true)) + { } + } + + public void AddToHistory(string command) + { + _readLineProxy.AddToHistory(command); + } + + public void ForcePSEventHandling() + { + _readLineProxy.ForcePSEventHandling(); + } + } +} diff --git a/src/PowerShellEditorServices/Session/PSReadLineProxy.cs b/src/PowerShellEditorServices/Session/PSReadLineProxy.cs new file mode 100644 index 000000000..50aaf4af3 --- /dev/null +++ b/src/PowerShellEditorServices/Session/PSReadLineProxy.cs @@ -0,0 +1,119 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Reflection; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + internal class PSReadLineProxy + { + private const string FieldMemberType = "field"; + + private const string MethodMemberType = "method"; + + private const string AddToHistoryMethodName = "AddToHistory"; + + private const string SetKeyHandlerMethodName = "SetKeyHandler"; + + private const string ReadKeyOverrideFieldName = "_readKeyOverride"; + + private const string VirtualTerminalTypeName = "Microsoft.PowerShell.Internal.VirtualTerminal"; + + private const string ForcePSEventHandlingMethodName = "ForcePSEventHandling"; + + private static readonly Type[] s_setKeyHandlerTypes = + { + typeof(string[]), + typeof(Action), + typeof(string), + typeof(string) + }; + + private static readonly Type[] s_addToHistoryTypes = { typeof(string) }; + + private readonly FieldInfo _readKeyOverrideField; + + internal PSReadLineProxy(Type psConsoleReadLine, ILogger logger) + { + ForcePSEventHandling = + (Action)psConsoleReadLine.GetMethod( + ForcePSEventHandlingMethodName, + BindingFlags.Static | BindingFlags.NonPublic) + ?.CreateDelegate(typeof(Action)); + + AddToHistory = (Action)psConsoleReadLine.GetMethod( + AddToHistoryMethodName, + s_addToHistoryTypes) + ?.CreateDelegate(typeof(Action)); + + SetKeyHandler = + (Action, string, string>)psConsoleReadLine.GetMethod( + SetKeyHandlerMethodName, + s_setKeyHandlerTypes) + ?.CreateDelegate(typeof(Action, string, string>)); + + _readKeyOverrideField = psConsoleReadLine.GetTypeInfo().Assembly + .GetType(VirtualTerminalTypeName) + ?.GetField(ReadKeyOverrideFieldName, BindingFlags.Static | BindingFlags.NonPublic); + + if (_readKeyOverrideField == null) + { + throw NewInvalidPSReadLineVersionException( + FieldMemberType, + ReadKeyOverrideFieldName, + logger); + } + + if (SetKeyHandler == null) + { + throw NewInvalidPSReadLineVersionException( + MethodMemberType, + SetKeyHandlerMethodName, + logger); + } + + if (AddToHistory == null) + { + throw NewInvalidPSReadLineVersionException( + MethodMemberType, + AddToHistoryMethodName, + logger); + } + + if (ForcePSEventHandling == null) + { + throw NewInvalidPSReadLineVersionException( + MethodMemberType, + ForcePSEventHandlingMethodName, + logger); + } + } + + internal Action AddToHistory { get; } + + internal Action, object>, string, string> SetKeyHandler { get; } + + internal Action ForcePSEventHandling { get; } + + internal void OverrideReadKey(Func readKeyFunc) + { + _readKeyOverrideField.SetValue(null, readKeyFunc); + } + + private static InvalidOperationException NewInvalidPSReadLineVersionException( + string memberType, + string memberName, + ILogger logger) + { + logger.Write( + LogLevel.Error, + $"The loaded version of PSReadLine is not supported. The {memberType} \"{memberName}\" was not found."); + + return new InvalidOperationException(); + } + } +} diff --git a/src/PowerShellEditorServices/Session/PipelineExecutionRequest.cs b/src/PowerShellEditorServices/Session/PipelineExecutionRequest.cs new file mode 100644 index 000000000..1c69f6a15 --- /dev/null +++ b/src/PowerShellEditorServices/Session/PipelineExecutionRequest.cs @@ -0,0 +1,80 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.Management.Automation; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + internal interface IPipelineExecutionRequest + { + Task ExecuteAsync(); + + Task WaitTask { get; } + } + + /// + /// Contains details relating to a request to execute a + /// command on the PowerShell pipeline thread. + /// + /// The expected result type of the execution. + internal class PipelineExecutionRequest : IPipelineExecutionRequest + { + private PowerShellContext _powerShellContext; + private PSCommand _psCommand; + private StringBuilder _errorMessages; + private ExecutionOptions _executionOptions; + private TaskCompletionSource> _resultsTask; + + public Task> Results + { + get { return this._resultsTask.Task; } + } + + public Task WaitTask { get { return Results; } } + + public PipelineExecutionRequest( + PowerShellContext powerShellContext, + PSCommand psCommand, + StringBuilder errorMessages, + bool sendOutputToHost) + : this( + powerShellContext, + psCommand, + errorMessages, + new ExecutionOptions() + { + WriteOutputToHost = sendOutputToHost + }) + { } + + + public PipelineExecutionRequest( + PowerShellContext powerShellContext, + PSCommand psCommand, + StringBuilder errorMessages, + ExecutionOptions executionOptions) + { + _powerShellContext = powerShellContext; + _psCommand = psCommand; + _errorMessages = errorMessages; + _executionOptions = executionOptions; + _resultsTask = new TaskCompletionSource>(); + } + + public async Task ExecuteAsync() + { + var results = + await _powerShellContext.ExecuteCommandAsync( + _psCommand, + _errorMessages, + _executionOptions); + + var unusedTask = Task.Run(() => _resultsTask.SetResult(results)); + } + } +} diff --git a/src/PowerShellEditorServices/Session/PowerShell3Operations.cs b/src/PowerShellEditorServices/Session/PowerShell3Operations.cs deleted file mode 100644 index 2199e1839..000000000 --- a/src/PowerShellEditorServices/Session/PowerShell3Operations.cs +++ /dev/null @@ -1,74 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Management.Automation; -using System.Management.Automation.Runspaces; - -namespace Microsoft.PowerShell.EditorServices.Session -{ - internal class PowerShell3Operations : IVersionSpecificOperations - { - public void ConfigureDebugger(Runspace runspace) - { - // The debugger has no SetDebugMode in PowerShell v3. - } - - public void PauseDebugger(Runspace runspace) - { - // The debugger cannot be paused in PowerShell v3. - throw new NotSupportedException("Debugger cannot be paused in PowerShell v3"); - } - - public IEnumerable ExecuteCommandInDebugger( - PowerShellContext powerShellContext, - Runspace currentRunspace, - PSCommand psCommand, - bool sendOutputToHost, - out DebuggerResumeAction? debuggerResumeAction) - { - IEnumerable executionResult = null; - - using (var nestedPipeline = currentRunspace.CreateNestedPipeline()) - { - foreach (var command in psCommand.Commands) - { - nestedPipeline.Commands.Add(command); - } - - var results = nestedPipeline.Invoke(); - - if (typeof(TResult) != typeof(PSObject)) - { - executionResult = - results - .Select(pso => pso.BaseObject) - .Cast(); - } - else - { - executionResult = results.Cast(); - } - } - - // Write the output to the host if necessary - if (sendOutputToHost) - { - foreach (var line in executionResult) - { - powerShellContext.WriteOutput(line.ToString(), true); - } - } - - // No DebuggerResumeAction result for PowerShell v3 - debuggerResumeAction = null; - - return executionResult; - } - } -} - diff --git a/src/PowerShellEditorServices/Session/PowerShell4Operations.cs b/src/PowerShellEditorServices/Session/PowerShell4Operations.cs deleted file mode 100644 index ea4070225..000000000 --- a/src/PowerShellEditorServices/Session/PowerShell4Operations.cs +++ /dev/null @@ -1,84 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Management.Automation; -using System.Management.Automation.Runspaces; - -namespace Microsoft.PowerShell.EditorServices.Session -{ - internal class PowerShell4Operations : IVersionSpecificOperations - { - public void ConfigureDebugger(Runspace runspace) - { -#if !PowerShellv3 - if (runspace.Debugger != null) - { - runspace.Debugger.SetDebugMode(DebugModes.LocalScript | DebugModes.RemoteScript); - } -#endif - } - - public virtual void PauseDebugger(Runspace runspace) - { - // The debugger cannot be paused in PowerShell v4. - throw new NotSupportedException("Debugger cannot be paused in PowerShell v4"); - } - - public IEnumerable ExecuteCommandInDebugger( - PowerShellContext powerShellContext, - Runspace currentRunspace, - PSCommand psCommand, - bool sendOutputToHost, - out DebuggerResumeAction? debuggerResumeAction) - { - debuggerResumeAction = null; - PSDataCollection outputCollection = new PSDataCollection(); - -#if !PowerShellv3 - if (sendOutputToHost) - { - outputCollection.DataAdded += - (obj, e) => - { - for (int i = e.Index; i < outputCollection.Count; i++) - { - powerShellContext.WriteOutput( - outputCollection[i].ToString(), - true); - } - }; - } - - DebuggerCommandResults commandResults = - currentRunspace.Debugger.ProcessCommand( - psCommand, - outputCollection); - - // Pass along the debugger's resume action if the user's - // command caused one to be returned - debuggerResumeAction = commandResults.ResumeAction; -#endif - - IEnumerable results = null; - if (typeof(TResult) != typeof(PSObject)) - { - results = - outputCollection - .Select(pso => pso.BaseObject) - .Cast(); - } - else - { - results = outputCollection.Cast(); - } - - return results; - } - } -} - diff --git a/src/PowerShellEditorServices/Session/PowerShell5Operations.cs b/src/PowerShellEditorServices/Session/PowerShell5Operations.cs index 54f434cb8..e91c0edc8 100644 --- a/src/PowerShellEditorServices/Session/PowerShell5Operations.cs +++ b/src/PowerShellEditorServices/Session/PowerShell5Operations.cs @@ -3,20 +3,100 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation; +using System.Management.Automation.Host; using System.Management.Automation.Runspaces; namespace Microsoft.PowerShell.EditorServices.Session { - internal class PowerShell5Operations : PowerShell4Operations + internal class PowerShell5Operations : IVersionSpecificOperations { - public override void PauseDebugger(Runspace runspace) + public void ConfigureDebugger(Runspace runspace) + { + if (runspace.Debugger != null) + { + runspace.Debugger.SetDebugMode(DebugModes.LocalScript | DebugModes.RemoteScript); + } + } + + public virtual void PauseDebugger(Runspace runspace) { -#if !PowerShellv3 && !PowerShellv4 if (runspace.Debugger != null) { runspace.Debugger.SetDebuggerStepMode(true); } -#endif + } + + public virtual bool IsDebuggerStopped(PromptNest promptNest, Runspace runspace) + { + return runspace.Debugger.InBreakpoint || (promptNest.IsRemote && promptNest.IsInDebugger); + } + + public IEnumerable ExecuteCommandInDebugger( + PowerShellContext powerShellContext, + Runspace currentRunspace, + PSCommand psCommand, + bool sendOutputToHost, + out DebuggerResumeAction? debuggerResumeAction) + { + debuggerResumeAction = null; + PSDataCollection outputCollection = new PSDataCollection(); + + if (sendOutputToHost) + { + outputCollection.DataAdded += + (obj, e) => + { + for (int i = e.Index; i < outputCollection.Count; i++) + { + powerShellContext.WriteOutput( + outputCollection[i].ToString(), + true); + } + }; + } + + DebuggerCommandResults commandResults = + currentRunspace.Debugger.ProcessCommand( + psCommand, + outputCollection); + + // Pass along the debugger's resume action if the user's + // command caused one to be returned + debuggerResumeAction = commandResults.ResumeAction; + + IEnumerable results = null; + if (typeof(TResult) != typeof(PSObject)) + { + results = + outputCollection + .Select(pso => pso.BaseObject) + .Cast(); + } + else + { + results = outputCollection.Cast(); + } + + return results; + } + + public void StopCommandInDebugger(PowerShellContext powerShellContext) + { + powerShellContext.CurrentRunspace.Runspace.Debugger.StopProcessCommand(); + } + + public void ExitNestedPrompt(PSHost host) + { + try + { + host.ExitNestedPrompt(); + } + catch (FlowControlException) + { + } } } } diff --git a/src/PowerShellEditorServices/Session/PowerShellContext.cs b/src/PowerShellEditorServices/Session/PowerShellContext.cs index cf1206b0f..f5eb09494 100644 --- a/src/PowerShellEditorServices/Session/PowerShellContext.cs +++ b/src/PowerShellEditorServices/Session/PowerShellContext.cs @@ -3,27 +3,29 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using Microsoft.PowerShell.EditorServices.Console; -using Microsoft.PowerShell.EditorServices.Utility; using System; -using System.Globalization; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Globalization; +using System.IO; +using System.Runtime.InteropServices; using System.Linq; +using System.Management.Automation.Host; +using System.Management.Automation.Remoting; +using System.Management.Automation.Runspaces; +using System.Reflection; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Session; +using Microsoft.PowerShell.EditorServices.Session.Capabilities; +using Microsoft.PowerShell.EditorServices.Utility; namespace Microsoft.PowerShell.EditorServices { - using Session; - using System.Management.Automation; - using System.Management.Automation.Host; - using System.Management.Automation.Runspaces; - using Microsoft.PowerShell.EditorServices.Session.Capabilities; - using System.IO; using System.ComponentModel; + using System.Management.Automation; /// /// Manages the lifetime and usage of a PowerShell session. @@ -32,8 +34,26 @@ namespace Microsoft.PowerShell.EditorServices /// public class PowerShellContext : IDisposable, IHostSupportsInteractiveSession { + private const string DotNetFrameworkDescription = ".NET Framework"; + + private static readonly Action s_runspaceApartmentStateSetter; + + static PowerShellContext() + { + // PowerShell ApartmentState APIs aren't available in PSStandard, so we need to use reflection + if (RuntimeInformation.FrameworkDescription.Equals(DotNetFrameworkDescription)) + { + MethodInfo setterInfo = typeof(Runspace).GetProperty("ApartmentState").GetSetMethod(); + Delegate setter = Delegate.CreateDelegate(typeof(Action), firstArgument: null, method: setterInfo); + s_runspaceApartmentStateSetter = (Action)setter; + } + } + #region Fields + private readonly SemaphoreSlim resumeRequestHandle = AsyncUtils.CreateSimpleLockingSemaphore(); + + private bool isPSReadLineEnabled; private ILogger logger; private PowerShell powerShell; private bool ownsInitialRunspace; @@ -44,32 +64,32 @@ public class PowerShellContext : IDisposable, IHostSupportsInteractiveSession private IVersionSpecificOperations versionSpecificOperations; - private int pipelineThreadId; - private TaskCompletionSource debuggerStoppedTask; - private TaskCompletionSource pipelineExecutionTask; - - private object runspaceMutex = new object(); - private AsyncQueue runspaceWaitQueue = new AsyncQueue(); - private Stack runspaceStack = new Stack(); + private int isCommandLoopRestarterSet; + #endregion #region Properties + private IPromptContext PromptContext { get; set; } + + private PromptNest PromptNest { get; set; } + + private InvocationEventQueue InvocationEventQueue { get; set; } + + private EngineIntrinsics EngineIntrinsics { get; set; } + + private PSHost ExternalHost { get; set; } + /// /// Gets a boolean that indicates whether the debugger is currently stopped, /// either at a breakpoint or because the user broke execution. /// - public bool IsDebuggerStopped - { - get - { - return - this.debuggerStoppedTask != null && - this.CurrentRunspace.Runspace.RunspaceAvailability != RunspaceAvailability.Available; - } - } + public bool IsDebuggerStopped => + this.versionSpecificOperations.IsDebuggerStopped( + PromptNest, + CurrentRunspace.Runspace); /// /// Gets the current state of the session. @@ -95,6 +115,8 @@ public PowerShellVersionDetails LocalPowerShellVersion /// private IHostOutput ConsoleWriter { get; set; } + private IHostInput ConsoleReader { get; set; } + /// /// Gets details pertaining to the current runspace. /// @@ -104,6 +126,12 @@ public RunspaceDetails CurrentRunspace private set; } + /// + /// Gets a value indicating whether the current runspace + /// is ready for a command + /// + public bool IsAvailable => this.SessionState == PowerShellContextState.Ready; + /// /// Gets the working directory path the PowerShell context was inititially set when the debugger launches. /// This path is used to determine whether a script in the call stack is an "external" script. @@ -118,9 +146,13 @@ public RunspaceDetails CurrentRunspace /// /// /// An ILogger implementation used for writing log messages. - public PowerShellContext(ILogger logger) + /// + /// Indicates whether PSReadLine should be used if possible + /// + public PowerShellContext(ILogger logger, bool isPSReadLineEnabled) { this.logger = logger; + this.isPSReadLineEnabled = isPSReadLineEnabled; } /// @@ -141,6 +173,7 @@ public static Runspace CreateRunspace( { var psHost = new EditorServicesPSHost(powerShellContext, hostDetails, hostUserInterface, logger); powerShellContext.ConsoleWriter = hostUserInterface; + powerShellContext.ConsoleReader = hostUserInterface; return CreateRunspace(psHost); } @@ -151,12 +184,22 @@ public static Runspace CreateRunspace( /// public static Runspace CreateRunspace(PSHost psHost) { - var initialSessionState = InitialSessionState.CreateDefault2(); + InitialSessionState initialSessionState; + if (Environment.GetEnvironmentVariable("PSES_TEST_USE_CREATE_DEFAULT") == "1") { + initialSessionState = InitialSessionState.CreateDefault(); + } else { + initialSessionState = InitialSessionState.CreateDefault2(); + } Runspace runspace = RunspaceFactory.CreateRunspace(psHost, initialSessionState); -#if !CoreCLR - runspace.ApartmentState = ApartmentState.STA; -#endif + + // Windows PowerShell must be hosted in STA mode + // This must be set on the runspace *before* it is opened + if (RuntimeInformation.FrameworkDescription.Equals(DotNetFrameworkDescription)) + { + s_runspaceApartmentStateSetter(runspace, ApartmentState.STA); + } + runspace.ThreadOptions = PSThreadOptions.ReuseThread; runspace.Open(); @@ -197,6 +240,7 @@ public void Initialize( this.ownsInitialRunspace = ownsInitialRunspace; this.SessionState = PowerShellContextState.NotStarted; this.ConsoleWriter = consoleHost; + this.ConsoleReader = consoleHost as IHostInput; // Get the PowerShell runtime version this.LocalPowerShellVersion = @@ -230,14 +274,6 @@ public void Initialize( { this.versionSpecificOperations = new PowerShell5Operations(); } - else if (powerShellVersion.Major == 4) - { - this.versionSpecificOperations = new PowerShell4Operations(); - } - else if (powerShellVersion.Major == 3) - { - this.versionSpecificOperations = new PowerShell3Operations(); - } else { throw new NotSupportedException( @@ -265,13 +301,48 @@ public void Initialize( } // Now that initialization is complete we can watch for InvocationStateChanged - this.powerShell.InvocationStateChanged += powerShell_InvocationStateChanged; - this.SessionState = PowerShellContextState.Ready; + // EngineIntrinsics is used in some instances to interact with the initial + // runspace without having to wait for PSReadLine to check for events. + this.EngineIntrinsics = + initialRunspace + .SessionStateProxy + .PSVariable + .GetValue("ExecutionContext") + as EngineIntrinsics; + + // The external host is used to properly exit from a nested prompt that + // was entered by the user. + this.ExternalHost = + initialRunspace + .SessionStateProxy + .PSVariable + .GetValue("Host") + as PSHost; + // Now that the runspace is ready, enqueue it for first use - RunspaceHandle runspaceHandle = new RunspaceHandle(this); - this.runspaceWaitQueue.EnqueueAsync(runspaceHandle).Wait(); + this.PromptNest = new PromptNest( + this, + this.powerShell, + this.ConsoleReader, + this.versionSpecificOperations); + this.InvocationEventQueue = InvocationEventQueue.Create(this, this.PromptNest); + + if (powerShellVersion.Major >= 5 && + this.isPSReadLineEnabled && + PSReadLinePromptContext.TryGetPSReadLineProxy(logger, initialRunspace, out PSReadLineProxy proxy)) + { + this.PromptContext = new PSReadLinePromptContext( + this, + this.PromptNest, + this.InvocationEventQueue, + proxy); + } + else + { + this.PromptContext = new LegacyReadLineContext(this); + } } /// @@ -280,7 +351,7 @@ public void Initialize( /// /// /// - public Task ImportCommandsModule(string moduleBasePath) + public Task ImportCommandsModuleAsync(string moduleBasePath) { PSCommand importCommand = new PSCommand(); importCommand @@ -290,7 +361,7 @@ public Task ImportCommandsModule(string moduleBasePath) moduleBasePath, "PowerShellEditorServices.Commands.psd1")); - return this.ExecuteCommand(importCommand, false, false); + return this.ExecuteCommandAsync(importCommand, false, false); } private static bool CheckIfRunspaceNeedsEventHandlers(RunspaceDetails runspaceDetails) @@ -338,9 +409,9 @@ private void CleanupRunspace(RunspaceDetails runspaceDetails) /// so that commands can be executed against it directly. /// /// A RunspaceHandle instance that gives access to the session's runspace. - public Task GetRunspaceHandle() + public Task GetRunspaceHandleAsync() { - return this.GetRunspaceHandle(CancellationToken.None); + return this.GetRunspaceHandleImplAsync(CancellationToken.None, isReadLine: false); } /// @@ -350,9 +421,9 @@ public Task GetRunspaceHandle() /// /// A CancellationToken that can be used to cancel the request. /// A RunspaceHandle instance that gives access to the session's runspace. - public Task GetRunspaceHandle(CancellationToken cancellationToken) + public Task GetRunspaceHandleAsync(CancellationToken cancellationToken) { - return this.runspaceWaitQueue.DequeueAsync(cancellationToken); + return this.GetRunspaceHandleImplAsync(cancellationToken, isReadLine: false); } /// @@ -371,12 +442,12 @@ public Task GetRunspaceHandle(CancellationToken cancellationToke /// An awaitable Task which will provide results once the command /// execution completes. /// - public async Task> ExecuteCommand( + public async Task> ExecuteCommandAsync( PSCommand psCommand, bool sendOutputToHost = false, bool sendErrorToHost = true) { - return await ExecuteCommand(psCommand, null, sendOutputToHost, sendErrorToHost); + return await ExecuteCommandAsync(psCommand, null, sendOutputToHost, sendErrorToHost); } /// @@ -399,7 +470,7 @@ public async Task> ExecuteCommand( /// An awaitable Task which will provide results once the command /// execution completes. /// - public Task> ExecuteCommand( + public Task> ExecuteCommandAsync( PSCommand psCommand, StringBuilder errorMessages, bool sendOutputToHost = false, @@ -407,7 +478,7 @@ public Task> ExecuteCommand( bool addToHistory = false) { return - this.ExecuteCommand( + this.ExecuteCommandAsync( psCommand, errorMessages, new ExecutionOptions @@ -430,33 +501,61 @@ public Task> ExecuteCommand( /// An awaitable Task which will provide results once the command /// execution completes. /// - public async Task> ExecuteCommand( + public async Task> ExecuteCommandAsync( PSCommand psCommand, StringBuilder errorMessages, ExecutionOptions executionOptions) { + // Add history to PSReadLine before cancelling, otherwise it will be restored as the + // cancelled prompt when it's called again. + if (executionOptions.AddToHistory) + { + this.PromptContext.AddToHistory(psCommand.Commands[0].CommandText); + } + bool hadErrors = false; RunspaceHandle runspaceHandle = null; + ExecutionTarget executionTarget = ExecutionTarget.PowerShell; IEnumerable executionResult = Enumerable.Empty(); + var shouldCancelReadLine = + executionOptions.InterruptCommandPrompt || + executionOptions.WriteOutputToHost; // If the debugger is active and the caller isn't on the pipeline // thread, send the command over to that thread to be executed. - if (Thread.CurrentThread.ManagedThreadId != this.pipelineThreadId && - this.pipelineExecutionTask != null) + // Determine if execution should take place in a different thread + // using the following criteria: + // 1. The current frame in the prompt nest has a thread controller + // (meaning it is a nested prompt or is in the debugger) + // 2. We aren't already on the thread in question + // 3. The command is not a candidate for background invocation + // via PowerShell eventing + // 4. The command cannot be for a PSReadLine pipeline while we + // are currently in a out of process runspace + var threadController = PromptNest.GetThreadController(); + if (!(threadController == null || + !threadController.IsPipelineThread || + threadController.IsCurrentThread() || + this.ShouldExecuteWithEventing(executionOptions) || + (PromptNest.IsRemote && executionOptions.IsReadLine))) { this.logger.Write(LogLevel.Verbose, "Passing command execution to pipeline thread."); - PipelineExecutionRequest executionRequest = + if (shouldCancelReadLine && PromptNest.IsReadLineBusy()) + { + // If a ReadLine pipeline is running in the debugger then we'll hang here + // if we don't cancel it. Typically we can rely on OnExecutionStatusChanged but + // the pipeline request won't even start without clearing the current task. + this.ConsoleReader?.StopCommandLoop(); + } + + // Send the pipeline execution request to the pipeline thread + return await threadController.RequestPipelineExecutionAsync( new PipelineExecutionRequest( this, psCommand, errorMessages, - executionOptions.WriteOutputToHost); - - // Send the pipeline execution request to the pipeline thread - this.pipelineExecutionTask.SetResult(executionRequest); - - return await executionRequest.Results; + executionOptions)); } else { @@ -474,73 +573,127 @@ public async Task> ExecuteCommand( endOfStatement: false)); } - this.OnExecutionStatusChanged( - ExecutionStatus.Running, - executionOptions, - false); + executionTarget = GetExecutionTarget(executionOptions); - if (this.CurrentRunspace.Runspace.RunspaceAvailability == RunspaceAvailability.AvailableForNestedCommand || - this.debuggerStoppedTask != null) + // If a ReadLine pipeline is running we can still execute commands that + // don't write output (e.g. command completion) + if (executionTarget == ExecutionTarget.InvocationEvent) { - executionResult = - this.ExecuteCommandInDebugger( + return (await this.InvocationEventQueue.ExecuteCommandOnIdleAsync( + psCommand, + errorMessages, + executionOptions)); + } + + // Prompt is stopped and started based on the execution status, so naturally + // we don't want PSReadLine pipelines to factor in. + if (!executionOptions.IsReadLine) + { + this.OnExecutionStatusChanged( + ExecutionStatus.Running, + executionOptions, + false); + } + + runspaceHandle = await this.GetRunspaceHandleAsync(executionOptions.IsReadLine); + if (executionOptions.WriteInputToHost) + { + this.WriteOutput(psCommand.Commands[0].CommandText, true); + } + + if (executionTarget == ExecutionTarget.Debugger) + { + // Manually change the session state for debugger commands because + // we don't have an invocation state event to attach to. + if (!executionOptions.IsReadLine) + { + this.OnSessionStateChanged( + this, + new SessionStateChangedEventArgs( + PowerShellContextState.Running, + PowerShellExecutionResult.NotFinished, + null)); + } + try + { + return this.ExecuteCommandInDebugger( psCommand, executionOptions.WriteOutputToHost); + } + catch (Exception e) + { + logger.Write( + LogLevel.Error, + "Exception occurred while executing debugger command:\r\n\r\n" + e.ToString()); + } + finally + { + if (!executionOptions.IsReadLine) + { + this.OnSessionStateChanged( + this, + new SessionStateChangedEventArgs( + PowerShellContextState.Ready, + PowerShellExecutionResult.Stopped, + null)); + } + } } - else + + var invocationSettings = new PSInvocationSettings() + { + AddToHistory = executionOptions.AddToHistory + }; + + this.logger.Write( + LogLevel.Verbose, + string.Format( + "Attempting to execute command(s):\r\n\r\n{0}", + GetStringForPSCommand(psCommand))); + + + PowerShell shell = this.PromptNest.GetPowerShell(executionOptions.IsReadLine); + shell.Commands = psCommand; + + // Don't change our SessionState for ReadLine. + if (!executionOptions.IsReadLine) + { + shell.InvocationStateChanged += powerShell_InvocationStateChanged; + } + + shell.Runspace = executionOptions.ShouldExecuteInOriginalRunspace + ? this.initialRunspace.Runspace + : this.CurrentRunspace.Runspace; + try { - this.logger.Write( - LogLevel.Verbose, - string.Format( - "Attempting to execute command(s):\r\n\r\n{0}", - GetStringForPSCommand(psCommand))); - - // Set the runspace - runspaceHandle = await this.GetRunspaceHandle(); - if (runspaceHandle.Runspace.RunspaceAvailability != RunspaceAvailability.AvailableForNestedCommand) + // Nested PowerShell instances can't be invoked asynchronously. This occurs + // in nested prompts and pipeline requests from eventing. + if (shell.IsNested) { - this.powerShell.Runspace = runspaceHandle.Runspace; + return shell.Invoke(null, invocationSettings); } - // Invoke the pipeline on a background thread - // TODO: Use built-in async invocation! - executionResult = - await Task.Factory.StartNew>( - () => - { - Collection result = null; - try - { - this.powerShell.Commands = psCommand; - - PSInvocationSettings invocationSettings = new PSInvocationSettings(); - invocationSettings.AddToHistory = executionOptions.AddToHistory; - result = this.powerShell.Invoke(null, invocationSettings); - } - catch (RemoteException e) - { - if (!e.SerializedRemoteException.TypeNames[0].EndsWith("PipelineStoppedException")) - { - // Rethrow anything that isn't a PipelineStoppedException - throw e; - } - } - - return result; - }, - CancellationToken.None, // Might need a cancellation token - TaskCreationOptions.None, - TaskScheduler.Default - ); - - if (this.powerShell.HadErrors) + return await Task.Factory.StartNew>( + () => shell.Invoke(null, invocationSettings), + CancellationToken.None, // Might need a cancellation token + TaskCreationOptions.None, + TaskScheduler.Default); + } + finally + { + if (!executionOptions.IsReadLine) + { + shell.InvocationStateChanged -= powerShell_InvocationStateChanged; + } + + if (shell.HadErrors) { var strBld = new StringBuilder(1024); strBld.AppendFormat("Execution of the following command(s) completed with errors:\r\n\r\n{0}\r\n", GetStringForPSCommand(psCommand)); int i = 1; - foreach (var error in this.powerShell.Streams.Error) + foreach (var error in shell.Streams.Error) { if (i > 1) strBld.Append("\r\n\r\n"); strBld.Append($"Error #{i++}:\r\n"); @@ -557,7 +710,7 @@ await Task.Factory.StartNew>( } // We've reported these errors, clear them so they don't keep showing up. - this.powerShell.Streams.Error.Clear(); + shell.Streams.Error.Clear(); var errorMessage = strBld.ToString(); @@ -574,6 +727,14 @@ await Task.Factory.StartNew>( } } } + catch (PSRemotingDataStructureException e) + { + this.logger.Write( + LogLevel.Error, + "Pipeline stopped while executing command:\r\n\r\n" + e.ToString()); + + errorMessages?.Append(e.Message); + } catch (PipelineStoppedException e) { this.logger.Write( @@ -614,22 +775,22 @@ await Task.Factory.StartNew>( SessionDetails sessionDetails = null; // Get the SessionDetails and then write the prompt - if (this.CurrentRunspace.Runspace.RunspaceAvailability == RunspaceAvailability.Available) + if (executionTarget == ExecutionTarget.Debugger) + { + sessionDetails = this.GetSessionDetailsInDebugger(); + } + else if (this.CurrentRunspace.Runspace.RunspaceAvailability == RunspaceAvailability.Available) { // This state can happen if the user types a command that causes the // debugger to exit before we reach this point. No RunspaceHandle // will exist already so we need to create one and then use it if (runspaceHandle == null) { - runspaceHandle = await this.GetRunspaceHandle(); + runspaceHandle = await this.GetRunspaceHandleAsync(); } sessionDetails = this.GetSessionDetailsInRunspace(runspaceHandle.Runspace); } - else if (this.IsDebuggerStopped) - { - sessionDetails = this.GetSessionDetailsInDebugger(); - } else { sessionDetails = this.GetSessionDetailsInNestedPipeline(); @@ -644,14 +805,14 @@ await Task.Factory.StartNew>( { runspaceHandle.Dispose(); } + + this.OnExecutionStatusChanged( + ExecutionStatus.Completed, + executionOptions, + hadErrors); } } - this.OnExecutionStatusChanged( - ExecutionStatus.Completed, - executionOptions, - hadErrors); - return executionResult; } @@ -664,9 +825,9 @@ await Task.Factory.StartNew>( /// An awaitable Task that the caller can use to know when /// execution completes. /// - public Task ExecuteCommand(PSCommand psCommand) + public Task ExecuteCommandAsync(PSCommand psCommand) { - return this.ExecuteCommand(psCommand); + return this.ExecuteCommandAsync(psCommand); } /// @@ -674,10 +835,10 @@ public Task ExecuteCommand(PSCommand psCommand) /// /// The script string to execute. /// A Task that can be awaited for the script completion. - public Task> ExecuteScriptString( + public Task> ExecuteScriptStringAsync( string scriptString) { - return this.ExecuteScriptString(scriptString, false, true); + return this.ExecuteScriptStringAsync(scriptString, false, true); } /// @@ -686,11 +847,11 @@ public Task> ExecuteScriptString( /// The script string to execute. /// Error messages from PowerShell will be written to the StringBuilder. /// A Task that can be awaited for the script completion. - public Task> ExecuteScriptString( + public Task> ExecuteScriptStringAsync( string scriptString, StringBuilder errorMessages) { - return this.ExecuteScriptString(scriptString, errorMessages, false, true, false); + return this.ExecuteScriptStringAsync(scriptString, errorMessages, false, true, false); } /// @@ -700,12 +861,12 @@ public Task> ExecuteScriptString( /// If true, causes the script string to be written to the host. /// If true, causes the script output to be written to the host. /// A Task that can be awaited for the script completion. - public Task> ExecuteScriptString( + public Task> ExecuteScriptStringAsync( string scriptString, bool writeInputToHost, bool writeOutputToHost) { - return this.ExecuteScriptString(scriptString, null, writeInputToHost, writeOutputToHost, false); + return this.ExecuteScriptStringAsync(scriptString, null, writeInputToHost, writeOutputToHost, false); } /// @@ -716,13 +877,13 @@ public Task> ExecuteScriptString( /// If true, causes the script output to be written to the host. /// If true, adds the command to the user's command history. /// A Task that can be awaited for the script completion. - public Task> ExecuteScriptString( + public Task> ExecuteScriptStringAsync( string scriptString, bool writeInputToHost, bool writeOutputToHost, bool addToHistory) { - return this.ExecuteScriptString(scriptString, null, writeInputToHost, writeOutputToHost, addToHistory); + return this.ExecuteScriptStringAsync(scriptString, null, writeInputToHost, writeOutputToHost, addToHistory); } /// @@ -734,30 +895,22 @@ public Task> ExecuteScriptString( /// If true, causes the script output to be written to the host. /// If true, adds the command to the user's command history. /// A Task that can be awaited for the script completion. - public async Task> ExecuteScriptString( + public async Task> ExecuteScriptStringAsync( string scriptString, StringBuilder errorMessages, bool writeInputToHost, bool writeOutputToHost, bool addToHistory) { - // Get rid of leading and trailing whitespace and newlines - scriptString = scriptString.Trim(); - - if (writeInputToHost) - { - this.WriteOutput(scriptString, false); - } - - PSCommand psCommand = new PSCommand(); - psCommand.AddScript(scriptString); - - return - await this.ExecuteCommand( - psCommand, - errorMessages, - writeOutputToHost, - addToHistory: addToHistory); + return await this.ExecuteCommandAsync( + new PSCommand().AddScript(scriptString.Trim()), + errorMessages, + new ExecutionOptions() + { + WriteOutputToHost = writeOutputToHost, + AddToHistory = addToHistory, + WriteInputToHost = writeInputToHost + }); } /// @@ -767,7 +920,7 @@ await this.ExecuteCommand( /// Arguments to pass to the script. /// Writes the executed script path and arguments to the host. /// A Task that can be awaited for completion. - public async Task ExecuteScriptWithArgs(string script, string arguments = null, bool writeInputToHost = false) + public async Task ExecuteScriptWithArgsAsync(string script, string arguments = null, bool writeInputToHost = false) { PSCommand command = new PSCommand(); @@ -778,8 +931,15 @@ public async Task ExecuteScriptWithArgs(string script, string arguments = null, try { // Assume we can only debug scripts from the FileSystem provider - string workingDir = - this.CurrentRunspace.Runspace.SessionStateProxy.Path.CurrentFileSystemLocation.ProviderPath; + string workingDir = (await ExecuteCommandAsync( + new PSCommand() + .AddCommand("Microsoft.PowerShell.Management\\Get-Location") + .AddParameter("PSProvider", "FileSystem"), + false, + false)) + .FirstOrDefault() + .ProviderPath; + workingDir = workingDir.TrimEnd(Path.DirectorySeparatorChar); scriptAbsPath = workingDir + Path.DirectorySeparatorChar + script; } @@ -792,10 +952,10 @@ public async Task ExecuteScriptWithArgs(string script, string arguments = null, var strBld = new StringBuilder(); - // The script parameter can refer to either a "script path" or a "command name". If it is a + // The script parameter can refer to either a "script path" or a "command name". If it is a // script path, we can determine that by seeing if the path exists. If so, we always single // quote that path in case it includes special PowerShell characters like ', &, (, ), [, ] and - // . Any embedded single quotes are escaped. + // . Any embedded single quotes are escaped. // If the provided path is already quoted, then File.Exists will not find it. // This keeps us from quoting an already quoted path. // Related to issue #123. @@ -831,53 +991,71 @@ public async Task ExecuteScriptWithArgs(string script, string arguments = null, true); } - await this.ExecuteCommand( + await this.ExecuteCommandAsync( command, null, sendOutputToHost: true, addToHistory: true); } - internal static TResult ExecuteScriptAndGetItem(string scriptToExecute, Runspace runspace, TResult defaultValue = default(TResult)) + /// + /// Forces the to trigger PowerShell event handling, + /// reliquishing control of the pipeline thread during event processing. + /// + /// + /// This method is called automatically by and + /// . Consider using them instead of this method directly when + /// possible. + /// + internal void ForcePSEventHandling() { - Pipeline pipeline = null; + PromptContext.ForcePSEventHandling(); + } - try + /// + /// Marshals a to run on the pipeline thread. A new + /// will be created for the invocation. + /// + /// + /// The to invoke on the pipeline thread. The nested + /// instance for the created + /// will be passed as an argument. + /// + /// + /// An awaitable that the caller can use to know when execution completes. + /// + /// + /// This method is called automatically by . Consider using + /// that method instead of calling this directly when possible. + /// + internal async Task InvokeOnPipelineThreadAsync(Action invocationAction) + { + if (this.PromptNest.IsReadLineBusy()) { - if (runspace.RunspaceAvailability == RunspaceAvailability.AvailableForNestedCommand) - { - pipeline = runspace.CreateNestedPipeline(scriptToExecute, false); - } - else - { - pipeline = runspace.CreatePipeline(scriptToExecute, false); - } + await this.InvocationEventQueue.InvokeOnPipelineThreadAsync(invocationAction); + return; + } - Collection results = pipeline.Invoke(); + // If this is invoked when ReadLine isn't busy then there shouldn't be any running + // pipelines. Right now this method is only used by command completion which doesn't + // actually require running on the pipeline thread, as long as nothing else is running. + invocationAction.Invoke(this.PromptNest.GetPowerShell()); + } - if (results.Count == 0 || results.FirstOrDefault() == null) - { - return defaultValue; - } + internal async Task InvokeReadLineAsync(bool isCommandLine, CancellationToken cancellationToken) + { + return await PromptContext.InvokeReadLineAsync( + isCommandLine, + cancellationToken); + } - if (typeof(TResult) != typeof(PSObject)) - { - return results - .Select(pso => pso.BaseObject) - .OfType() - .FirstOrDefault(); - } - else - { - return - results - .OfType() - .FirstOrDefault(); - } - } - finally + internal static TResult ExecuteScriptAndGetItem(string scriptToExecute, Runspace runspace, TResult defaultValue = default(TResult)) + { + using (PowerShell pwsh = PowerShell.Create()) { - pipeline.Dispose(); + pwsh.Runspace = runspace; + IEnumerable results = pwsh.AddScript(scriptToExecute).Invoke(); + return results.DefaultIfEmpty(defaultValue).First(); } } @@ -887,7 +1065,7 @@ await this.ExecuteCommand( /// loaded. /// /// A Task that can be awaited for completion. - public async Task LoadHostProfiles() + public async Task LoadHostProfilesAsync() { if (this.profilePaths != null) { @@ -897,36 +1075,60 @@ public async Task LoadHostProfiles() { command = new PSCommand(); command.AddCommand(profilePath, false); - await this.ExecuteCommand(command, true, true); + await this.ExecuteCommandAsync(command, true, true); } // Gather the session details (particularly the prompt) after // loading the user's profiles. - await this.GetSessionDetailsInRunspace(); + await this.GetSessionDetailsInRunspaceAsync(); } } /// - /// Causes the current execution to be aborted no matter what state + /// Causes the most recent execution to be aborted no matter what state /// it is currently in. /// public void AbortExecution() + { + this.AbortExecution(shouldAbortDebugSession: false); + } + + /// + /// Causes the most recent execution to be aborted no matter what state + /// it is currently in. + /// + /// + /// A value indicating whether a debug session should be aborted if one + /// is currently active. + /// + public void AbortExecution(bool shouldAbortDebugSession) { if (this.SessionState != PowerShellContextState.Aborting && this.SessionState != PowerShellContextState.Disposed) { this.logger.Write(LogLevel.Verbose, "Execution abort requested..."); - // Clean up the debugger - if (this.IsDebuggerStopped) + if (shouldAbortDebugSession) { - this.ResumeDebugger(DebuggerResumeAction.Stop); - this.debuggerStoppedTask = null; - this.pipelineExecutionTask = null; + this.ExitAllNestedPrompts(); } - // Stop the running pipeline - this.powerShell.BeginStop(null, null); + if (this.PromptNest.IsInDebugger) + { + if (shouldAbortDebugSession) + { + this.versionSpecificOperations.StopCommandInDebugger(this); + this.ResumeDebugger(DebuggerResumeAction.Stop); + } + else + { + this.versionSpecificOperations.StopCommandInDebugger(this); + } + } + else + { + this.PromptNest.GetPowerShell(isReadLine: false).BeginStop(null, null); + } this.SessionState = PowerShellContextState.Aborting; @@ -944,6 +1146,33 @@ public void AbortExecution() } } + /// + /// Exit all consecutive nested prompts that the user has entered. + /// + internal void ExitAllNestedPrompts() + { + while (this.PromptNest.IsNestedPrompt) + { + this.PromptNest.WaitForCurrentFrameExit(frame => this.ExitNestedPrompt()); + this.versionSpecificOperations.ExitNestedPrompt(ExternalHost); + } + } + + /// + /// Exit all consecutive nested prompts that the user has entered. + /// + /// + /// A task object that represents all nested prompts being exited + /// + internal async Task ExitAllNestedPromptsAsync() + { + while (this.PromptNest.IsNestedPrompt) + { + await this.PromptNest.WaitForCurrentFrameExitAsync(frame => this.ExitNestedPrompt()); + this.versionSpecificOperations.ExitNestedPrompt(ExternalHost); + } + } + /// /// Causes the debugger to break execution wherever it currently is. /// This method is internal because the real Break API is provided @@ -960,22 +1189,56 @@ internal void BreakExecution() internal void ResumeDebugger(DebuggerResumeAction resumeAction) { - if (this.debuggerStoppedTask != null) + ResumeDebugger(resumeAction, shouldWaitForExit: true); + } + + private void ResumeDebugger(DebuggerResumeAction resumeAction, bool shouldWaitForExit) + { + resumeRequestHandle.Wait(); + try { - // Set the result so that the execution thread resumes. - // The execution thread will clean up the task. - if (!this.debuggerStoppedTask.TrySetResult(resumeAction)) + if (this.PromptNest.IsNestedPrompt) + { + this.ExitAllNestedPrompts(); + } + + if (this.PromptNest.IsInDebugger) + { + // Set the result so that the execution thread resumes. + // The execution thread will clean up the task. + if (shouldWaitForExit) + { + this.PromptNest.WaitForCurrentFrameExit( + frame => + { + frame.ThreadController.StartThreadExit(resumeAction); + this.ConsoleReader?.StopCommandLoop(); + if (this.SessionState != PowerShellContextState.Ready) + { + this.versionSpecificOperations.StopCommandInDebugger(this); + } + }); + } + else + { + this.PromptNest.GetThreadController().StartThreadExit(resumeAction); + this.ConsoleReader?.StopCommandLoop(); + if (this.SessionState != PowerShellContextState.Ready) + { + this.versionSpecificOperations.StopCommandInDebugger(this); + } + } + } + else { this.logger.Write( LogLevel.Error, - $"Tried to resume debugger with action {resumeAction} but the task was already completed."); + $"Tried to resume debugger with action {resumeAction} but there was no debuggerStoppedTask."); } } - else + finally { - this.logger.Write( - LogLevel.Error, - $"Tried to resume debugger with action {resumeAction} but there was no debuggerStoppedTask."); + resumeRequestHandle.Release(); } } @@ -985,22 +1248,9 @@ internal void ResumeDebugger(DebuggerResumeAction resumeAction) /// public void Dispose() { - // Do we need to abort a running execution? - if (this.SessionState == PowerShellContextState.Running || - this.IsDebuggerStopped) - { - this.AbortExecution(); - } - + this.PromptNest.Dispose(); this.SessionState = PowerShellContextState.Disposed; - if (this.powerShell != null) - { - this.powerShell.InvocationStateChanged -= this.powerShell_InvocationStateChanged; - this.powerShell.Dispose(); - this.powerShell = null; - } - // Clean up the active runspace this.CleanupRunspace(this.CurrentRunspace); @@ -1029,6 +1279,57 @@ public void Dispose() this.initialRunspace = null; } + private async Task GetRunspaceHandleAsync(bool isReadLine) + { + return await this.GetRunspaceHandleImplAsync(CancellationToken.None, isReadLine); + } + + private async Task GetRunspaceHandleImplAsync(CancellationToken cancellationToken, bool isReadLine) + { + return await this.PromptNest.GetRunspaceHandleAsync(cancellationToken, isReadLine); + } + + private ExecutionTarget GetExecutionTarget(ExecutionOptions options = null) + { + if (options == null) + { + options = new ExecutionOptions(); + } + + var noBackgroundInvocation = + options.InterruptCommandPrompt || + options.WriteOutputToHost || + options.IsReadLine || + PromptNest.IsRemote; + + // Take over the pipeline if PSReadLine is running, we aren't trying to run PSReadLine, and + // we aren't in a remote session. + if (!noBackgroundInvocation && PromptNest.IsReadLineBusy() && PromptNest.IsMainThreadBusy()) + { + return ExecutionTarget.InvocationEvent; + } + + // We can't take the pipeline from PSReadLine if it's in a remote session, so we need to + // invoke locally in that case. + if (IsDebuggerStopped && PromptNest.IsInDebugger && !(options.IsReadLine && PromptNest.IsRemote)) + { + return ExecutionTarget.Debugger; + } + + return ExecutionTarget.PowerShell; + } + + private bool ShouldExecuteWithEventing(ExecutionOptions executionOptions) + { + return + this.PromptNest.IsReadLineBusy() && + this.PromptNest.IsMainThreadBusy() && + !(executionOptions.IsReadLine || + executionOptions.InterruptCommandPrompt || + executionOptions.WriteOutputToHost || + IsCurrentRunspaceOutOfProcess()); + } + private void CloseRunspace(RunspaceDetails runspaceDetails) { string exitCommand = null; @@ -1093,18 +1394,99 @@ internal void ReleaseRunspaceHandle(RunspaceHandle runspaceHandle) { Validate.IsNotNull("runspaceHandle", runspaceHandle); - if (this.runspaceWaitQueue.IsEmpty) + if (PromptNest.IsMainThreadBusy() || (runspaceHandle.IsReadLine && PromptNest.IsReadLineBusy())) { - var newRunspaceHandle = new RunspaceHandle(this); - this.runspaceWaitQueue.EnqueueAsync(newRunspaceHandle).Wait(); + var unusedTask = PromptNest + .ReleaseRunspaceHandleAsync(runspaceHandle) + .ConfigureAwait(false); } else { // Write the situation to the log since this shouldn't happen this.logger.Write( LogLevel.Error, - "The PowerShellContext.runspaceWaitQueue has more than one item"); + "ReleaseRunspaceHandle was called when the main thread was not busy."); + } + } + + /// + /// Determines if the current runspace is out of process. + /// + /// + /// A value indicating whether the current runspace is out of process. + /// + internal bool IsCurrentRunspaceOutOfProcess() + { + return + CurrentRunspace.Context == RunspaceContext.EnteredProcess || + CurrentRunspace.Context == RunspaceContext.DebuggedRunspace || + CurrentRunspace.Location == RunspaceLocation.Remote; + } + + /// + /// Called by the external PSHost when $Host.EnterNestedPrompt is called. + /// + internal void EnterNestedPrompt() + { + if (this.IsCurrentRunspaceOutOfProcess()) + { + throw new NotSupportedException(); + } + + this.PromptNest.PushPromptContext(PromptNestFrameType.NestedPrompt); + var localThreadController = this.PromptNest.GetThreadController(); + this.OnSessionStateChanged( + this, + new SessionStateChangedEventArgs( + PowerShellContextState.Ready, + PowerShellExecutionResult.Stopped, + null)); + + // Reset command loop mainly for PSReadLine + this.ConsoleReader?.StopCommandLoop(); + this.ConsoleReader?.StartCommandLoop(); + + var localPipelineExecutionTask = localThreadController.TakeExecutionRequestAsync(); + var localDebuggerStoppedTask = localThreadController.Exit(); + + // Wait for off-thread pipeline requests and/or ExitNestedPrompt + while (true) + { + int taskIndex = Task.WaitAny( + localPipelineExecutionTask, + localDebuggerStoppedTask); + + if (taskIndex == 0) + { + var localExecutionTask = localPipelineExecutionTask.GetAwaiter().GetResult(); + localPipelineExecutionTask = localThreadController.TakeExecutionRequestAsync(); + localExecutionTask.ExecuteAsync().GetAwaiter().GetResult(); + continue; + } + + this.ConsoleReader?.StopCommandLoop(); + this.PromptNest.PopPromptContext(); + break; + } + } + + /// + /// Called by the external PSHost when $Host.ExitNestedPrompt is called. + /// + internal void ExitNestedPrompt() + { + if (this.PromptNest.NestedPromptLevel == 1 || !this.PromptNest.IsNestedPrompt) + { + this.logger.Write( + LogLevel.Error, + "ExitNestedPrompt was called outside of a nested prompt."); + return; } + + // Stop the command input loop so PSReadLine isn't invoked between ExitNestedPrompt + // being invoked and EnterNestedPrompt getting the message to exit. + this.ConsoleReader?.StopCommandLoop(); + this.PromptNest.GetThreadController().StartThreadExit(DebuggerResumeAction.Stop); } /// @@ -1112,9 +1494,9 @@ internal void ReleaseRunspaceHandle(RunspaceHandle runspaceHandle) /// unescaped before calling this method. /// /// - public async Task SetWorkingDirectory(string path) + public async Task SetWorkingDirectoryAsync(string path) { - await this.SetWorkingDirectory(path, true); + await this.SetWorkingDirectoryAsync(path, true); } /// @@ -1122,19 +1504,21 @@ public async Task SetWorkingDirectory(string path) /// /// /// Specify false to have the path escaped, otherwise specify true if the path has already been escaped. - public async Task SetWorkingDirectory(string path, bool isPathAlreadyEscaped) + public async Task SetWorkingDirectoryAsync(string path, bool isPathAlreadyEscaped) { this.InitialWorkingDirectory = path; - using (RunspaceHandle runspaceHandle = await this.GetRunspaceHandle()) + if (!isPathAlreadyEscaped) { - if (!isPathAlreadyEscaped) - { - path = WildcardEscapePath(path); - } - - runspaceHandle.Runspace.SessionStateProxy.Path.SetLocation(path); + path = WildcardEscapePath(path); } + + await ExecuteCommandAsync( + new PSCommand().AddCommand("Set-Location").AddParameter("Path", path), + null, + sendOutputToHost: false, + sendErrorToHost: false, + addToHistory: false); } /// @@ -1367,7 +1751,9 @@ private IEnumerable ExecuteCommandInDebugger(PSCommand psComma if (debuggerResumeAction.HasValue) { // Resume the debugger with the specificed action - this.ResumeDebugger(debuggerResumeAction.Value); + this.ResumeDebugger( + debuggerResumeAction.Value, + shouldWaitForExit: false); } return output; @@ -1530,11 +1916,11 @@ private Command GetOutputCommand(bool endOfStatement) { Command outputCommand = new Command( - command: this.IsDebuggerStopped ? "Out-String" : "Out-Default", + command: this.PromptNest.IsInDebugger ? "Out-String" : "Out-Default", isScript: false, useLocalScope: true); - if (this.IsDebuggerStopped) + if (this.PromptNest.IsInDebugger) { // Out-String needs the -Stream parameter added outputCommand.Parameters.Add("Stream"); @@ -1641,15 +2027,21 @@ private SessionDetails GetSessionDetails(Func invokeAction) LogLevel.Verbose, "Runtime exception occurred while gathering runspace info:\r\n\r\n" + e.ToString()); } + catch (ArgumentNullException) + { + this.logger.Write( + LogLevel.Error, + "Could not retrieve session details but no exception was thrown."); + } // TODO: Return a harmless object if necessary this.mostRecentSessionDetails = null; return this.mostRecentSessionDetails; } - private async Task GetSessionDetailsInRunspace() + private async Task GetSessionDetailsInRunspaceAsync() { - using (RunspaceHandle runspaceHandle = await this.GetRunspaceHandle()) + using (RunspaceHandle runspaceHandle = await this.GetRunspaceHandleAsync()) { return this.GetSessionDetailsInRunspace(runspaceHandle.Runspace); } @@ -1692,20 +2084,18 @@ private SessionDetails GetSessionDetailsInDebugger() private SessionDetails GetSessionDetailsInNestedPipeline() { - using (var pipeline = this.CurrentRunspace.Runspace.CreateNestedPipeline()) - { - return this.GetSessionDetails( - command => + // We don't need to check what thread we're on here. If it's a local + // nested pipeline then we will already be on the correct thread, and + // non-debugger nested pipelines aren't supported in remote runspaces. + return this.GetSessionDetails( + command => + { + using (var localPwsh = PowerShell.Create(RunspaceMode.CurrentRunspace)) { - pipeline.Commands.Clear(); - pipeline.Commands.Add(command.Commands[0]); - - return - pipeline - .Invoke() - .FirstOrDefault(); - }); - } + localPwsh.Commands = command; + return localPwsh.Invoke().FirstOrDefault(); + } + }); } private void SetProfileVariableInCurrentRunspace(ProfilePaths profilePaths) @@ -1781,21 +2171,46 @@ private void HandleRunspaceStateChanged(object sender, RunspaceStateEventArgs ar /// public event EventHandler DebuggerResumed; + private void StartCommandLoopOnRunspaceAvailable() + { + if (Interlocked.CompareExchange(ref this.isCommandLoopRestarterSet, 1, 1) == 1) + { + return; + } + + EventHandler handler = null; + handler = (runspace, eventArgs) => + { + if (eventArgs.RunspaceAvailability != RunspaceAvailability.Available || + this.versionSpecificOperations.IsDebuggerStopped(this.PromptNest, (Runspace)runspace)) + { + return; + } + + ((Runspace)runspace).AvailabilityChanged -= handler; + Interlocked.Exchange(ref this.isCommandLoopRestarterSet, 0); + this.ConsoleReader?.StartCommandLoop(); + }; + + this.CurrentRunspace.Runspace.AvailabilityChanged += handler; + Interlocked.Exchange(ref this.isCommandLoopRestarterSet, 1); + } + private void OnDebuggerStop(object sender, DebuggerStopEventArgs e) { - this.logger.Write(LogLevel.Verbose, "Debugger stopped execution."); + if (CurrentRunspace.Context == RunspaceContext.Original) + { + StartCommandLoopOnRunspaceAvailable(); + } - // Set the task so a result can be set - this.debuggerStoppedTask = - new TaskCompletionSource(); + this.logger.Write(LogLevel.Verbose, "Debugger stopped execution."); - // Save the pipeline thread ID and create the pipeline execution task - this.pipelineThreadId = Thread.CurrentThread.ManagedThreadId; - this.pipelineExecutionTask = new TaskCompletionSource(); + PromptNest.PushPromptContext( + IsCurrentRunspaceOutOfProcess() + ? PromptNestFrameType.Debug | PromptNestFrameType.Remote + : PromptNestFrameType.Debug); - // Hold on to local task vars so that the fields can be cleared independently - Task localDebuggerStoppedTask = this.debuggerStoppedTask.Task; - Task localPipelineExecutionTask = this.pipelineExecutionTask.Task; + ThreadController localThreadController = PromptNest.GetThreadController(); // Update the session state this.OnSessionStateChanged( @@ -1805,18 +2220,35 @@ private void OnDebuggerStop(object sender, DebuggerStopEventArgs e) PowerShellExecutionResult.Stopped, null)); - // Get the session details and push the current - // runspace if the session has changed - var sessionDetails = this.GetSessionDetailsInDebugger(); + // Get the session details and push the current + // runspace if the session has changed + SessionDetails sessionDetails = null; + try + { + sessionDetails = this.GetSessionDetailsInDebugger(); + } + catch (InvalidOperationException) + { + this.logger.Write( + LogLevel.Verbose, + "Attempting to get session details failed, most likely due to a running pipeline that is attempting to stop."); + } - // Push the current runspace if the session has changed - this.UpdateRunspaceDetailsIfSessionChanged(sessionDetails, isDebuggerStop: true); + if (!localThreadController.FrameExitTask.Task.IsCompleted) + { + // Push the current runspace if the session has changed + this.UpdateRunspaceDetailsIfSessionChanged(sessionDetails, isDebuggerStop: true); - // Raise the event for the debugger service - this.DebuggerStop?.Invoke(sender, e); + // Raise the event for the debugger service + this.DebuggerStop?.Invoke(sender, e); + } this.logger.Write(LogLevel.Verbose, "Starting pipeline thread message loop..."); + Task localPipelineExecutionTask = + localThreadController.TakeExecutionRequestAsync(); + Task localDebuggerStoppedTask = + localThreadController.Exit(); while (true) { int taskIndex = @@ -1829,7 +2261,7 @@ private void OnDebuggerStop(object sender, DebuggerStopEventArgs e) // Write a new output line before continuing this.WriteOutput("", true); - e.ResumeAction = localDebuggerStoppedTask.Result; + e.ResumeAction = localDebuggerStoppedTask.GetAwaiter().GetResult(); this.logger.Write(LogLevel.Verbose, "Received debugger resume action " + e.ResumeAction.ToString()); // Notify listeners that the debugger has resumed @@ -1860,15 +2292,14 @@ private void OnDebuggerStop(object sender, DebuggerStopEventArgs e) this.logger.Write(LogLevel.Verbose, "Received pipeline thread execution request."); IPipelineExecutionRequest executionRequest = localPipelineExecutionTask.Result; - - this.pipelineExecutionTask = new TaskCompletionSource(); - localPipelineExecutionTask = this.pipelineExecutionTask.Task; - - executionRequest.Execute().Wait(); + localPipelineExecutionTask = localThreadController.TakeExecutionRequestAsync(); + executionRequest.ExecuteAsync().GetAwaiter().GetResult(); this.logger.Write(LogLevel.Verbose, "Pipeline thread execution completed."); - if (this.CurrentRunspace.Runspace.RunspaceAvailability == RunspaceAvailability.Available) + if (!this.versionSpecificOperations.IsDebuggerStopped( + this.PromptNest, + this.CurrentRunspace.Runspace)) { if (this.CurrentRunspace.Context == RunspaceContext.DebuggedRunspace) { @@ -1890,9 +2321,7 @@ private void OnDebuggerStop(object sender, DebuggerStopEventArgs e) } } - // Clear the task so that it won't be used again - this.debuggerStoppedTask = null; - this.pipelineExecutionTask = null; + PromptNest.PopPromptContext(); } // NOTE: This event is 'internal' because the DebugService provides @@ -1908,56 +2337,6 @@ private void OnBreakpointUpdated(object sender, BreakpointUpdatedEventArgs e) #region Nested Classes - private interface IPipelineExecutionRequest - { - Task Execute(); - } - - /// - /// Contains details relating to a request to execute a - /// command on the PowerShell pipeline thread. - /// - /// The expected result type of the execution. - private class PipelineExecutionRequest : IPipelineExecutionRequest - { - PowerShellContext powerShellContext; - PSCommand psCommand; - StringBuilder errorMessages; - bool sendOutputToHost; - TaskCompletionSource> resultsTask; - - public Task> Results - { - get { return this.resultsTask.Task; } - } - - public PipelineExecutionRequest( - PowerShellContext powerShellContext, - PSCommand psCommand, - StringBuilder errorMessages, - bool sendOutputToHost) - { - this.powerShellContext = powerShellContext; - this.psCommand = psCommand; - this.errorMessages = errorMessages; - this.sendOutputToHost = sendOutputToHost; - this.resultsTask = new TaskCompletionSource>(); - } - - public async Task Execute() - { - var results = - await this.powerShellContext.ExecuteCommand( - psCommand, - errorMessages, - sendOutputToHost); - - this.resultsTask.SetResult(results); - - // TODO: Deal with errors? - } - } - private void ConfigureRunspaceCapabilities(RunspaceDetails runspaceDetails) { DscBreakpointCapability.CheckForCapability(this.CurrentRunspace, this, this.logger); diff --git a/src/PowerShellEditorServices/Session/PromptNest.cs b/src/PowerShellEditorServices/Session/PromptNest.cs new file mode 100644 index 000000000..9cf4437f2 --- /dev/null +++ b/src/PowerShellEditorServices/Session/PromptNest.cs @@ -0,0 +1,564 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + using System; + using System.Management.Automation; + + /// + /// Represents the stack of contexts in which PowerShell commands can be invoked. + /// + internal class PromptNest : IDisposable + { + private ConcurrentStack _frameStack; + + private PromptNestFrame _readLineFrame; + + private IHostInput _consoleReader; + + private PowerShellContext _powerShellContext; + + private IVersionSpecificOperations _versionSpecificOperations; + + private bool _isDisposed; + + private object _syncObject = new object(); + + private object _disposeSyncObject = new object(); + + /// + /// Initializes a new instance of the class. + /// + /// + /// The to track prompt status for. + /// + /// + /// The instance for the first frame. + /// + /// + /// The input handler. + /// + /// + /// The for the calling + /// instance. + /// + /// + /// This constructor should only be called when + /// is set to the initial runspace. + /// + internal PromptNest( + PowerShellContext powerShellContext, + PowerShell initialPowerShell, + IHostInput consoleReader, + IVersionSpecificOperations versionSpecificOperations) + { + _versionSpecificOperations = versionSpecificOperations; + _consoleReader = consoleReader; + _powerShellContext = powerShellContext; + _frameStack = new ConcurrentStack(); + _frameStack.Push( + new PromptNestFrame( + initialPowerShell, + NewHandleQueue())); + + var readLineShell = PowerShell.Create(); + readLineShell.Runspace = powerShellContext.CurrentRunspace.Runspace; + _readLineFrame = new PromptNestFrame( + readLineShell, + new AsyncQueue()); + + ReleaseRunspaceHandleImpl(isReadLine: true); + } + + /// + /// Gets a value indicating whether the current frame was created by a debugger stop event. + /// + internal bool IsInDebugger => CurrentFrame.FrameType.HasFlag(PromptNestFrameType.Debug); + + /// + /// Gets a value indicating whether the current frame was created for an out of process runspace. + /// + internal bool IsRemote => CurrentFrame.FrameType.HasFlag(PromptNestFrameType.Remote); + + /// + /// Gets a value indicating whether the current frame was created by PSHost.EnterNestedPrompt(). + /// + internal bool IsNestedPrompt => CurrentFrame.FrameType.HasFlag(PromptNestFrameType.NestedPrompt); + + /// + /// Gets a value indicating the current number of frames managed by this PromptNest. + /// + internal int NestedPromptLevel => _frameStack.Count; + + private PromptNestFrame CurrentFrame + { + get + { + _frameStack.TryPeek(out PromptNestFrame currentFrame); + return _isDisposed ? _readLineFrame : currentFrame; + } + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + lock (_disposeSyncObject) + { + if (_isDisposed || !disposing) + { + return; + } + + while (NestedPromptLevel > 1) + { + _consoleReader?.StopCommandLoop(); + var currentFrame = CurrentFrame; + if (currentFrame.FrameType.HasFlag(PromptNestFrameType.Debug)) + { + _versionSpecificOperations.StopCommandInDebugger(_powerShellContext); + currentFrame.ThreadController.StartThreadExit(DebuggerResumeAction.Stop); + currentFrame.WaitForFrameExit(CancellationToken.None); + continue; + } + + if (currentFrame.FrameType.HasFlag(PromptNestFrameType.NestedPrompt)) + { + _powerShellContext.ExitAllNestedPrompts(); + continue; + } + + currentFrame.PowerShell.BeginStop(null, null); + currentFrame.WaitForFrameExit(CancellationToken.None); + } + + _consoleReader?.StopCommandLoop(); + _readLineFrame.Dispose(); + CurrentFrame.Dispose(); + _frameStack.Clear(); + _powerShellContext = null; + _consoleReader = null; + _isDisposed = true; + } + } + + /// + /// Gets the for the current frame. + /// + /// + /// The for the current frame, or + /// if the current frame does not have one. + /// + internal ThreadController GetThreadController() + { + if (_isDisposed) + { + return null; + } + + return CurrentFrame.IsThreadController ? CurrentFrame.ThreadController : null; + } + + /// + /// Create a new and set it as the current frame. + /// + internal void PushPromptContext() + { + if (_isDisposed) + { + return; + } + + PushPromptContext(PromptNestFrameType.Normal); + } + + /// + /// Create a new and set it as the current frame. + /// + /// The frame type. + internal void PushPromptContext(PromptNestFrameType frameType) + { + if (_isDisposed) + { + return; + } + + _frameStack.Push( + new PromptNestFrame( + frameType.HasFlag(PromptNestFrameType.Remote) + ? PowerShell.Create() + : PowerShell.Create(RunspaceMode.CurrentRunspace), + NewHandleQueue(), + frameType)); + } + + /// + /// Dispose of the current and revert to the previous frame. + /// + internal void PopPromptContext() + { + PromptNestFrame currentFrame; + lock (_syncObject) + { + if (_isDisposed || _frameStack.Count == 1) + { + return; + } + + _frameStack.TryPop(out currentFrame); + } + + currentFrame.Dispose(); + } + + /// + /// Get the instance for the current + /// . + /// + /// Indicates whether this is for a PSReadLine command. + /// The instance for the current frame. + internal PowerShell GetPowerShell(bool isReadLine = false) + { + if (_isDisposed) + { + return null; + } + + // Typically we want to run PSReadLine on the current nest frame. + // The exception is when the current frame is remote, in which + // case we need to run it in it's own frame because we can't take + // over a remote pipeline through event invocation. + if (NestedPromptLevel > 1 && !IsRemote) + { + return CurrentFrame.PowerShell; + } + + return isReadLine ? _readLineFrame.PowerShell : CurrentFrame.PowerShell; + } + + /// + /// Get the for the current . + /// + /// + /// The that can be used to cancel the request. + /// + /// Indicates whether this is for a PSReadLine command. + /// The for the current frame. + internal RunspaceHandle GetRunspaceHandle(CancellationToken cancellationToken, bool isReadLine) + { + if (_isDisposed) + { + return null; + } + + // Also grab the main runspace handle if this is for a ReadLine pipeline and the runspace + // is in process. + if (isReadLine && !_powerShellContext.IsCurrentRunspaceOutOfProcess()) + { + GetRunspaceHandleImpl(cancellationToken, isReadLine: false); + } + + return GetRunspaceHandleImpl(cancellationToken, isReadLine); + } + + + /// + /// Get the for the current . + /// + /// + /// The that will be checked prior to + /// completing the returned task. + /// + /// Indicates whether this is for a PSReadLine command. + /// + /// A object representing the asynchronous operation. + /// The property will return the + /// for the current frame. + /// + internal async Task GetRunspaceHandleAsync(CancellationToken cancellationToken, bool isReadLine) + { + if (_isDisposed) + { + return null; + } + + // Also grab the main runspace handle if this is for a ReadLine pipeline and the runspace + // is in process. + if (isReadLine && !_powerShellContext.IsCurrentRunspaceOutOfProcess()) + { + await GetRunspaceHandleImplAsync(cancellationToken, isReadLine: false); + } + + return await GetRunspaceHandleImplAsync(cancellationToken, isReadLine); + } + + /// + /// Releases control of the runspace aquired via the . + /// + /// + /// The representing the control to release. + /// + internal void ReleaseRunspaceHandle(RunspaceHandle runspaceHandle) + { + if (_isDisposed) + { + return; + } + + ReleaseRunspaceHandleImpl(runspaceHandle.IsReadLine); + if (runspaceHandle.IsReadLine && !_powerShellContext.IsCurrentRunspaceOutOfProcess()) + { + ReleaseRunspaceHandleImpl(isReadLine: false); + } + } + + /// + /// Releases control of the runspace aquired via the . + /// + /// + /// The representing the control to release. + /// + /// + /// A object representing the release of the + /// . + /// + internal async Task ReleaseRunspaceHandleAsync(RunspaceHandle runspaceHandle) + { + if (_isDisposed) + { + return; + } + + await ReleaseRunspaceHandleImplAsync(runspaceHandle.IsReadLine); + if (runspaceHandle.IsReadLine && !_powerShellContext.IsCurrentRunspaceOutOfProcess()) + { + await ReleaseRunspaceHandleImplAsync(isReadLine: false); + } + } + + /// + /// Determines if the current frame is unavailable for commands. + /// + /// + /// A value indicating whether the current frame is unavailable for commands. + /// + internal bool IsMainThreadBusy() + { + return !_isDisposed && CurrentFrame.Queue.IsEmpty; + } + + /// + /// Determines if a PSReadLine command is currently running. + /// + /// + /// A value indicating whether a PSReadLine command is currently running. + /// + internal bool IsReadLineBusy() + { + return !_isDisposed && _readLineFrame.Queue.IsEmpty; + } + + /// + /// Blocks until the current frame has been disposed. + /// + /// + /// A delegate that when invoked initates the exit of the current frame. + /// + internal void WaitForCurrentFrameExit(Action initiator) + { + if (_isDisposed) + { + return; + } + + var currentFrame = CurrentFrame; + try + { + initiator.Invoke(currentFrame); + } + finally + { + currentFrame.WaitForFrameExit(CancellationToken.None); + } + } + + /// + /// Blocks until the current frame has been disposed. + /// + internal void WaitForCurrentFrameExit() + { + if (_isDisposed) + { + return; + } + + CurrentFrame.WaitForFrameExit(CancellationToken.None); + } + + /// + /// Blocks until the current frame has been disposed. + /// + /// + /// The used the exit the block prior to + /// the current frame being disposed. + /// + internal void WaitForCurrentFrameExit(CancellationToken cancellationToken) + { + if (_isDisposed) + { + return; + } + + CurrentFrame.WaitForFrameExit(cancellationToken); + } + + /// + /// Creates a task that is completed when the current frame has been disposed. + /// + /// + /// A delegate that when invoked initates the exit of the current frame. + /// + /// + /// A object representing the current frame being disposed. + /// + internal async Task WaitForCurrentFrameExitAsync(Func initiator) + { + if (_isDisposed) + { + return; + } + + var currentFrame = CurrentFrame; + try + { + await initiator.Invoke(currentFrame); + } + finally + { + await currentFrame.WaitForFrameExitAsync(CancellationToken.None); + } + } + + /// + /// Creates a task that is completed when the current frame has been disposed. + /// + /// + /// A delegate that when invoked initates the exit of the current frame. + /// + /// + /// A object representing the current frame being disposed. + /// + internal async Task WaitForCurrentFrameExitAsync(Action initiator) + { + if (_isDisposed) + { + return; + } + + var currentFrame = CurrentFrame; + try + { + initiator.Invoke(currentFrame); + } + finally + { + await currentFrame.WaitForFrameExitAsync(CancellationToken.None); + } + } + + /// + /// Creates a task that is completed when the current frame has been disposed. + /// + /// + /// A object representing the current frame being disposed. + /// + internal async Task WaitForCurrentFrameExitAsync() + { + if (_isDisposed) + { + return; + } + + await WaitForCurrentFrameExitAsync(CancellationToken.None); + } + + /// + /// Creates a task that is completed when the current frame has been disposed. + /// + /// + /// The used the exit the block prior to the current frame being disposed. + /// + /// + /// A object representing the current frame being disposed. + /// + internal async Task WaitForCurrentFrameExitAsync(CancellationToken cancellationToken) + { + if (_isDisposed) + { + return; + } + + await CurrentFrame.WaitForFrameExitAsync(cancellationToken); + } + + private AsyncQueue NewHandleQueue() + { + var queue = new AsyncQueue(); + queue.Enqueue(new RunspaceHandle(_powerShellContext)); + return queue; + } + + private RunspaceHandle GetRunspaceHandleImpl(CancellationToken cancellationToken, bool isReadLine) + { + if (isReadLine) + { + return _readLineFrame.Queue.Dequeue(cancellationToken); + } + + return CurrentFrame.Queue.Dequeue(cancellationToken); + } + + private async Task GetRunspaceHandleImplAsync(CancellationToken cancellationToken, bool isReadLine) + { + if (isReadLine) + { + return await _readLineFrame.Queue.DequeueAsync(cancellationToken); + } + + return await CurrentFrame.Queue.DequeueAsync(cancellationToken); + } + + private void ReleaseRunspaceHandleImpl(bool isReadLine) + { + if (isReadLine) + { + _readLineFrame.Queue.Enqueue(new RunspaceHandle(_powerShellContext, true)); + return; + } + + CurrentFrame.Queue.Enqueue(new RunspaceHandle(_powerShellContext, false)); + } + + private async Task ReleaseRunspaceHandleImplAsync(bool isReadLine) + { + if (isReadLine) + { + await _readLineFrame.Queue.EnqueueAsync(new RunspaceHandle(_powerShellContext, true)); + return; + } + + await CurrentFrame.Queue.EnqueueAsync(new RunspaceHandle(_powerShellContext, false)); + } + } +} diff --git a/src/PowerShellEditorServices/Session/PromptNestFrame.cs b/src/PowerShellEditorServices/Session/PromptNestFrame.cs new file mode 100644 index 000000000..cae7dfb8a --- /dev/null +++ b/src/PowerShellEditorServices/Session/PromptNestFrame.cs @@ -0,0 +1,137 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + using System.Management.Automation; + + /// + /// Represents a single frame in the . + /// + internal class PromptNestFrame : IDisposable + { + private const PSInvocationState IndisposableStates = PSInvocationState.Stopping | PSInvocationState.Running; + + private SemaphoreSlim _frameExited = new SemaphoreSlim(initialCount: 0); + + private bool _isDisposed = false; + + /// + /// Gets the instance. + /// + internal PowerShell PowerShell { get; } + + /// + /// Gets the queue that controls command invocation order. + /// + internal AsyncQueue Queue { get; } + + /// + /// Gets the frame type. + /// + internal PromptNestFrameType FrameType { get; } + + /// + /// Gets the . + /// + internal ThreadController ThreadController { get; } + + /// + /// Gets a value indicating whether the frame requires command invocations + /// to be routed to a specific thread. + /// + internal bool IsThreadController { get; } + + internal PromptNestFrame(PowerShell powerShell, AsyncQueue handleQueue) + : this(powerShell, handleQueue, PromptNestFrameType.Normal) + { } + + internal PromptNestFrame( + PowerShell powerShell, + AsyncQueue handleQueue, + PromptNestFrameType frameType) + { + PowerShell = powerShell; + Queue = handleQueue; + FrameType = frameType; + IsThreadController = (frameType & (PromptNestFrameType.Debug | PromptNestFrameType.NestedPrompt)) != 0; + if (!IsThreadController) + { + return; + } + + ThreadController = new ThreadController(this); + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (_isDisposed) + { + return; + } + + if (disposing) + { + if (IndisposableStates.HasFlag(PowerShell.InvocationStateInfo.State)) + { + PowerShell.BeginStop( + asyncResult => + { + PowerShell.Runspace = null; + PowerShell.Dispose(); + }, + state: null); + } + else + { + PowerShell.Runspace = null; + PowerShell.Dispose(); + } + + _frameExited.Release(); + } + + _isDisposed = true; + } + + /// + /// Blocks until the frame has been disposed. + /// + /// + /// The that will exit the block when cancelled. + /// + internal void WaitForFrameExit(CancellationToken cancellationToken) + { + _frameExited.Wait(cancellationToken); + _frameExited.Release(); + } + + /// + /// Creates a task object that is completed when the frame has been disposed. + /// + /// + /// The that will be checked prior to completing + /// the returned task. + /// + /// + /// A object that represents this frame being disposed. + /// + internal async Task WaitForFrameExitAsync(CancellationToken cancellationToken) + { + await _frameExited.WaitAsync(cancellationToken); + _frameExited.Release(); + } + } +} diff --git a/src/PowerShellEditorServices/Session/PromptNestFrameType.cs b/src/PowerShellEditorServices/Session/PromptNestFrameType.cs new file mode 100644 index 000000000..b42b42098 --- /dev/null +++ b/src/PowerShellEditorServices/Session/PromptNestFrameType.cs @@ -0,0 +1,21 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + [Flags] + internal enum PromptNestFrameType + { + Normal = 0, + + NestedPrompt = 1, + + Debug = 2, + + Remote = 4 + } +} diff --git a/src/PowerShellEditorServices/Session/RemoteFileManager.cs b/src/PowerShellEditorServices/Session/RemoteFileManager.cs index e2d47dd88..c9885b84f 100644 --- a/src/PowerShellEditorServices/Session/RemoteFileManager.cs +++ b/src/PowerShellEditorServices/Session/RemoteFileManager.cs @@ -253,7 +253,7 @@ public RemoteFileManager( this.logger = logger; this.powerShellContext = powerShellContext; - this.powerShellContext.RunspaceChanged += HandleRunspaceChanged; + this.powerShellContext.RunspaceChanged += HandleRunspaceChangedAsync; this.editorOperations = editorOperations; @@ -287,7 +287,7 @@ public RemoteFileManager( /// /// The local file path where the remote file's contents have been stored. /// - public async Task FetchRemoteFile( + public async Task FetchRemoteFileAsync( string remoteFilePath, RunspaceDetails runspaceDetails) { @@ -313,7 +313,7 @@ public async Task FetchRemoteFile( command.AddParameter("Encoding", "Byte"); byte[] fileContent = - (await this.powerShellContext.ExecuteCommand(command, false, false)) + (await this.powerShellContext.ExecuteCommandAsync(command, false, false)) .FirstOrDefault(); if (fileContent != null) @@ -350,7 +350,7 @@ public async Task FetchRemoteFile( /// file to disk before this method is called. /// /// A Task to be awaited for completion. - public async Task SaveRemoteFile(string localFilePath) + public async Task SaveRemoteFileAsync(string localFilePath) { string remoteFilePath = this.GetMappedPath( @@ -383,7 +383,7 @@ public async Task SaveRemoteFile(string localFilePath) StringBuilder errorMessages = new StringBuilder(); - await this.powerShellContext.ExecuteCommand( + await this.powerShellContext.ExecuteCommandAsync( saveCommand, errorMessages, false, @@ -509,7 +509,7 @@ private RemotePathMappings GetPathMappings(RunspaceDetails runspaceDetails) return remotePathMappings; } - private async void HandleRunspaceChanged(object sender, RunspaceChangedEventArgs e) + private async void HandleRunspaceChangedAsync(object sender, RunspaceChangedEventArgs e) { if (e.ChangeAction == RunspaceChangeAction.Enter) { @@ -530,7 +530,7 @@ private async void HandleRunspaceChanged(object sender, RunspaceChangedEventArgs { foreach (string remotePath in remotePathMappings.OpenedPaths) { - await this.editorOperations?.CloseFile(remotePath); + await this.editorOperations?.CloseFileAsync(remotePath); } } } @@ -542,7 +542,7 @@ private async void HandleRunspaceChanged(object sender, RunspaceChangedEventArgs } } - private async void HandlePSEventReceived(object sender, PSEventArgs args) + private async void HandlePSEventReceivedAsync(object sender, PSEventArgs args) { if (string.Equals(RemoteSessionOpenFile, args.SourceIdentifier, StringComparison.CurrentCultureIgnoreCase)) { @@ -591,8 +591,8 @@ private async void HandlePSEventReceived(object sender, PSEventArgs args) } else { - await this.editorOperations?.NewFile(); - EditorContext context = await this.editorOperations?.GetEditorContext(); + await this.editorOperations?.NewFileAsync(); + EditorContext context = await this.editorOperations?.GetEditorContextAsync(); context?.CurrentFile.InsertText(Encoding.UTF8.GetString(fileContent, 0, fileContent.Length)); } } @@ -605,7 +605,7 @@ private async void HandlePSEventReceived(object sender, PSEventArgs args) } // Open the file in the editor - this.editorOperations?.OpenFile(localFilePath, preview); + this.editorOperations?.OpenFileAsync(localFilePath, preview); } } catch (NullReferenceException e) @@ -622,7 +622,7 @@ private void RegisterPSEditFunction(RunspaceDetails runspaceDetails) { try { - runspaceDetails.Runspace.Events.ReceivedEvents.PSEventReceived += HandlePSEventReceived; + runspaceDetails.Runspace.Events.ReceivedEvents.PSEventReceived += HandlePSEventReceivedAsync; PSCommand createCommand = new PSCommand(); createCommand @@ -631,7 +631,7 @@ private void RegisterPSEditFunction(RunspaceDetails runspaceDetails) if (runspaceDetails.Context == RunspaceContext.DebuggedRunspace) { - this.powerShellContext.ExecuteCommand(createCommand).Wait(); + this.powerShellContext.ExecuteCommandAsync(createCommand).Wait(); } else { @@ -659,7 +659,7 @@ private void RemovePSEditFunction(RunspaceDetails runspaceDetails) { if (runspaceDetails.Runspace.Events != null) { - runspaceDetails.Runspace.Events.ReceivedEvents.PSEventReceived -= HandlePSEventReceived; + runspaceDetails.Runspace.Events.ReceivedEvents.PSEventReceived -= HandlePSEventReceivedAsync; } if (runspaceDetails.Runspace.RunspaceStateInfo.State == RunspaceState.Opened) diff --git a/src/PowerShellEditorServices/Session/RunspaceHandle.cs b/src/PowerShellEditorServices/Session/RunspaceHandle.cs index b7fc0e8f1..4947eadbe 100644 --- a/src/PowerShellEditorServices/Session/RunspaceHandle.cs +++ b/src/PowerShellEditorServices/Session/RunspaceHandle.cs @@ -28,14 +28,21 @@ public Runspace Runspace } } + internal bool IsReadLine { get; } + /// /// Initializes a new instance of the RunspaceHandle class using the /// given runspace. /// /// The PowerShellContext instance which manages the runspace. public RunspaceHandle(PowerShellContext powerShellContext) + : this(powerShellContext, false) + { } + + internal RunspaceHandle(PowerShellContext powerShellContext, bool isReadLine) { this.powerShellContext = powerShellContext; + this.IsReadLine = isReadLine; } /// diff --git a/src/PowerShellEditorServices/Session/ThreadController.cs b/src/PowerShellEditorServices/Session/ThreadController.cs new file mode 100644 index 000000000..8720e3fa7 --- /dev/null +++ b/src/PowerShellEditorServices/Session/ThreadController.cs @@ -0,0 +1,131 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.Management.Automation; +using System.Management.Automation.Runspaces; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + /// + /// Provides the ability to route PowerShell command invocations to a specific thread. + /// + internal class ThreadController + { + private PromptNestFrame _nestFrame; + + internal AsyncQueue PipelineRequestQueue { get; } + + internal TaskCompletionSource FrameExitTask { get; } + + internal int ManagedThreadId { get; } + + internal bool IsPipelineThread { get; } + + /// + /// Initializes an new instance of the ThreadController class. This constructor should only + /// ever been called from the thread it is meant to control. + /// + /// The parent PromptNestFrame object. + internal ThreadController(PromptNestFrame nestFrame) + { + _nestFrame = nestFrame; + PipelineRequestQueue = new AsyncQueue(); + FrameExitTask = new TaskCompletionSource(); + ManagedThreadId = Thread.CurrentThread.ManagedThreadId; + + // If the debugger stop is triggered on a thread with no default runspace we + // shouldn't attempt to route commands to it. + IsPipelineThread = Runspace.DefaultRunspace != null; + } + + /// + /// Determines if the caller is already on the thread that this object maintains. + /// + /// + /// A value indicating if the caller is already on the thread maintained by this object. + /// + internal bool IsCurrentThread() + { + return Thread.CurrentThread.ManagedThreadId == ManagedThreadId; + } + + /// + /// Requests the invocation of a PowerShell command on the thread maintained by this object. + /// + /// The execution request to send. + /// + /// A task object representing the asynchronous operation. The Result property will return + /// the output of the command invocation. + /// + internal async Task> RequestPipelineExecutionAsync( + PipelineExecutionRequest executionRequest) + { + await PipelineRequestQueue.EnqueueAsync(executionRequest); + return await executionRequest.Results; + } + + /// + /// Retrieves the first currently queued execution request. If there are no pending + /// execution requests then the task will be completed when one is requested. + /// + /// + /// A task object representing the asynchronous operation. The Result property will return + /// the retrieved pipeline execution request. + /// + internal async Task TakeExecutionRequestAsync() + { + return await PipelineRequestQueue.DequeueAsync(); + } + + /// + /// Marks the thread to be exited. + /// + /// + /// The resume action for the debugger. If the frame is not a debugger frame this parameter + /// is ignored. + /// + internal void StartThreadExit(DebuggerResumeAction action) + { + StartThreadExit(action, waitForExit: false); + } + + /// + /// Marks the thread to be exited. + /// + /// + /// The resume action for the debugger. If the frame is not a debugger frame this parameter + /// is ignored. + /// + /// + /// Indicates whether the method should block until the exit is completed. + /// + internal void StartThreadExit(DebuggerResumeAction action, bool waitForExit) + { + Task.Run(() => FrameExitTask.TrySetResult(action)); + if (!waitForExit) + { + return; + } + + _nestFrame.WaitForFrameExit(CancellationToken.None); + } + + /// + /// Creates a task object that completes when the thread has be marked for exit. + /// + /// + /// A task object representing the frame receiving a request to exit. The Result property + /// will return the DebuggerResumeAction supplied with the request. + /// + internal async Task Exit() + { + return await FrameExitTask.Task.ConfigureAwait(false); + } + } +} diff --git a/src/PowerShellEditorServices/Templates/TemplateService.cs b/src/PowerShellEditorServices/Templates/TemplateService.cs index 8ee3da496..18f58085c 100644 --- a/src/PowerShellEditorServices/Templates/TemplateService.cs +++ b/src/PowerShellEditorServices/Templates/TemplateService.cs @@ -50,7 +50,7 @@ public TemplateService(PowerShellContext powerShellContext, ILogger logger) /// Checks if Plaster is installed on the user's machine. /// /// A Task that can be awaited until the check is complete. The result will be true if Plaster is installed. - public async Task ImportPlasterIfInstalled() + public async Task ImportPlasterIfInstalledAsync() { if (!this.isPlasterInstalled.HasValue) { @@ -73,7 +73,7 @@ public async Task ImportPlasterIfInstalled() this.logger.Write(LogLevel.Verbose, "Checking if Plaster is installed..."); var getResult = - await this.powerShellContext.ExecuteCommand( + await this.powerShellContext.ExecuteCommandAsync( psCommand, false, false); PSObject moduleObject = getResult.First(); @@ -98,7 +98,7 @@ await this.powerShellContext.ExecuteCommand( .AddParameter("PassThru"); var importResult = - await this.powerShellContext.ExecuteCommand( + await this.powerShellContext.ExecuteCommandAsync( psCommand, false, false); this.isPlasterLoaded = importResult.Any(); @@ -124,7 +124,7 @@ await this.powerShellContext.ExecuteCommand( /// included templates. /// /// A Task which can be awaited for the TemplateDetails list to be returned. - public async Task GetAvailableTemplates( + public async Task GetAvailableTemplatesAsync( bool includeInstalledModules) { if (!this.isPlasterLoaded) @@ -141,7 +141,7 @@ public async Task GetAvailableTemplates( } var templateObjects = - await this.powerShellContext.ExecuteCommand( + await this.powerShellContext.ExecuteCommandAsync( psCommand, false, false); this.logger.Write( @@ -162,7 +162,7 @@ await this.powerShellContext.ExecuteCommand( /// The folder path containing the template. /// The folder path where the files will be created. /// A boolean-returning Task which communicates success or failure. - public async Task CreateFromTemplate( + public async Task CreateFromTemplateAsync( string templatePath, string destinationPath) { @@ -176,7 +176,7 @@ public async Task CreateFromTemplate( command.AddParameter("DestinationPath", destinationPath); var errorString = new System.Text.StringBuilder(); - await this.powerShellContext.ExecuteCommand( + await this.powerShellContext.ExecuteCommandAsync( command, errorString, new ExecutionOptions diff --git a/src/PowerShellEditorServices/Utility/AsyncDebouncer.cs b/src/PowerShellEditorServices/Utility/AsyncDebouncer.cs index f8bbd2422..9644eff77 100644 --- a/src/PowerShellEditorServices/Utility/AsyncDebouncer.cs +++ b/src/PowerShellEditorServices/Utility/AsyncDebouncer.cs @@ -59,12 +59,12 @@ public AsyncDebouncer(int flushInterval, bool restartOnInvoke) /// The argument for this implementation's Invoke method. /// /// A Task to be awaited until the Invoke is queued. - public async Task Invoke(TInvokeArgs invokeArgument) + public async Task InvokeAsync(TInvokeArgs invokeArgument) { using (await this.asyncLock.LockAsync()) { // Invoke the implementor - await this.OnInvoke(invokeArgument); + await this.OnInvokeAsync(invokeArgument); // If there's no timer, start one if (this.currentTimerTask == null) @@ -88,7 +88,7 @@ public async Task Invoke(TInvokeArgs invokeArgument) /// deadlocks could occur. /// /// A Task to be awaited until Flush completes. - public async Task Flush() + public async Task FlushAsync() { using (await this.asyncLock.LockAsync()) { @@ -96,7 +96,7 @@ public async Task Flush() this.CancelTimer(); // Flush the current output - await this.OnFlush(); + await this.OnFlushAsync(); } } @@ -112,13 +112,13 @@ public async Task Flush() /// The argument for this implementation's OnInvoke method. /// /// A Task to be awaited for the invoke to complete. - protected abstract Task OnInvoke(TInvokeArgs invokeArgument); + protected abstract Task OnInvokeAsync(TInvokeArgs invokeArgument); /// /// Implemented by the subclass to complete the current operation. /// /// A Task to be awaited for the operation to complete. - protected abstract Task OnFlush(); + protected abstract Task OnFlushAsync(); #endregion @@ -135,7 +135,7 @@ private void StartTimer() { if (!t.IsCanceled) { - return this.Flush(); + return this.FlushAsync(); } else { @@ -153,8 +153,8 @@ private bool CancelTimer() } // Was the task cancelled? - bool wasCancelled = - this.currentTimerTask == null || + bool wasCancelled = + this.currentTimerTask == null || this.currentTimerTask.IsCanceled; // Clear the current task so that another may be created diff --git a/src/PowerShellEditorServices/Utility/AsyncLock.cs b/src/PowerShellEditorServices/Utility/AsyncLock.cs index eee894d9c..5eba1b24f 100644 --- a/src/PowerShellEditorServices/Utility/AsyncLock.cs +++ b/src/PowerShellEditorServices/Utility/AsyncLock.cs @@ -74,6 +74,31 @@ public Task LockAsync(CancellationToken cancellationToken) TaskScheduler.Default); } + /// + /// Obtains or waits for a lock which can be used to synchronize + /// access to a resource. + /// + /// + public IDisposable Lock() + { + return Lock(CancellationToken.None); + } + + /// + /// Obtains or waits for a lock which can be used to synchronize + /// access to a resource. The wait may be cancelled with the + /// given CancellationToken. + /// + /// + /// A CancellationToken which can be used to cancel the lock. + /// + /// + public IDisposable Lock(CancellationToken cancellationToken) + { + lockSemaphore.Wait(cancellationToken); + return this.lockReleaseTask.Result; + } + #endregion #region Private Classes diff --git a/src/PowerShellEditorServices/Utility/AsyncQueue.cs b/src/PowerShellEditorServices/Utility/AsyncQueue.cs index 98c00dc8e..85bbc1592 100644 --- a/src/PowerShellEditorServices/Utility/AsyncQueue.cs +++ b/src/PowerShellEditorServices/Utility/AsyncQueue.cs @@ -87,13 +87,38 @@ public async Task EnqueueAsync(T item) return; } } - + // No more requests waiting, queue the item for a later request this.itemQueue.Enqueue(item); this.IsEmpty = false; } } + /// + /// Enqueues an item onto the end of the queue. + /// + /// The item to be added to the queue. + public void Enqueue(T item) + { + using (queueLock.Lock()) + { + while (this.requestQueue.Count > 0) + { + var requestTaskSource = this.requestQueue.Dequeue(); + if (requestTaskSource.Task.IsCanceled) + { + continue; + } + + requestTaskSource.SetResult(item); + return; + } + } + + this.itemQueue.Enqueue(item); + this.IsEmpty = false; + } + /// /// Dequeues an item from the queue or waits asynchronously /// until an item is available. @@ -149,6 +174,50 @@ public async Task DequeueAsync(CancellationToken cancellationToken) return await requestTask; } + /// + /// Dequeues an item from the queue or waits asynchronously + /// until an item is available. + /// + /// + public T Dequeue() + { + return Dequeue(CancellationToken.None); + } + + /// + /// Dequeues an item from the queue or waits asynchronously + /// until an item is available. The wait can be cancelled + /// using the given CancellationToken. + /// + /// + /// A CancellationToken with which a dequeue wait can be cancelled. + /// + /// + public T Dequeue(CancellationToken cancellationToken) + { + TaskCompletionSource requestTask; + using (queueLock.Lock(cancellationToken)) + { + if (this.itemQueue.Count > 0) + { + T item = this.itemQueue.Dequeue(); + this.IsEmpty = this.itemQueue.Count == 0; + + return item; + } + + requestTask = new TaskCompletionSource(); + this.requestQueue.Enqueue(requestTask); + + if (cancellationToken.CanBeCanceled) + { + cancellationToken.Register(() => requestTask.TrySetCanceled()); + } + } + + return requestTask.Task.GetAwaiter().GetResult(); + } + #endregion } } diff --git a/src/PowerShellEditorServices/Utility/AsyncUtils.cs b/src/PowerShellEditorServices/Utility/AsyncUtils.cs new file mode 100644 index 000000000..8da21b942 --- /dev/null +++ b/src/PowerShellEditorServices/Utility/AsyncUtils.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Threading; + +namespace Microsoft.PowerShell.EditorServices.Utility +{ + /// + /// Provides utility methods for common asynchronous operations. + /// + internal static class AsyncUtils + { + /// + /// Creates a with an handle initial and + /// max count of one. + /// + /// A simple single handle . + internal static SemaphoreSlim CreateSimpleLockingSemaphore() + { + return new SemaphoreSlim(initialCount: 1, maxCount: 1); + } + } +} diff --git a/src/PowerShellEditorServices/Workspace/ScriptFile.cs b/src/PowerShellEditorServices/Workspace/ScriptFile.cs index 0e0d4d6e7..f4dd3de26 100644 --- a/src/PowerShellEditorServices/Workspace/ScriptFile.cs +++ b/src/PowerShellEditorServices/Workspace/ScriptFile.cs @@ -602,10 +602,9 @@ private void ParseFileContents() { Token[] scriptTokens; -#if PowerShellv5r2 // This overload appeared with Windows 10 Update 1 - if (this.powerShellVersion.Major >= 5 && - this.powerShellVersion.Build >= 10586) + if (this.powerShellVersion.Major >= 6 || + (this.powerShellVersion.Major == 5 && this.powerShellVersion.Build >= 10586)) { // Include the file path so that module relative // paths are evaluated correctly @@ -624,13 +623,6 @@ private void ParseFileContents() out scriptTokens, out parseErrors); } -#else - this.ScriptAst = - Parser.ParseInput( - this.Contents, - out scriptTokens, - out parseErrors); -#endif this.ScriptTokens = scriptTokens; } diff --git a/test/PowerShellEditorServices.Test.Host/DebugAdapterTests.cs b/test/PowerShellEditorServices.Test.Host/DebugAdapterTests.cs index a07e79501..14c6171a6 100644 --- a/test/PowerShellEditorServices.Test.Host/DebugAdapterTests.cs +++ b/test/PowerShellEditorServices.Test.Host/DebugAdapterTests.cs @@ -7,6 +7,7 @@ using Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter; using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; +using Microsoft.PowerShell.EditorServices.Test.Shared; using Microsoft.PowerShell.EditorServices.Utility; using System; using System.IO; @@ -20,17 +21,13 @@ public class DebugAdapterTests : ServerTestsBase, IAsyncLifetime private ILogger logger; private DebugAdapterClient debugAdapterClient; private string DebugScriptPath = - Path.GetFullPath(@"..\..\..\..\PowerShellEditorServices.Test.Shared\Debugging\DebugTest.ps1"); + Path.GetFullPath(TestUtilities.NormalizePath("../../../../PowerShellEditorServices.Test.Shared/Debugging/DebugTest.ps1")); public async Task InitializeAsync() { string testLogPath = Path.Combine( -#if CoreCLR AppContext.BaseDirectory, -#else - AppDomain.CurrentDomain.BaseDirectory, -#endif "logs", this.GetType().Name, Guid.NewGuid().ToString().Substring(0, 8)); @@ -51,7 +48,7 @@ await this.LaunchService( this.debugAdapterClient = new DebugAdapterClient( - await NamedPipeClientChannel.Connect( + await NamedPipeClientChannel.ConnectAsync( pipeNames.Item2, MessageProtocolType.DebugAdapter, this.logger), @@ -60,7 +57,7 @@ await NamedPipeClientChannel.Connect( this.messageSender = this.debugAdapterClient; this.messageHandlers = this.debugAdapterClient; - await this.debugAdapterClient.Start(); + await this.debugAdapterClient.StartAsync(); } public Task DisposeAsync() @@ -139,7 +136,7 @@ public async Task DebugAdapterReceivesOutputEvents() private async Task LaunchScript(string scriptPath) { - await this.debugAdapterClient.LaunchScript(scriptPath); + await this.debugAdapterClient.LaunchScriptAsync(scriptPath); } } } diff --git a/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs b/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs index 27daf6fae..5d25fccd3 100644 --- a/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs +++ b/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs @@ -11,6 +11,7 @@ using Microsoft.PowerShell.EditorServices.Protocol.Messages; using Microsoft.PowerShell.EditorServices.Protocol.Server; using Microsoft.PowerShell.EditorServices.Session; +using Microsoft.PowerShell.EditorServices.Test.Shared; using Microsoft.PowerShell.EditorServices.Utility; using System; using System.IO; @@ -29,11 +30,7 @@ public async Task InitializeAsync() { string testLogPath = Path.Combine( -#if CoreCLR AppContext.BaseDirectory, -#else - AppDomain.CurrentDomain.BaseDirectory, -#endif "logs", this.GetType().Name, Guid.NewGuid().ToString().Substring(0, 8)); @@ -54,7 +51,7 @@ await this.LaunchService( this.languageServiceClient = new LanguageServiceClient( - await NamedPipeClientChannel.Connect( + await NamedPipeClientChannel.ConnectAsync( pipeNames.Item1, MessageProtocolType.LanguageServer, this.logger), @@ -68,7 +65,7 @@ await NamedPipeClientChannel.Connect( public async Task DisposeAsync() { - await this.languageServiceClient.Stop(); + await this.languageServiceClient.StopAsync(); this.KillService(); } @@ -77,7 +74,7 @@ public async Task DisposeAsync() public async Task ServiceReturnsSyntaxErrors() { // Send the 'didOpen' event - await this.SendOpenFileEvent("TestFiles\\SimpleSyntaxError.ps1", false); + await this.SendOpenFileEvent(TestUtilities.NormalizePath("TestFiles/SimpleSyntaxError.ps1"), false); // Wait for the diagnostic event PublishDiagnosticsNotification diagnostics = @@ -90,11 +87,11 @@ await this.WaitForEvent( string.IsNullOrEmpty(diagnostics.Diagnostics[0].Message)); } - [Fact(Skip = "Skipping until Script Analyzer integration is added back")] + [Fact] public async Task ServiceReturnsSemanticMarkers() { // Send the 'didOpen' event - await this.SendOpenFileEvent("TestFiles\\SimpleSemanticError.ps1", false); + await this.SendOpenFileEvent(TestUtilities.NormalizePath("TestFiles/SimpleSemanticError.ps1"), false); // Wait for the diagnostic event PublishDiagnosticsNotification diagnostics = @@ -110,7 +107,7 @@ await this.WaitForEvent( public async Task ServiceReturnsNoErrorsForUsingRelativeModulePaths() { // Send the 'didOpen' event - await this.SendOpenFileEvent("TestFiles\\Module.psm1", false); + await this.SendOpenFileEvent(TestUtilities.NormalizePath("TestFiles/Module.psm1"), false); // Wait for the diagnostic event PublishDiagnosticsNotification diagnostics = @@ -124,7 +121,7 @@ await this.WaitForEvent( [Fact] public async Task ServiceCompletesFunctionName() { - await this.SendOpenFileEvent("TestFiles\\CompleteFunctionName.ps1"); + await this.SendOpenFileEvent(TestUtilities.NormalizePath("TestFiles/CompleteFunctionName.ps1")); CompletionItem[] completions = await this.SendRequest( @@ -133,7 +130,7 @@ await this.SendRequest( { TextDocument = new TextDocumentIdentifier { - Uri = "TestFiles\\CompleteFunctionName.ps1", + Uri = TestUtilities.NormalizePath("TestFiles/CompleteFunctionName.ps1"), }, Position = new Position { @@ -151,7 +148,7 @@ await this.SendRequest( [Fact] public async Task CompletesDetailOnVariableSuggestion() { - await this.SendOpenFileEvent("TestFiles\\CompleteFunctionName.ps1"); + await this.SendOpenFileEvent(TestUtilities.NormalizePath("TestFiles/CompleteFunctionName.ps1")); CompletionItem[] completions = await this.SendRequest( @@ -160,7 +157,7 @@ await this.SendRequest( { TextDocument = new TextDocumentIdentifier { - Uri = "TestFiles\\CompleteFunctionName.ps1" + Uri = TestUtilities.NormalizePath("TestFiles/CompleteFunctionName.ps1") }, Position = new Position { @@ -181,7 +178,7 @@ await this.SendRequest( [Fact(Skip = "Skipped until variable documentation gathering is added back.")] public async Task CompletesDetailOnVariableDocSuggestion() { - await this.SendOpenFileEvent("TestFiles\\CompleteFunctionName.ps1"); + await this.SendOpenFileEvent(TestUtilities.NormalizePath("TestFiles/CompleteFunctionName.ps1")); await this.SendRequest( CompletionRequest.Type, @@ -189,7 +186,7 @@ await this.SendRequest( { TextDocument = new TextDocumentIdentifier { - Uri = "TestFiles\\CompleteFunctionName.ps1" + Uri = TestUtilities.NormalizePath("TestFiles/CompleteFunctionName.ps1") }, Position = new Position { @@ -223,7 +220,7 @@ await this.SendRequest( [Fact] public async Task CompletesDetailOnCommandSuggestion() { - await this.SendOpenFileEvent("TestFiles\\CompleteFunctionName.ps1"); + await this.SendOpenFileEvent(TestUtilities.NormalizePath("TestFiles/CompleteFunctionName.ps1")); CompletionItem[] completions = await this.SendRequest( @@ -232,7 +229,7 @@ await this.SendRequest( { TextDocument = new TextDocumentIdentifier { - Uri = "TestFiles\\CompleteFunctionName.ps1" + Uri = TestUtilities.NormalizePath("TestFiles/CompleteFunctionName.ps1") }, Position = new Position { @@ -263,7 +260,7 @@ await this.SendRequest( [Fact] public async Task FindsReferencesOfVariable() { - await this.SendOpenFileEvent("TestFiles\\FindReferences.ps1"); + await this.SendOpenFileEvent(TestUtilities.NormalizePath("TestFiles/FindReferences.ps1")); Location[] locations = await this.SendRequest( @@ -272,7 +269,7 @@ await this.SendRequest( { TextDocument = new TextDocumentIdentifier { - Uri = "TestFiles\\FindReferences.ps1" + Uri = TestUtilities.NormalizePath("TestFiles/FindReferences.ps1") }, Position = new Position { @@ -295,7 +292,7 @@ await this.SendRequest( [Fact] public async Task FindsNoReferencesOfEmptyLine() { - await this.SendOpenFileEvent("TestFiles\\FindReferences.ps1"); + await this.SendOpenFileEvent(TestUtilities.NormalizePath("TestFiles/FindReferences.ps1")); Location[] locations = await this.SendRequest( @@ -304,7 +301,7 @@ await this.SendRequest( { TextDocument = new TextDocumentIdentifier { - Uri = "TestFiles\\FindReferences.ps1" + Uri = TestUtilities.NormalizePath("TestFiles/FindReferences.ps1") }, Position = new Position { @@ -320,7 +317,7 @@ await this.SendRequest( [Fact] public async Task FindsReferencesOnFunctionDefinition() { - await this.SendOpenFileEvent("TestFiles\\FindReferences.ps1"); + await this.SendOpenFileEvent(TestUtilities.NormalizePath("TestFiles/FindReferences.ps1")); Location[] locations = await this.SendRequest( @@ -329,7 +326,7 @@ await this.SendRequest( { TextDocument = new TextDocumentIdentifier { - Uri = "TestFiles\\FindReferences.ps1" + Uri = TestUtilities.NormalizePath("TestFiles/FindReferences.ps1") }, Position = new Position { @@ -352,7 +349,7 @@ await this.SendRequest( [Fact] public async Task FindsReferencesOnCommand() { - await this.SendOpenFileEvent("TestFiles\\FindReferences.ps1"); + await this.SendOpenFileEvent(TestUtilities.NormalizePath("TestFiles/FindReferences.ps1")); Location[] locations = await this.SendRequest( @@ -361,7 +358,7 @@ await this.SendRequest( { TextDocument = new TextDocumentIdentifier { - Uri = "TestFiles\\FindReferences.ps1" + Uri = TestUtilities.NormalizePath("TestFiles/FindReferences.ps1") }, Position = new Position { @@ -384,7 +381,7 @@ await this.SendRequest( [Fact] public async Task FindsDefinitionOfCommand() { - await this.SendOpenFileEvent("TestFiles\\FindReferences.ps1"); + await this.SendOpenFileEvent(TestUtilities.NormalizePath("TestFiles/FindReferences.ps1")); Location[] locations = await this.SendRequest( @@ -393,7 +390,7 @@ await this.SendRequest( { TextDocument = new TextDocumentIdentifier { - Uri = "TestFiles\\FindReferences.ps1", + Uri = TestUtilities.NormalizePath("TestFiles/FindReferences.ps1"), }, Position = new Position { @@ -411,7 +408,7 @@ await this.SendRequest( [Fact] public async Task FindsNoDefinitionOfBuiltinCommand() { - await this.SendOpenFileEvent("TestFiles\\FindReferences.ps1"); + await this.SendOpenFileEvent(TestUtilities.NormalizePath("TestFiles/FindReferences.ps1")); Location[] locations = await this.SendRequest( @@ -420,7 +417,7 @@ await this.SendRequest( { TextDocument = new TextDocumentIdentifier { - Uri = "TestFiles\\FindReferences.ps1" + Uri = TestUtilities.NormalizePath("TestFiles/FindReferences.ps1") }, Position = new Position { @@ -436,7 +433,7 @@ await this.SendRequest( [Fact] public async Task FindsDefinitionOfVariable() { - await this.SendOpenFileEvent("TestFiles\\FindReferences.ps1"); + await this.SendOpenFileEvent(TestUtilities.NormalizePath("TestFiles/FindReferences.ps1")); Location[] locations = await this.SendRequest( @@ -445,7 +442,7 @@ await this.SendRequest( { TextDocument = new TextDocumentIdentifier { - Uri = "TestFiles\\FindReferences.ps1" + Uri = TestUtilities.NormalizePath("TestFiles/FindReferences.ps1") }, Position = new Position { @@ -465,7 +462,7 @@ await this.SendRequest( [Fact] public async Task FindsDefinitionOfVariableInOtherFile() { - await this.SendOpenFileEvent("TestFiles\\FindReferences.ps1"); + await this.SendOpenFileEvent(TestUtilities.NormalizePath("TestFiles/FindReferences.ps1")); Location[] locations = await this.SendRequest( @@ -474,7 +471,7 @@ await this.SendRequest( { TextDocument = new TextDocumentIdentifier { - Uri = "TestFiles\\FindReferences.ps1" + Uri = TestUtilities.NormalizePath("TestFiles/FindReferences.ps1") }, Position = new Position { @@ -495,7 +492,7 @@ await this.SendRequest( [Fact] public async Task FindDefinitionOfVariableWithSpecialChars() { - await this.SendOpenFileEvent("TestFiles\\FindReferences.ps1"); + await this.SendOpenFileEvent(TestUtilities.NormalizePath("TestFiles/FindReferences.ps1")); Location[] locations = await this.SendRequest( @@ -504,7 +501,7 @@ await this.SendRequest( { TextDocument = new TextDocumentIdentifier { - Uri = "TestFiles\\FindReferences.ps1" + Uri = TestUtilities.NormalizePath("TestFiles/FindReferences.ps1") }, Position = new Position { @@ -525,7 +522,7 @@ await this.SendRequest( [Fact] public async Task FindsOccurencesOnFunctionDefinition() { - await this.SendOpenFileEvent("TestFiles\\FindReferences.ps1"); + await this.SendOpenFileEvent(TestUtilities.NormalizePath("TestFiles/FindReferences.ps1")); DocumentHighlight[] highlights = await this.SendRequest( @@ -534,7 +531,7 @@ await this.SendRequest( { TextDocument = new TextDocumentIdentifier { - Uri = "TestFiles\\FindReferences.ps1" + Uri = TestUtilities.NormalizePath("TestFiles/FindReferences.ps1") }, Position = new Position { @@ -548,10 +545,10 @@ await this.SendRequest( Assert.Equal(2, highlights[1].Range.Start.Line); } - [Fact(Skip = "This test hangs in VSTS for some reason...")] + [Fact] public async Task GetsParameterHintsOnCommand() { - await this.SendOpenFileEvent("TestFiles\\FindReferences.ps1"); + await this.SendOpenFileEvent(TestUtilities.NormalizePath("TestFiles/FindReferences.ps1")); SignatureHelp signatureHelp = await this.SendRequest( @@ -560,7 +557,7 @@ await this.SendRequest( { TextDocument = new TextDocumentIdentifier { - Uri = "TestFiles\\FindReferences.ps1" + Uri = TestUtilities.NormalizePath("TestFiles/FindReferences.ps1") }, Position = new Position { @@ -603,9 +600,9 @@ public async Task ServiceExpandsAliases() string expandedText = await this.SendRequest( ExpandAliasRequest.Type, - "gci\r\npwd"); + TestUtilities.NormalizeNewlines("gci\npwd")); - Assert.Equal("Get-ChildItem\r\nGet-Location", expandedText); + Assert.Equal(TestUtilities.NormalizeNewlines("Get-ChildItem\nGet-Location"), expandedText); } [Fact] @@ -647,7 +644,7 @@ public async Task ServiceExecutesReplCommandAndReceivesChoicePrompt() Assert.Equal(1, showChoicePromptRequest.DefaultChoices[0]); // Respond to the prompt request - await requestContext.SendResult( + await requestContext.SendResultAsync( new ShowChoicePromptResponse { ResponseText = "a" @@ -698,7 +695,7 @@ public async Task ServiceExecutesReplCommandAndReceivesInputPrompt() Assert.Equal("Name", showInputPromptRequest.Name); // Respond to the prompt request - await requestContext.SendResult( + await requestContext.SendResultAsync( new ShowInputPromptResponse { ResponseText = "John" @@ -757,7 +754,7 @@ public async Task ServiceLoadsProfilesOnDemand() string testProfilePath = Path.Combine( Path.GetFullPath( - @"..\..\..\..\PowerShellEditorServices.Test.Shared\Profile\"), + TestUtilities.NormalizePath("../../../../PowerShellEditorServices.Test.Shared/Profile/")), profileName); string currentUserCurrentHostPath = @@ -868,7 +865,7 @@ private async Task SendConfigurationRequest( bool enableProfileLoading = false) { // Send the configuration change to cause profiles to be loaded - await this.languageServiceClient.SendEvent( + await this.languageServiceClient.SendEventAsync( DidChangeConfigurationNotification.Type, new DidChangeConfigurationParams { diff --git a/test/PowerShellEditorServices.Test.Host/PowerShellEditorServices.Test.Host.csproj b/test/PowerShellEditorServices.Test.Host/PowerShellEditorServices.Test.Host.csproj index 8d7f65ba2..c183c3da3 100644 --- a/test/PowerShellEditorServices.Test.Host/PowerShellEditorServices.Test.Host.csproj +++ b/test/PowerShellEditorServices.Test.Host/PowerShellEditorServices.Test.Host.csproj @@ -1,42 +1,35 @@  - - netcoreapp2.0;net452 + netcoreapp2.1;net461 Microsoft.PowerShell.EditorServices.Test.Host - PreserveNewest - + - - - - 6.0.0-alpha13 - - - - - - - + $(DefineConstants);CoreCLR - - - - - - + + + + + + + + + + + diff --git a/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs b/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs index f8eef1062..e805d85cd 100644 --- a/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs +++ b/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs @@ -4,6 +4,7 @@ // using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; +using Microsoft.PowerShell.EditorServices.Test.Shared; using Microsoft.PowerShell.EditorServices.Utility; using Newtonsoft.Json.Linq; using System; @@ -11,6 +12,7 @@ using System.Diagnostics; using System.IO; using System.Reflection; +using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -34,19 +36,15 @@ protected async Task> LaunchService( string logPath, bool waitForDebugger = false) { - string modulePath = Path.GetFullPath(@"..\..\..\..\..\module"); - string scriptPath = Path.GetFullPath(Path.Combine(modulePath, @"PowerShellEditorServices\Start-EditorServices.ps1")); + string modulePath = Path.GetFullPath(TestUtilities.NormalizePath("../../../../../module")); + string scriptPath = Path.GetFullPath(Path.Combine(modulePath, "PowerShellEditorServices", "Start-EditorServices.ps1")); if (!File.Exists(scriptPath)) { throw new IOException(String.Format("Bad start script path: '{0}'", scriptPath)); } -#if CoreCLR Assembly assembly = this.GetType().GetTypeInfo().Assembly; -#else - Assembly assembly = this.GetType().Assembly; -#endif string assemblyPath = new Uri(assembly.CodeBase).LocalPath; FileVersionInfo fileVersionInfo = @@ -98,7 +96,7 @@ protected async Task> LaunchService( { StartInfo = new ProcessStartInfo { - FileName = "powershell.exe", + FileName = GetPwshExeName(), Arguments = string.Join(" ", args), CreateNoWindow = true, UseShellExecute = false, @@ -172,7 +170,7 @@ protected Task SendRequest SendRequest(NotificationType eventType, TParams eventParams) { return - this.messageSender.SendEvent( + this.messageSender.SendEventAsync( eventType, eventParams); } @@ -305,5 +303,14 @@ protected async Task>> WaitForRequest - - netcoreapp2.0;net452 + netcoreapp2.1;net461 Microsoft.PowerShell.EditorServices.Test.Protocol - + + + + + + + - - - - 10.0.3 - - - 6.0.0-alpha13 - - - + + + + - - + $(DefineConstants);CoreCLR - - - - - - diff --git a/test/PowerShellEditorServices.Test.Protocol/Server/OutputDebouncerTests.cs b/test/PowerShellEditorServices.Test.Protocol/Server/OutputDebouncerTests.cs index 50aad110e..fa70bd631 100644 --- a/test/PowerShellEditorServices.Test.Protocol/Server/OutputDebouncerTests.cs +++ b/test/PowerShellEditorServices.Test.Protocol/Server/OutputDebouncerTests.cs @@ -6,6 +6,7 @@ using Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter; using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; using Microsoft.PowerShell.EditorServices.Protocol.Server; +using Microsoft.PowerShell.EditorServices.Test.Shared; using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -39,7 +40,7 @@ public async Task OutputDebouncerAggregatesOutputEvents() // Assert that there's only one event with the expected string Assert.Equal(1, messageSender.OutputEvents.Count); Assert.Equal( - "This is a test\r\nAnother line", + TestUtilities.NormalizeNewlines("This is a test\nAnother line"), messageSender.OutputEvents[0].Output); // Wait for the next output to be flushed @@ -48,7 +49,7 @@ public async Task OutputDebouncerAggregatesOutputEvents() // Assert that there's only one event with the expected string Assert.Equal(2, messageSender.OutputEvents.Count); Assert.Equal( - "Another test line\r\nfor great justice", + TestUtilities.NormalizeNewlines("Another test line\nfor great justice"), messageSender.OutputEvents[1].Output); } @@ -76,8 +77,8 @@ public async Task OutputDebouncerDoesNotDuplicateOutput() // Ensure that the two events start with the correct lines Assert.Equal(2, messageSender.OutputEvents.Count); - Assert.Equal("Output 1", messageSender.OutputEvents[0].Output.Split('\r')[0]); - Assert.Equal("Output 26", messageSender.OutputEvents[1].Output.Split('\r')[0]); + Assert.Equal("Output 1", messageSender.OutputEvents[0].Output.Split('\n')[0].Trim('\r')); + Assert.Equal("Output 26", messageSender.OutputEvents[1].Output.Split('\n')[0].Trim('\r')); } private static Task SendOutput( @@ -85,7 +86,7 @@ private static Task SendOutput( string outputText, bool includeNewLine = false) { - return debouncer.Invoke( + return debouncer.InvokeAsync( new OutputWrittenEventArgs( outputText, includeNewLine, @@ -99,7 +100,7 @@ internal class TestMessageSender : IMessageSender { public List OutputEvents { get; } = new List(); - public Task SendEvent( + public Task SendEventAsync( NotificationType eventType, TParams eventParams) { @@ -113,7 +114,7 @@ public Task SendEvent( return Task.FromResult(true); } - public Task SendRequest( + public Task SendRequestAsync( RequestType requestType, TParams requestParams, bool waitForResponse) { @@ -121,7 +122,7 @@ public Task SendRequest throw new NotImplementedException(); } - public Task SendRequest(RequestType0 requestType0) + public Task SendRequestAsync(RequestType0 requestType0) { // Legitimately not implemented for these tests. throw new NotImplementedException(); diff --git a/test/PowerShellEditorServices.Test.Shared/Completion/CompleteAttributeValue.cs b/test/PowerShellEditorServices.Test.Shared/Completion/CompleteAttributeValue.cs index 1b5319ac2..43bf5bc86 100644 --- a/test/PowerShellEditorServices.Test.Shared/Completion/CompleteAttributeValue.cs +++ b/test/PowerShellEditorServices.Test.Shared/Completion/CompleteAttributeValue.cs @@ -7,10 +7,10 @@ namespace Microsoft.PowerShell.EditorServices.Test.Shared.Completion { public class CompleteAttributeValue { - public static readonly ScriptRegion SourceDetails = + public static readonly ScriptRegion SourceDetails = new ScriptRegion { - File = @"Completion\CompletionExamples.psm1", + File = TestUtilities.NormalizePath("Completion/CompletionExamples.psm1"), StartLineNumber = 16, StartColumnNumber = 38 }; diff --git a/test/PowerShellEditorServices.Test.Shared/Completion/CompleteCommandFromModule.cs b/test/PowerShellEditorServices.Test.Shared/Completion/CompleteCommandFromModule.cs index 3177dcbd5..5b03f9183 100644 --- a/test/PowerShellEditorServices.Test.Shared/Completion/CompleteCommandFromModule.cs +++ b/test/PowerShellEditorServices.Test.Shared/Completion/CompleteCommandFromModule.cs @@ -3,24 +3,31 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using System; using Microsoft.PowerShell.EditorServices; namespace Microsoft.PowerShell.EditorServices.Test.Shared.Completion { public class CompleteCommandFromModule { - public static readonly ScriptRegion SourceDetails = + private static readonly string[] s_getRandomParamSets = { + "Get-Random [[-Maximum] ] [-SetSeed ] [-Minimum ] []", + "Get-Random [-InputObject] [-SetSeed ] [-Count ] []" + }; + + public static readonly ScriptRegion SourceDetails = new ScriptRegion { - File = @"Completion\CompletionExamples.psm1", + File = TestUtilities.NormalizePath("Completion/CompletionExamples.psm1"), StartLineNumber = 13, - StartColumnNumber = 11 + StartColumnNumber = 8 }; public static readonly CompletionDetails ExpectedCompletion = CompletionDetails.Create( - "Install-Module", + "Get-Random", CompletionType.Command, - "Install-Module"); + string.Join(Environment.NewLine + Environment.NewLine, s_getRandomParamSets) + ); } } diff --git a/test/PowerShellEditorServices.Test.Shared/Completion/CompleteCommandInFile.cs b/test/PowerShellEditorServices.Test.Shared/Completion/CompleteCommandInFile.cs index 3c201cd9d..93bfb739d 100644 --- a/test/PowerShellEditorServices.Test.Shared/Completion/CompleteCommandInFile.cs +++ b/test/PowerShellEditorServices.Test.Shared/Completion/CompleteCommandInFile.cs @@ -9,10 +9,10 @@ namespace Microsoft.PowerShell.EditorServices.Test.Shared.Completion { public class CompleteCommandInFile { - public static readonly ScriptRegion SourceDetails = + public static readonly ScriptRegion SourceDetails = new ScriptRegion { - File = @"Completion\CompletionExamples.psm1", + File = TestUtilities.NormalizePath("Completion/CompletionExamples.psm1"), StartLineNumber = 8, StartColumnNumber = 7 }; diff --git a/test/PowerShellEditorServices.Test.Shared/Completion/CompleteFilePath.cs b/test/PowerShellEditorServices.Test.Shared/Completion/CompleteFilePath.cs index 4bc71b8bc..1d2a77863 100644 --- a/test/PowerShellEditorServices.Test.Shared/Completion/CompleteFilePath.cs +++ b/test/PowerShellEditorServices.Test.Shared/Completion/CompleteFilePath.cs @@ -13,12 +13,12 @@ namespace Microsoft.PowerShell.EditorServices.Test.Shared.Completion { public class CompleteFilePath { - public static readonly ScriptRegion SourceDetails = + public static readonly ScriptRegion SourceDetails = new ScriptRegion { - File = @"Completion\CompletionExamples.psm1", + File = TestUtilities.NormalizePath("Completion/CompletionExamples.psm1"), StartLineNumber = 19, - StartColumnNumber = 25 + StartColumnNumber = 15 }; public static readonly BufferRange ExpectedRange = diff --git a/test/PowerShellEditorServices.Test.Shared/Completion/CompleteVariableInFile.cs b/test/PowerShellEditorServices.Test.Shared/Completion/CompleteVariableInFile.cs index 9a728fa69..3a7bd4b40 100644 --- a/test/PowerShellEditorServices.Test.Shared/Completion/CompleteVariableInFile.cs +++ b/test/PowerShellEditorServices.Test.Shared/Completion/CompleteVariableInFile.cs @@ -9,10 +9,10 @@ namespace Microsoft.PowerShell.EditorServices.Test.Shared.Completion { public class CompleteVariableInFile { - public static readonly ScriptRegion SourceDetails = + public static readonly ScriptRegion SourceDetails = new ScriptRegion { - File = @"Completion\CompletionExamples.psm1", + File = TestUtilities.NormalizePath("Completion/CompletionExamples.psm1"), StartLineNumber = 10, StartColumnNumber = 9 }; diff --git a/test/PowerShellEditorServices.Test.Shared/Completion/CompletionExamples.psm1 b/test/PowerShellEditorServices.Test.Shared/Completion/CompletionExamples.psm1 index f34690cca..ee3dd9098 100644 --- a/test/PowerShellEditorServices.Test.Shared/Completion/CompletionExamples.psm1 +++ b/test/PowerShellEditorServices.Test.Shared/Completion/CompletionExamples.psm1 @@ -10,10 +10,10 @@ Get-So $testVar Import-Module PowerShellGet -Install-Mo +Get-Rand function Test-Completion { param([Parameter(Mandatory, Value)]) } -Get-ChildItem c:\Program +Get-ChildItem / diff --git a/test/PowerShellEditorServices.Test.Shared/Debugging/VariableTest.ps1 b/test/PowerShellEditorServices.Test.Shared/Debugging/VariableTest.ps1 index 7398d8c01..fc6896990 100644 --- a/test/PowerShellEditorServices.Test.Shared/Debugging/VariableTest.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Debugging/VariableTest.ps1 @@ -18,4 +18,4 @@ function Test-Variables { Write-Output "Done" } -Test-Variables \ No newline at end of file +Test-Variables diff --git a/test/PowerShellEditorServices.Test.Shared/Definition/FindsDotSourcedFile.cs b/test/PowerShellEditorServices.Test.Shared/Definition/FindsDotSourcedFile.cs index 20160081a..8147334c8 100644 --- a/test/PowerShellEditorServices.Test.Shared/Definition/FindsDotSourcedFile.cs +++ b/test/PowerShellEditorServices.Test.Shared/Definition/FindsDotSourcedFile.cs @@ -12,7 +12,7 @@ public class FindsDotSourcedFile public static readonly ScriptRegion SourceDetails = new ScriptRegion { - File = @"References\DotSources.ps1", + File = TestUtilities.NormalizeNewlines("References/DotSources.ps1"), StartLineNumber = 1, StartColumnNumber = 3 }; diff --git a/test/PowerShellEditorServices.Test.Shared/Definition/FindsFunctionDefinition.cs b/test/PowerShellEditorServices.Test.Shared/Definition/FindsFunctionDefinition.cs index 164d5798c..387e8e58b 100644 --- a/test/PowerShellEditorServices.Test.Shared/Definition/FindsFunctionDefinition.cs +++ b/test/PowerShellEditorServices.Test.Shared/Definition/FindsFunctionDefinition.cs @@ -10,7 +10,7 @@ public class FindsFunctionDefinition public static readonly ScriptRegion SourceDetails = new ScriptRegion { - File = @"References\SimpleFile.ps1", + File = TestUtilities.NormalizePath("References/SimpleFile.ps1"), StartLineNumber = 3, StartColumnNumber = 12 }; diff --git a/test/PowerShellEditorServices.Test.Shared/Definition/FindsFunctionDefinitionInDotSourceReference.cs b/test/PowerShellEditorServices.Test.Shared/Definition/FindsFunctionDefinitionInDotSourceReference.cs index bba3f2b27..e8fb1f3b5 100644 --- a/test/PowerShellEditorServices.Test.Shared/Definition/FindsFunctionDefinitionInDotSourceReference.cs +++ b/test/PowerShellEditorServices.Test.Shared/Definition/FindsFunctionDefinitionInDotSourceReference.cs @@ -12,7 +12,7 @@ public class FindsFunctionDefinitionInDotSourceReference public static readonly ScriptRegion SourceDetails = new ScriptRegion { - File = @"References\FileWithReferences.ps1", + File = TestUtilities.NormalizePath("References/FileWithReferences.ps1"), StartLineNumber = 3, StartColumnNumber = 6 }; diff --git a/test/PowerShellEditorServices.Test.Shared/Definition/FindsFunctionDefinitionInWorkspace.cs b/test/PowerShellEditorServices.Test.Shared/Definition/FindsFunctionDefinitionInWorkspace.cs index b32a6159f..1c48cfefe 100644 --- a/test/PowerShellEditorServices.Test.Shared/Definition/FindsFunctionDefinitionInWorkspace.cs +++ b/test/PowerShellEditorServices.Test.Shared/Definition/FindsFunctionDefinitionInWorkspace.cs @@ -12,7 +12,7 @@ public class FindsFunctionDefinitionInWorkspace public static readonly ScriptRegion SourceDetails = new ScriptRegion { - File = @"References\ReferenceFileD.ps1", + File = TestUtilities.NormalizePath("References/ReferenceFileD.ps1"), StartLineNumber = 1, StartColumnNumber = 2 }; diff --git a/test/PowerShellEditorServices.Test.Shared/Definition/FindsVariableDefinition.cs b/test/PowerShellEditorServices.Test.Shared/Definition/FindsVariableDefinition.cs index 6dab3ddad..1c785be21 100644 --- a/test/PowerShellEditorServices.Test.Shared/Definition/FindsVariableDefinition.cs +++ b/test/PowerShellEditorServices.Test.Shared/Definition/FindsVariableDefinition.cs @@ -16,7 +16,7 @@ public class FindsVariableDefinition public static readonly ScriptRegion SourceDetails = new ScriptRegion { - File = @"References\SimpleFile.ps1", + File = TestUtilities.NormalizePath("References/SimpleFile.ps1"), StartLineNumber = 8, StartColumnNumber = 3 }; diff --git a/test/PowerShellEditorServices.Test.Shared/Occurrences/FindOccurrencesOnParameter.cs b/test/PowerShellEditorServices.Test.Shared/Occurrences/FindOccurrencesOnParameter.cs index 62fc82716..7b3c368b7 100644 --- a/test/PowerShellEditorServices.Test.Shared/Occurrences/FindOccurrencesOnParameter.cs +++ b/test/PowerShellEditorServices.Test.Shared/Occurrences/FindOccurrencesOnParameter.cs @@ -10,7 +10,7 @@ public class FindOccurrencesOnParameter public static readonly ScriptRegion SourceDetails = new ScriptRegion { - File = @"References\SimpleFile.ps1", + File = TestUtilities.NormalizePath("References/SimpleFile.ps1"), StartLineNumber = 1, StartColumnNumber = 31 }; diff --git a/test/PowerShellEditorServices.Test.Shared/Occurrences/FindsOccurrencesOnFunction.cs b/test/PowerShellEditorServices.Test.Shared/Occurrences/FindsOccurrencesOnFunction.cs index 2b8258410..72c2adf95 100644 --- a/test/PowerShellEditorServices.Test.Shared/Occurrences/FindsOccurrencesOnFunction.cs +++ b/test/PowerShellEditorServices.Test.Shared/Occurrences/FindsOccurrencesOnFunction.cs @@ -12,7 +12,7 @@ public class FindsOccurrencesOnFunction public static readonly ScriptRegion SourceDetails = new ScriptRegion { - File = @"References\SimpleFile.ps1", + File = TestUtilities.NormalizePath("References/SimpleFile.ps1"), StartLineNumber = 1, StartColumnNumber = 17 }; diff --git a/test/PowerShellEditorServices.Test.Shared/ParameterHints/FindsParameterSetsOnCommand.cs b/test/PowerShellEditorServices.Test.Shared/ParameterHints/FindsParameterSetsOnCommand.cs index fcb4e88f0..797d93bd1 100644 --- a/test/PowerShellEditorServices.Test.Shared/ParameterHints/FindsParameterSetsOnCommand.cs +++ b/test/PowerShellEditorServices.Test.Shared/ParameterHints/FindsParameterSetsOnCommand.cs @@ -12,7 +12,7 @@ public class FindsParameterSetsOnCommand public static readonly ScriptRegion SourceDetails = new ScriptRegion { - File = @"ParameterHints\ParamHints.ps1", + File = TestUtilities.NormalizePath("ParameterHints/ParamHints.ps1"), StartLineNumber = 1, StartColumnNumber = 14 }; diff --git a/test/PowerShellEditorServices.Test.Shared/ParameterHints/FindsParameterSetsOnCommandWithSpaces.cs b/test/PowerShellEditorServices.Test.Shared/ParameterHints/FindsParameterSetsOnCommandWithSpaces.cs index 920530ec3..e7d40489e 100644 --- a/test/PowerShellEditorServices.Test.Shared/ParameterHints/FindsParameterSetsOnCommandWithSpaces.cs +++ b/test/PowerShellEditorServices.Test.Shared/ParameterHints/FindsParameterSetsOnCommandWithSpaces.cs @@ -10,7 +10,7 @@ public class FindsParameterSetsOnCommandWithSpaces public static readonly ScriptRegion SourceDetails = new ScriptRegion { - File = @"ParameterHints\ParamHints.ps1", + File = TestUtilities.NormalizePath("ParameterHints/ParamHints.ps1"), StartLineNumber = 9, StartColumnNumber = 31 }; diff --git a/test/PowerShellEditorServices.Test.Shared/PowerShellEditorServices.Test.Shared.csproj b/test/PowerShellEditorServices.Test.Shared/PowerShellEditorServices.Test.Shared.csproj index cee19e4f2..84bf41ab8 100644 --- a/test/PowerShellEditorServices.Test.Shared/PowerShellEditorServices.Test.Shared.csproj +++ b/test/PowerShellEditorServices.Test.Shared/PowerShellEditorServices.Test.Shared.csproj @@ -1,22 +1,15 @@  - 0.9.0-beta - netstandard1.6;net452 + netstandard2.0 Microsoft.PowerShell.EditorServices.Test.Shared - - + - - - $(DefineConstants);CoreCLR + + true + true - - - - - diff --git a/test/PowerShellEditorServices.Test.Shared/References/FileWithReferences.ps1 b/test/PowerShellEditorServices.Test.Shared/References/FileWithReferences.ps1 index fb39070e8..7a4e4cfe6 100644 --- a/test/PowerShellEditorServices.Test.Shared/References/FileWithReferences.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/References/FileWithReferences.ps1 @@ -1,3 +1,3 @@ -. .\SimpleFile.ps1 +. ./SimpleFile.ps1 My-Function "test" diff --git a/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnBuiltInCommandWithAlias.cs b/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnBuiltInCommandWithAlias.cs index 2a22b13f2..c06e2452c 100644 --- a/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnBuiltInCommandWithAlias.cs +++ b/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnBuiltInCommandWithAlias.cs @@ -10,7 +10,7 @@ public class FindsReferencesOnBuiltInCommandWithAlias public static readonly ScriptRegion SourceDetails = new ScriptRegion { - File = @"References\SimpleFile.ps1", + File = TestUtilities.NormalizePath("References/SimpleFile.ps1"), StartLineNumber = 14, StartColumnNumber = 3 }; @@ -20,7 +20,7 @@ public class FindsReferencesOnBuiltInAlias public static readonly ScriptRegion SourceDetails = new ScriptRegion { - File = @"References\SimpleFile.ps1", + File = TestUtilities.NormalizePath("References/SimpleFile.ps1"), StartLineNumber = 15, StartColumnNumber = 2 }; diff --git a/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnFunction.cs b/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnFunction.cs index 6a53d69f2..28c3249fd 100644 --- a/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnFunction.cs +++ b/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnFunction.cs @@ -10,7 +10,7 @@ public class FindsReferencesOnFunction public static readonly ScriptRegion SourceDetails = new ScriptRegion { - File = @"References\SimpleFile.ps1", + File = TestUtilities.NormalizePath("References/SimpleFile.ps1"), StartLineNumber = 3, StartColumnNumber = 8 }; diff --git a/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnFunctionMultiFileDotSource.cs b/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnFunctionMultiFileDotSource.cs index 5624edf02..872df6ff1 100644 --- a/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnFunctionMultiFileDotSource.cs +++ b/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnFunctionMultiFileDotSource.cs @@ -12,7 +12,7 @@ public class FindsReferencesOnFunctionMultiFileDotSourceFileB public static readonly ScriptRegion SourceDetails = new ScriptRegion { - File = @"References\ReferenceFileB.ps1", + File = TestUtilities.NormalizePath("References/ReferenceFileB.ps1"), StartLineNumber = 5, StartColumnNumber = 8 }; @@ -22,7 +22,7 @@ public class FindsReferencesOnFunctionMultiFileDotSourceFileC public static readonly ScriptRegion SourceDetails = new ScriptRegion { - File = @"References\ReferenceFileC.ps1", + File = TestUtilities.NormalizePath("References/ReferenceFileC.ps1"), StartLineNumber = 4, StartColumnNumber = 10 }; diff --git a/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesonVariable.cs b/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesonVariable.cs index b5ae61af4..1860e56d9 100644 --- a/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesonVariable.cs +++ b/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesonVariable.cs @@ -12,7 +12,7 @@ public class FindsReferencesOnVariable public static readonly ScriptRegion SourceDetails = new ScriptRegion { - File = @"References\SimpleFile.ps1", + File = TestUtilities.NormalizePath("References/SimpleFile.ps1"), StartLineNumber = 10, StartColumnNumber = 17 }; diff --git a/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileA.ps1 b/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileA.ps1 index a43f59fb2..31ef35600 100644 --- a/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileA.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileA.ps1 @@ -1,9 +1,9 @@ . .\ReferenceFileA.ps1 -. .\ReferenceFileB.ps1 +. ./ReferenceFileB.ps1 . .\ReferenceFileC.ps1 function My-Function ($myInput) { My-Function $myInput } -Get-ChildItem \ No newline at end of file +Get-ChildItem diff --git a/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileC.ps1 b/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileC.ps1 index e17fd096f..6e1ee3131 100644 --- a/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileC.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileC.ps1 @@ -1,4 +1,4 @@ -. .\ReferenceFileA.ps1 +. ./ReferenceFileA.ps1 Get-ChildItem -My-Function "testc" \ No newline at end of file +My-Function "testc" diff --git a/test/PowerShellEditorServices.Test.Shared/References/SimpleFile.ps1 b/test/PowerShellEditorServices.Test.Shared/References/SimpleFile.ps1 index 7e9022cf2..527045b1e 100644 --- a/test/PowerShellEditorServices.Test.Shared/References/SimpleFile.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/References/SimpleFile.ps1 @@ -12,9 +12,7 @@ My-Function $things Write-Output "Hello World"; Get-ChildItem -ls gci dir -LS Write-Host -Get-ChildItem \ No newline at end of file +Get-ChildItem diff --git a/test/PowerShellEditorServices.Test.Shared/SymbolDetails/FindsDetailsForBuiltInCommand.cs b/test/PowerShellEditorServices.Test.Shared/SymbolDetails/FindsDetailsForBuiltInCommand.cs index c3526938b..d46206bcb 100644 --- a/test/PowerShellEditorServices.Test.Shared/SymbolDetails/FindsDetailsForBuiltInCommand.cs +++ b/test/PowerShellEditorServices.Test.Shared/SymbolDetails/FindsDetailsForBuiltInCommand.cs @@ -10,7 +10,7 @@ public class FindsDetailsForBuiltInCommand public static readonly ScriptRegion SourceDetails = new ScriptRegion { - File = @"SymbolDetails\SymbolDetails.ps1", + File = TestUtilities.NormalizePath("SymbolDetails/SymbolDetails.ps1"), StartLineNumber = 1, StartColumnNumber = 10 }; diff --git a/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInMultiSymbolFile.cs b/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInMultiSymbolFile.cs index 7f3dc68e8..ab3ce87b3 100644 --- a/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInMultiSymbolFile.cs +++ b/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInMultiSymbolFile.cs @@ -9,7 +9,7 @@ public class FindSymbolsInMultiSymbolFile { public static readonly ScriptRegion SourceDetails = new ScriptRegion { - File = @"Symbols\MultipleSymbols.ps1" + File = TestUtilities.NormalizePath("Symbols/MultipleSymbols.ps1") }; } } diff --git a/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInNoSymbolsFile.cs b/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInNoSymbolsFile.cs index cf1692030..c8c814ca0 100644 --- a/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInNoSymbolsFile.cs +++ b/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInNoSymbolsFile.cs @@ -9,7 +9,7 @@ public class FindSymbolsInNoSymbolsFile { public static readonly ScriptRegion SourceDetails = new ScriptRegion { - File = @"Symbols\NoSymbols.ps1" + File = TestUtilities.NormalizePath("Symbols/NoSymbols.ps1") }; } } diff --git a/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInPSDFile.cs b/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInPSDFile.cs index 8d96f49d6..6343b79cc 100644 --- a/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInPSDFile.cs +++ b/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInPSDFile.cs @@ -10,7 +10,7 @@ public class FindSymbolsInPSDFile public static readonly ScriptRegion SourceDetails = new ScriptRegion { - File = @"Symbols\PowerShellDataFile.psd1" + File = TestUtilities.NormalizePath("Symbols/PowerShellDataFile.psd1") }; } } diff --git a/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInPesterFile.cs b/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInPesterFile.cs index cebcae3aa..19a7c6519 100644 --- a/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInPesterFile.cs +++ b/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInPesterFile.cs @@ -10,7 +10,7 @@ public class FindSymbolsInPesterFile public static readonly ScriptRegion SourceDetails = new ScriptRegion { - File = @"Symbols\PesterFile.tests.ps1" + File = TestUtilities.NormalizePath("Symbols/PesterFile.tests.ps1") }; } } diff --git a/test/PowerShellEditorServices.Test.Shared/TestUtilities/TestUtilities.cs b/test/PowerShellEditorServices.Test.Shared/TestUtilities/TestUtilities.cs new file mode 100644 index 000000000..dd6cc1211 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/TestUtilities/TestUtilities.cs @@ -0,0 +1,90 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Microsoft.PowerShell.EditorServices.Test.Shared +{ + /// + /// Convenience class to simplify cross-platform testing + /// + public static class TestUtilities + { + private static readonly char[] s_unixPathSeparators = new [] { '/' }; + + private static readonly char[] s_unixNewlines = new [] { '\n' }; + + /// + /// Takes a UNIX-style path and converts it to the path appropriate to the platform. + /// + /// A forward-slash separated path. + /// A path with directories separated by the appropriate separator. + public static string NormalizePath(string unixPath) + { + if (unixPath == null) + { + return unixPath; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return unixPath.Replace('/', Path.DirectorySeparatorChar); + } + + return unixPath; + } + + /// + /// Take a string with UNIX newlines and replaces them with platform-appropriate newlines. + /// + /// The string with UNIX-style newlines. + /// The platform-newline-normalized string. + public static string NormalizeNewlines(string unixString) + { + if (unixString == null) + { + return unixString; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return String.Join(Environment.NewLine, unixString.Split(s_unixNewlines)); + } + + return unixString; + } + + /// + /// Platform-normalize a string -- takes a UNIX-style string and gives it platform-appropriate newlines and path separators. + /// + /// The string to normalize for the platform, given with UNIX-specific separators. + /// The same string but separated by platform-appropriate directory and newline separators. + public static string PlatformNormalize(string unixString) + { + return NormalizeNewlines(NormalizePath(unixString)); + } + + /// + /// Not for use in production -- convenience code for debugging tests. + /// + public static void AWAIT_DEBUGGER_HERE( + [CallerMemberName] string callerName = null, + [CallerFilePath] string callerPath = null, + [CallerLineNumber] int callerLine = -1) + { + if (Debugger.IsAttached) + { + return; + } + + System.Console.WriteLine(); + System.Console.WriteLine("===== AWAITING DEBUGGER ====="); + System.Console.WriteLine($" PID: {Process.GetCurrentProcess().Id}"); + System.Console.WriteLine($" Waiting at {callerPath} line {callerLine} ({callerName})"); + System.Console.WriteLine(" PRESS ANY KEY TO CONTINUE"); + System.Console.WriteLine("============================="); + System.Console.ReadKey(); + } + } +} diff --git a/test/PowerShellEditorServices.Test/Console/ChoicePromptHandlerTests.cs b/test/PowerShellEditorServices.Test/Console/ChoicePromptHandlerTests.cs index 2fba1ea5e..55f8b60e9 100644 --- a/test/PowerShellEditorServices.Test/Console/ChoicePromptHandlerTests.cs +++ b/test/PowerShellEditorServices.Test/Console/ChoicePromptHandlerTests.cs @@ -28,7 +28,7 @@ public void ChoicePromptReturnsCorrectIdForChoice() { TestChoicePromptHandler choicePromptHandler = new TestChoicePromptHandler(); Task promptTask = - choicePromptHandler.PromptForChoice( + choicePromptHandler.PromptForChoiceAsync( "Test prompt", "Message is irrelevant", Choices, @@ -50,7 +50,7 @@ public void ChoicePromptReturnsCorrectIdForHotKey() { TestChoicePromptHandler choicePromptHandler = new TestChoicePromptHandler(); Task promptTask = - choicePromptHandler.PromptForChoice( + choicePromptHandler.PromptForChoiceAsync( "Test prompt", "Message is irrelevant", Choices, @@ -75,7 +75,7 @@ public void ChoicePromptRepromptsOnInvalidInput() new TestChoicePromptHandler(); Task promptTask = - choicePromptHandler.PromptForChoice( + choicePromptHandler.PromptForChoiceAsync( "Test prompt", "Message is irrelevant", Choices, @@ -105,7 +105,7 @@ public void ReturnInputString(string inputString) this.linePromptTask.SetResult(inputString); } - protected override Task ReadInputString(CancellationToken cancellationToken) + protected override Task ReadInputStringAsync(CancellationToken cancellationToken) { this.linePromptTask = new TaskCompletionSource(); return this.linePromptTask.Task; diff --git a/test/PowerShellEditorServices.Test/Console/InputPromptHandlerTests.cs b/test/PowerShellEditorServices.Test/Console/InputPromptHandlerTests.cs index 04bed7314..689ae1c8b 100644 --- a/test/PowerShellEditorServices.Test/Console/InputPromptHandlerTests.cs +++ b/test/PowerShellEditorServices.Test/Console/InputPromptHandlerTests.cs @@ -39,7 +39,7 @@ public void InputPromptHandlerReturnsValuesOfCorrectType() { TestInputPromptHandler inputPromptHandler = new TestInputPromptHandler(); Task> promptTask = - inputPromptHandler.PromptForInput( + inputPromptHandler.PromptForInputAsync( "Test Prompt", "Message is irrelevant", Fields, @@ -71,7 +71,7 @@ public void InputPromptHandlerAcceptsArrayOfNonStringValues() { TestInputPromptHandler inputPromptHandler = new TestInputPromptHandler(); Task> promptTask = - inputPromptHandler.PromptForInput( + inputPromptHandler.PromptForInputAsync( "Test Prompt", "Message is irrelevant", new FieldDetails[] @@ -95,7 +95,7 @@ public void InputPromptRetriesWhenCannotCastValue() { TestInputPromptHandler inputPromptHandler = new TestInputPromptHandler(); Task> promptTask = - inputPromptHandler.PromptForInput( + inputPromptHandler.PromptForInputAsync( "Test Prompt", "Message is irrelevant", Fields, @@ -146,13 +146,13 @@ public void ReturnSecureString(SecureString secureString) this.securePromptTask.SetResult(secureString); } - protected override Task ReadInputString(CancellationToken cancellationToken) + protected override Task ReadInputStringAsync(CancellationToken cancellationToken) { this.linePromptTask = new TaskCompletionSource(); return this.linePromptTask.Task; } - protected override Task ReadSecureString(CancellationToken cancellationToken) + protected override Task ReadSecureStringAsync(CancellationToken cancellationToken) { this.securePromptTask = new TaskCompletionSource(); return this.securePromptTask.Task; diff --git a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs index 18ce3cf30..da956a640 100644 --- a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs @@ -5,6 +5,7 @@ using Microsoft.PowerShell.EditorServices.Debugging; using Microsoft.PowerShell.EditorServices.Utility; +using Microsoft.PowerShell.EditorServices.Test.Shared; using System; using System.Collections.Generic; using System.Linq; @@ -41,11 +42,11 @@ public DebugServiceTests() // Load the test debug file this.debugScriptFile = this.workspace.GetFile( - @"..\..\..\..\PowerShellEditorServices.Test.Shared\Debugging\DebugTest.ps1"); + TestUtilities.NormalizePath("../../../../PowerShellEditorServices.Test.Shared/Debugging/VariableTest.ps1")); this.variableScriptFile = this.workspace.GetFile( - @"..\..\..\..\PowerShellEditorServices.Test.Shared\Debugging\VariableTest.ps1"); + TestUtilities.NormalizePath("../../../../PowerShellEditorServices.Test.Shared/Debugging/VariableTest.ps1")); this.debugService = new DebugService(this.powerShellContext, logger); this.debugService.DebuggerStopped += debugService_DebuggerStopped; @@ -55,7 +56,7 @@ public DebugServiceTests() // Load the test debug file this.debugScriptFile = this.workspace.GetFile( - @"..\..\..\..\PowerShellEditorServices.Test.Shared\Debugging\DebugTest.ps1"); + TestUtilities.NormalizePath("../../../../PowerShellEditorServices.Test.Shared/Debugging/DebugTest.ps1")); } async void powerShellContext_SessionStateChanged(object sender, SessionStateChangedEventArgs e) @@ -72,9 +73,12 @@ void debugService_BreakpointUpdated(object sender, BreakpointUpdatedEventArgs e) // TODO: Needed? } - async void debugService_DebuggerStopped(object sender, DebuggerStoppedEventArgs e) + void debugService_DebuggerStopped(object sender, DebuggerStoppedEventArgs e) { - await this.debuggerStoppedQueue.EnqueueAsync(e); + // We need to ensure this is run on a different thread than the one it's + // called on because it can cause PowerShellContext.OnDebuggerStopped to + // never hit the while loop. + Task.Run(() => this.debuggerStoppedQueue.Enqueue(e)); } public void Dispose() @@ -105,9 +109,9 @@ public async Task DebuggerAcceptsScriptArgs(string[] args) // it should not escape already escaped chars. ScriptFile debugWithParamsFile = this.workspace.GetFile( - @"..\..\..\..\PowerShellEditorServices.Test.Shared\Debugging\Debug` W&ith Params `[Test].ps1"); + TestUtilities.NormalizePath("../../../../PowerShellEditorServices.Test.Shared/Debugging/Debug` W&ith Params `[Test].ps1")); - await this.debugService.SetLineBreakpoints( + await this.debugService.SetLineBreakpointsAsync( debugWithParamsFile, new[] { BreakpointDetails.Create("", 3) }); @@ -115,7 +119,7 @@ await this.debugService.SetLineBreakpoints( // Execute the script and wait for the breakpoint to be hit Task executeTask = - this.powerShellContext.ExecuteScriptWithArgs( + this.powerShellContext.ExecuteScriptWithArgsAsync( debugWithParamsFile.FilePath, arguments); await this.AssertDebuggerStopped(debugWithParamsFile.FilePath); @@ -160,7 +164,7 @@ await this.debugService.SetLineBreakpoints( public async Task DebuggerSetsAndClearsFunctionBreakpoints() { CommandBreakpointDetails[] breakpoints = - await this.debugService.SetCommandBreakpoints( + await this.debugService.SetCommandBreakpointsAsync( new[] { CommandBreakpointDetails.Create("Write-Host"), CommandBreakpointDetails.Create("Get-Date") @@ -171,14 +175,14 @@ await this.debugService.SetCommandBreakpoints( Assert.Equal("Get-Date", breakpoints[1].Name); breakpoints = - await this.debugService.SetCommandBreakpoints( + await this.debugService.SetCommandBreakpointsAsync( new[] { CommandBreakpointDetails.Create("Get-Host") }); Assert.Equal(1, breakpoints.Length); Assert.Equal("Get-Host", breakpoints[0].Name); breakpoints = - await this.debugService.SetCommandBreakpoints( + await this.debugService.SetCommandBreakpointsAsync( new CommandBreakpointDetails[] {}); Assert.Equal(0, breakpoints.Length); @@ -188,7 +192,7 @@ await this.debugService.SetCommandBreakpoints( public async Task DebuggerStopsOnFunctionBreakpoints() { CommandBreakpointDetails[] breakpoints = - await this.debugService.SetCommandBreakpoints( + await this.debugService.SetCommandBreakpointsAsync( new[] { CommandBreakpointDetails.Create("Write-Host") }); @@ -196,7 +200,7 @@ await this.debugService.SetCommandBreakpoints( await this.AssertStateChange(PowerShellContextState.Ready); Task executeTask = - this.powerShellContext.ExecuteScriptWithArgs( + this.powerShellContext.ExecuteScriptWithArgsAsync( this.debugScriptFile.FilePath); // Wait for function breakpoint to hit @@ -234,7 +238,7 @@ await this.debugService.SetCommandBreakpoints( public async Task DebuggerSetsAndClearsLineBreakpoints() { BreakpointDetails[] breakpoints = - await this.debugService.SetLineBreakpoints( + await this.debugService.SetLineBreakpointsAsync( this.debugScriptFile, new[] { BreakpointDetails.Create("", 5), @@ -248,7 +252,7 @@ await this.debugService.SetLineBreakpoints( Assert.Equal(10, breakpoints[1].LineNumber); breakpoints = - await this.debugService.SetLineBreakpoints( + await this.debugService.SetLineBreakpointsAsync( this.debugScriptFile, new[] { BreakpointDetails.Create("", 2) }); @@ -257,7 +261,7 @@ await this.debugService.SetLineBreakpoints( Assert.Equal(1, confirmedBreakpoints.Count()); Assert.Equal(2, breakpoints[0].LineNumber); - await this.debugService.SetLineBreakpoints( + await this.debugService.SetLineBreakpointsAsync( this.debugScriptFile, new[] { BreakpointDetails.Create("", 0) }); @@ -272,7 +276,7 @@ await this.debugService.SetLineBreakpoints( public async Task DebuggerStopsOnLineBreakpoints() { BreakpointDetails[] breakpoints = - await this.debugService.SetLineBreakpoints( + await this.debugService.SetLineBreakpointsAsync( this.debugScriptFile, new[] { BreakpointDetails.Create("", 5), @@ -282,7 +286,7 @@ await this.debugService.SetLineBreakpoints( await this.AssertStateChange(PowerShellContextState.Ready); Task executeTask = - this.powerShellContext.ExecuteScriptWithArgs( + this.powerShellContext.ExecuteScriptWithArgsAsync( this.debugScriptFile.FilePath); // Wait for a couple breakpoints @@ -303,7 +307,7 @@ public async Task DebuggerStopsOnConditionalBreakpoints() const int breakpointValue2 = 20; BreakpointDetails[] breakpoints = - await this.debugService.SetLineBreakpoints( + await this.debugService.SetLineBreakpointsAsync( this.debugScriptFile, new[] { BreakpointDetails.Create("", 7, null, $"$i -eq {breakpointValue1} -or $i -eq {breakpointValue2}"), @@ -312,7 +316,7 @@ await this.debugService.SetLineBreakpoints( await this.AssertStateChange(PowerShellContextState.Ready); Task executeTask = - this.powerShellContext.ExecuteScriptWithArgs( + this.powerShellContext.ExecuteScriptWithArgsAsync( this.debugScriptFile.FilePath); // Wait for conditional breakpoint to hit @@ -353,7 +357,7 @@ public async Task DebuggerStopsOnHitConditionBreakpoint() const int hitCount = 5; BreakpointDetails[] breakpoints = - await this.debugService.SetLineBreakpoints( + await this.debugService.SetLineBreakpointsAsync( this.debugScriptFile, new[] { BreakpointDetails.Create("", 6, null, null, $"{hitCount}"), @@ -362,7 +366,7 @@ await this.debugService.SetLineBreakpoints( await this.AssertStateChange(PowerShellContextState.Ready); Task executeTask = - this.powerShellContext.ExecuteScriptWithArgs( + this.powerShellContext.ExecuteScriptWithArgsAsync( this.debugScriptFile.FilePath); // Wait for conditional breakpoint to hit @@ -389,7 +393,7 @@ public async Task DebuggerStopsOnConditionalAndHitConditionBreakpoint() const int hitCount = 5; BreakpointDetails[] breakpoints = - await this.debugService.SetLineBreakpoints( + await this.debugService.SetLineBreakpointsAsync( this.debugScriptFile, new[] { BreakpointDetails.Create("", 6, null, $"$i % 2 -eq 0", $"{hitCount}"), @@ -398,7 +402,7 @@ await this.debugService.SetLineBreakpoints( await this.AssertStateChange(PowerShellContextState.Ready); Task executeTask = - this.powerShellContext.ExecuteScriptWithArgs( + this.powerShellContext.ExecuteScriptWithArgsAsync( this.debugScriptFile.FilePath); // Wait for conditional breakpoint to hit @@ -424,7 +428,7 @@ await this.debugService.SetLineBreakpoints( public async Task DebuggerProvidesMessageForInvalidConditionalBreakpoint() { BreakpointDetails[] breakpoints = - await this.debugService.SetLineBreakpoints( + await this.debugService.SetLineBreakpointsAsync( this.debugScriptFile, new[] { BreakpointDetails.Create("", 5), @@ -446,7 +450,7 @@ await this.debugService.SetLineBreakpoints( public async Task DebuggerFindsParseableButInvalidSimpleBreakpointConditions() { BreakpointDetails[] breakpoints = - await this.debugService.SetLineBreakpoints( + await this.debugService.SetLineBreakpointsAsync( this.debugScriptFile, new[] { BreakpointDetails.Create("", 5, column: null, condition: "$i == 100"), @@ -478,7 +482,7 @@ await this.AssertStateChange( "Unexpected breakpoint found in script file"); Task executeTask = - this.powerShellContext.ExecuteScriptString( + this.powerShellContext.ExecuteScriptStringAsync( this.debugScriptFile.FilePath); // Break execution and wait for the debugger to stop @@ -491,16 +495,17 @@ await this.AssertStateChange( // Abort execution and wait for the debugger to exit this.debugService.Abort(); + await this.AssertStateChange( PowerShellContextState.Ready, - PowerShellExecutionResult.Aborted); + PowerShellExecutionResult.Stopped); } [Fact] public async Task DebuggerRunsCommandsWhileStopped() { Task executeTask = - this.powerShellContext.ExecuteScriptString( + this.powerShellContext.ExecuteScriptStringAsync( this.debugScriptFile.FilePath); // Break execution and wait for the debugger to stop @@ -510,25 +515,26 @@ await this.AssertStateChange( PowerShellExecutionResult.Stopped); // Try running a command from outside the pipeline thread - await this.powerShellContext.ExecuteScriptString("Get-Command Get-Process"); + await this.powerShellContext.ExecuteScriptStringAsync("Get-Command Get-Process"); // Abort execution and wait for the debugger to exit this.debugService.Abort(); + await this.AssertStateChange( PowerShellContextState.Ready, - PowerShellExecutionResult.Aborted); + PowerShellExecutionResult.Stopped); } [Fact] public async Task DebuggerVariableStringDisplaysCorrectly() { - await this.debugService.SetLineBreakpoints( + await this.debugService.SetLineBreakpointsAsync( this.variableScriptFile, new[] { BreakpointDetails.Create("", 18) }); // Execute the script and wait for the breakpoint to be hit Task executeTask = - this.powerShellContext.ExecuteScriptString( + this.powerShellContext.ExecuteScriptStringAsync( this.variableScriptFile.FilePath); await this.AssertDebuggerStopped(this.variableScriptFile.FilePath); @@ -550,13 +556,13 @@ await this.debugService.SetLineBreakpoints( [Fact] public async Task DebuggerGetsVariables() { - await this.debugService.SetLineBreakpoints( + await this.debugService.SetLineBreakpointsAsync( this.variableScriptFile, new[] { BreakpointDetails.Create("", 14) }); // Execute the script and wait for the breakpoint to be hit Task executeTask = - this.powerShellContext.ExecuteScriptString( + this.powerShellContext.ExecuteScriptStringAsync( this.variableScriptFile.FilePath); await this.AssertDebuggerStopped(this.variableScriptFile.FilePath); @@ -600,13 +606,13 @@ await this.debugService.SetLineBreakpoints( [Fact] public async Task DebuggerSetsVariablesNoConversion() { - await this.debugService.SetLineBreakpoints( + await this.debugService.SetLineBreakpointsAsync( this.variableScriptFile, new[] { BreakpointDetails.Create("", 14) }); // Execute the script and wait for the breakpoint to be hit Task executeTask = - this.powerShellContext.ExecuteScriptString( + this.powerShellContext.ExecuteScriptStringAsync( this.variableScriptFile.FilePath); await this.AssertDebuggerStopped(this.variableScriptFile.FilePath); @@ -618,7 +624,7 @@ await this.debugService.SetLineBreakpoints( // Test set of a local string variable (not strongly typed) string newStrValue = "\"Goodbye\""; - string setStrValue = await debugService.SetVariable(stackFrames[0].LocalVariables.Id, "$strVar", newStrValue); + string setStrValue = await debugService.SetVariableAsync(stackFrames[0].LocalVariables.Id, "$strVar", newStrValue); Assert.Equal(newStrValue, setStrValue); VariableScope[] scopes = this.debugService.GetVariableScopes(0); @@ -627,13 +633,13 @@ await this.debugService.SetLineBreakpoints( VariableScope scriptScope = scopes.FirstOrDefault(s => s.Name == VariableContainerDetails.ScriptScopeName); string newIntValue = "49"; string newIntExpr = "7 * 7"; - string setIntValue = await debugService.SetVariable(scriptScope.Id, "$scriptInt", newIntExpr); + string setIntValue = await debugService.SetVariableAsync(scriptScope.Id, "$scriptInt", newIntExpr); Assert.Equal(newIntValue, setIntValue); // Test set of global scope int variable (not strongly typed) VariableScope globalScope = scopes.FirstOrDefault(s => s.Name == VariableContainerDetails.GlobalScopeName); string newGlobalIntValue = "4242"; - string setGlobalIntValue = await debugService.SetVariable(globalScope.Id, "$MaximumAliasCount", newGlobalIntValue); + string setGlobalIntValue = await debugService.SetVariableAsync(globalScope.Id, "$MaximumHistoryCount", newGlobalIntValue); Assert.Equal(newGlobalIntValue, setGlobalIntValue); // The above just tests that the debug service returns the correct new value string. @@ -659,7 +665,7 @@ await this.debugService.SetLineBreakpoints( // Test set of global scope int variable (not strongly typed) globalScope = scopes.FirstOrDefault(s => s.Name == VariableContainerDetails.GlobalScopeName); variables = debugService.GetVariables(globalScope.Id); - var intGlobalVar = variables.FirstOrDefault(v => v.Name == "$MaximumAliasCount"); + var intGlobalVar = variables.FirstOrDefault(v => v.Name == "$MaximumHistoryCount"); Assert.Equal(newGlobalIntValue, intGlobalVar.ValueString); // Abort execution of the script @@ -669,13 +675,13 @@ await this.debugService.SetLineBreakpoints( [Fact] public async Task DebuggerSetsVariablesWithConversion() { - await this.debugService.SetLineBreakpoints( + await this.debugService.SetLineBreakpointsAsync( this.variableScriptFile, new[] { BreakpointDetails.Create("", 14) }); // Execute the script and wait for the breakpoint to be hit Task executeTask = - this.powerShellContext.ExecuteScriptString( + this.powerShellContext.ExecuteScriptStringAsync( this.variableScriptFile.FilePath); await this.AssertDebuggerStopped(this.variableScriptFile.FilePath); @@ -688,7 +694,7 @@ await this.debugService.SetLineBreakpoints( // Test set of a local string variable (not strongly typed but force conversion) string newStrValue = "\"False\""; string newStrExpr = "$false"; - string setStrValue = await debugService.SetVariable(stackFrames[0].LocalVariables.Id, "$strVar2", newStrExpr); + string setStrValue = await debugService.SetVariableAsync(stackFrames[0].LocalVariables.Id, "$strVar2", newStrExpr); Assert.Equal(newStrValue, setStrValue); VariableScope[] scopes = this.debugService.GetVariableScopes(0); @@ -697,14 +703,14 @@ await this.debugService.SetLineBreakpoints( VariableScope scriptScope = scopes.FirstOrDefault(s => s.Name == VariableContainerDetails.ScriptScopeName); string newBoolValue = "$true"; string newBoolExpr = "1"; - string setBoolValue = await debugService.SetVariable(scriptScope.Id, "$scriptBool", newBoolExpr); + string setBoolValue = await debugService.SetVariableAsync(scriptScope.Id, "$scriptBool", newBoolExpr); Assert.Equal(newBoolValue, setBoolValue); // Test set of global scope ActionPreference variable (strongly typed) VariableScope globalScope = scopes.FirstOrDefault(s => s.Name == VariableContainerDetails.GlobalScopeName); string newGlobalValue = "Continue"; string newGlobalExpr = "'Continue'"; - string setGlobalValue = await debugService.SetVariable(globalScope.Id, "$VerbosePreference", newGlobalExpr); + string setGlobalValue = await debugService.SetVariableAsync(globalScope.Id, "$VerbosePreference", newGlobalExpr); Assert.Equal(newGlobalValue, setGlobalValue); // The above just tests that the debug service returns the correct new value string. @@ -740,13 +746,13 @@ await this.debugService.SetLineBreakpoints( [Fact] public async Task DebuggerVariableEnumDisplaysCorrectly() { - await this.debugService.SetLineBreakpoints( + await this.debugService.SetLineBreakpointsAsync( this.variableScriptFile, new[] { BreakpointDetails.Create("", 18) }); // Execute the script and wait for the breakpoint to be hit Task executeTask = - this.powerShellContext.ExecuteScriptString( + this.powerShellContext.ExecuteScriptStringAsync( this.variableScriptFile.FilePath); await this.AssertDebuggerStopped(this.variableScriptFile.FilePath); @@ -768,13 +774,13 @@ await this.debugService.SetLineBreakpoints( [Fact] public async Task DebuggerVariableHashtableDisplaysCorrectly() { - await this.debugService.SetLineBreakpoints( + await this.debugService.SetLineBreakpointsAsync( this.variableScriptFile, new[] { BreakpointDetails.Create("", 18) }); // Execute the script and wait for the breakpoint to be hit Task executeTask = - this.powerShellContext.ExecuteScriptString( + this.powerShellContext.ExecuteScriptStringAsync( this.variableScriptFile.FilePath); await this.AssertDebuggerStopped(this.variableScriptFile.FilePath); @@ -784,17 +790,26 @@ await this.debugService.SetLineBreakpoints( VariableDetailsBase[] variables = debugService.GetVariables(stackFrames[0].LocalVariables.Id); - var var = variables.FirstOrDefault(v => v.Name == "$assocArrVar"); + VariableDetailsBase var = variables.FirstOrDefault(v => v.Name == "$assocArrVar"); Assert.NotNull(var); Assert.Equal("[Hashtable: 2]", var.ValueString); Assert.True(var.IsExpandable); - var childVars = debugService.GetVariables(var.Id); + VariableDetailsBase[] childVars = debugService.GetVariables(var.Id); Assert.Equal(9, childVars.Length); Assert.Equal("[0]", childVars[0].Name); - Assert.Equal("[secondChild, 42]", childVars[0].ValueString); Assert.Equal("[1]", childVars[1].Name); - Assert.Equal("[firstChild, \"Child\"]", childVars[1].ValueString); + + var childVarStrs = new HashSet(childVars.Select(v => v.ValueString)); + var expectedVars = new [] { + "[firstChild, \"Child\"]", + "[secondChild, 42]" + }; + + foreach (string expectedVar in expectedVars) + { + Assert.Contains(expectedVar, childVarStrs); + } // Abort execution of the script this.powerShellContext.AbortExecution(); @@ -803,13 +818,13 @@ await this.debugService.SetLineBreakpoints( [Fact] public async Task DebuggerVariablePSObjectDisplaysCorrectly() { - await this.debugService.SetLineBreakpoints( + await this.debugService.SetLineBreakpointsAsync( this.variableScriptFile, new[] { BreakpointDetails.Create("", 18) }); // Execute the script and wait for the breakpoint to be hit Task executeTask = - this.powerShellContext.ExecuteScriptString( + this.powerShellContext.ExecuteScriptStringAsync( this.variableScriptFile.FilePath); await this.AssertDebuggerStopped(this.variableScriptFile.FilePath); @@ -819,17 +834,17 @@ await this.debugService.SetLineBreakpoints( VariableDetailsBase[] variables = debugService.GetVariables(stackFrames[0].LocalVariables.Id); - var var = variables.FirstOrDefault(v => v.Name == "$psObjVar"); - Assert.NotNull(var); - Assert.Equal("@{Age=75; Name=John}", var.ValueString); - Assert.True(var.IsExpandable); + var psObjVar = variables.FirstOrDefault(v => v.Name == "$psObjVar"); + Assert.NotNull(psObjVar); + Assert.True("@{Age=75; Name=John}".Equals(psObjVar.ValueString) || "@{Name=John; Age=75}".Equals(psObjVar.ValueString)); + Assert.True(psObjVar.IsExpandable); - var childVars = debugService.GetVariables(var.Id); - Assert.Equal(2, childVars.Length); - Assert.Equal("Age", childVars[0].Name); - Assert.Equal("75", childVars[0].ValueString); - Assert.Equal("Name", childVars[1].Name); - Assert.Equal("\"John\"", childVars[1].ValueString); + IDictionary childVars = debugService.GetVariables(psObjVar.Id).ToDictionary(v => v.Name, v => v.ValueString); + Assert.Equal(2, childVars.Count); + Assert.Contains("Age", childVars.Keys); + Assert.Contains("Name", childVars.Keys); + Assert.Equal(childVars["Age"], "75"); + Assert.Equal(childVars["Name"], "\"John\""); // Abort execution of the script this.powerShellContext.AbortExecution(); @@ -838,13 +853,13 @@ await this.debugService.SetLineBreakpoints( [Fact] public async Task DebuggerVariablePSCustomObjectDisplaysCorrectly() { - await this.debugService.SetLineBreakpoints( + await this.debugService.SetLineBreakpointsAsync( this.variableScriptFile, new[] { BreakpointDetails.Create("", 18) }); // Execute the script and wait for the breakpoint to be hit Task executeTask = - this.powerShellContext.ExecuteScriptString( + this.powerShellContext.ExecuteScriptStringAsync( this.variableScriptFile.FilePath); await this.AssertDebuggerStopped(this.variableScriptFile.FilePath); @@ -870,18 +885,24 @@ await this.debugService.SetLineBreakpoints( this.powerShellContext.AbortExecution(); } +// TODO: Make this test cross platform by using the PowerShell process +// (the only process we can guarantee cross-platform) +#if CoreCLR + [Fact(Skip = "Need to use the PowerShell process in a cross-platform way for this test to work")] +#else // Verifies fix for issue #86, $proc = Get-Process foo displays just the // ETS property set and not all process properties. [Fact] +#endif public async Task DebuggerVariableProcessObjDisplaysCorrectly() { - await this.debugService.SetLineBreakpoints( + await this.debugService.SetLineBreakpointsAsync( this.variableScriptFile, new[] { BreakpointDetails.Create("", 18) }); // Execute the script and wait for the breakpoint to be hit Task executeTask = - this.powerShellContext.ExecuteScriptString( + this.powerShellContext.ExecuteScriptStringAsync( this.variableScriptFile.FilePath); await this.AssertDebuggerStopped(this.variableScriptFile.FilePath); @@ -901,14 +922,14 @@ await this.debugService.SetLineBreakpoints( // Abort execution of the script this.powerShellContext.AbortExecution(); - } + } public async Task AssertDebuggerPaused() { SynchronizationContext syncContext = SynchronizationContext.Current; DebuggerStoppedEventArgs eventArgs = - await this.debuggerStoppedQueue.DequeueAsync(); + await this.debuggerStoppedQueue.DequeueAsync(new CancellationTokenSource(5000).Token); Assert.Equal(0, eventArgs.OriginalEvent.Breakpoints.Count); } @@ -920,7 +941,7 @@ public async Task AssertDebuggerStopped( SynchronizationContext syncContext = SynchronizationContext.Current; DebuggerStoppedEventArgs eventArgs = - await this.debuggerStoppedQueue.DequeueAsync(); + await this.debuggerStoppedQueue.DequeueAsync(new CancellationTokenSource(5000).Token); @@ -936,7 +957,7 @@ private async Task AssertStateChange( PowerShellExecutionResult expectedResult = PowerShellExecutionResult.Completed) { SessionStateChangedEventArgs newState = - await this.sessionStateQueue.DequeueAsync(); + await this.sessionStateQueue.DequeueAsync(new CancellationTokenSource(5000).Token); Assert.Equal(expectedState, newState.NewSessionState); Assert.Equal(expectedResult, newState.ExecutionResult); @@ -945,7 +966,7 @@ private async Task AssertStateChange( private async Task> GetConfirmedBreakpoints(ScriptFile scriptFile) { return - await this.powerShellContext.ExecuteCommand( + await this.powerShellContext.ExecuteCommandAsync( new PSCommand() .AddCommand("Get-PSBreakpoint") .AddParameter("Script", scriptFile.FilePath)); diff --git a/test/PowerShellEditorServices.Test/Extensions/ExtensionServiceTests.cs b/test/PowerShellEditorServices.Test/Extensions/ExtensionServiceTests.cs index e56656a91..60e3095b0 100644 --- a/test/PowerShellEditorServices.Test/Extensions/ExtensionServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Extensions/ExtensionServiceTests.cs @@ -5,6 +5,7 @@ using Microsoft.PowerShell.EditorServices.Components; using Microsoft.PowerShell.EditorServices.Extensions; +using Microsoft.PowerShell.EditorServices.Test.Shared; using Microsoft.PowerShell.EditorServices.Utility; using System; using System.Collections.Generic; @@ -38,7 +39,8 @@ public async Task InitializeAsync() { var logger = Logging.NullLogger; this.powerShellContext = PowerShellContextFactory.Create(logger); - await this.powerShellContext.ImportCommandsModule(@"..\..\..\..\..\module\PowerShellEditorServices\Commands"); + await this.powerShellContext.ImportCommandsModuleAsync( + TestUtilities.NormalizePath("../../../../../module/PowerShellEditorServices/Commands")); this.extensionService = new ExtensionService(this.powerShellContext); this.editorOperations = new TestEditorOperations(); @@ -47,11 +49,11 @@ public async Task InitializeAsync() this.extensionService.CommandUpdated += ExtensionService_ExtensionUpdated; this.extensionService.CommandRemoved += ExtensionService_ExtensionRemoved; - await this.extensionService.Initialize( + await this.extensionService.InitializeAsync( this.editorOperations, new ComponentRegistry()); - var filePath = @"c:\Test\Test.ps1"; + var filePath = TestUtilities.NormalizePath("c:/Test/Test.ps1"); this.currentFile = new ScriptFile(filePath, filePath, "This is a test file", new Version("5.0")); this.commandContext = new EditorContext( @@ -70,39 +72,39 @@ public Task DisposeAsync() [Fact] public async Task CanRegisterAndInvokeCommandWithCmdletName() { - await extensionService.PowerShellContext.ExecuteScriptString( - "function Invoke-Extension { $global:extensionValue = 5 }\r\n" + + await extensionService.PowerShellContext.ExecuteScriptStringAsync( + TestUtilities.NormalizeNewlines("function Invoke-Extension { $global:extensionValue = 5 }\n") + "Register-EditorCommand -Name \"test.function\" -DisplayName \"Function extension\" -Function \"Invoke-Extension\""); // Wait for the add event EditorCommand command = await this.AssertExtensionEvent(EventType.Add, "test.function"); // Invoke the command - await extensionService.InvokeCommand("test.function", this.commandContext); + await extensionService.InvokeCommandAsync("test.function", this.commandContext); // Assert the expected value PSCommand psCommand = new PSCommand(); psCommand.AddScript("$global:extensionValue"); - var results = await powerShellContext.ExecuteCommand(psCommand); + var results = await powerShellContext.ExecuteCommandAsync(psCommand); Assert.Equal(5, results.FirstOrDefault()); } [Fact] public async Task CanRegisterAndInvokeCommandWithScriptBlock() { - await extensionService.PowerShellContext.ExecuteScriptString( + await extensionService.PowerShellContext.ExecuteScriptStringAsync( "Register-EditorCommand -Name \"test.scriptblock\" -DisplayName \"ScriptBlock extension\" -ScriptBlock { $global:extensionValue = 10 }"); // Wait for the add event EditorCommand command = await this.AssertExtensionEvent(EventType.Add, "test.scriptblock"); // Invoke the command - await extensionService.InvokeCommand("test.scriptblock", this.commandContext); + await extensionService.InvokeCommandAsync("test.scriptblock", this.commandContext); // Assert the expected value PSCommand psCommand = new PSCommand(); psCommand.AddScript("$global:extensionValue"); - var results = await powerShellContext.ExecuteCommand(psCommand); + var results = await powerShellContext.ExecuteCommandAsync(psCommand); Assert.Equal(10, results.FirstOrDefault()); } @@ -110,10 +112,10 @@ await extensionService.PowerShellContext.ExecuteScriptString( public async Task CanUpdateRegisteredCommand() { // Register a command and then update it - await extensionService.PowerShellContext.ExecuteScriptString( - "function Invoke-Extension { Write-Output \"Extension output!\" }\r\n" + - "Register-EditorCommand -Name \"test.function\" -DisplayName \"Function extension\" -Function \"Invoke-Extension\"\r\n" + - "Register-EditorCommand -Name \"test.function\" -DisplayName \"Updated Function extension\" -Function \"Invoke-Extension\""); + await extensionService.PowerShellContext.ExecuteScriptStringAsync(TestUtilities.NormalizeNewlines( + "function Invoke-Extension { Write-Output \"Extension output!\" }\n" + + "Register-EditorCommand -Name \"test.function\" -DisplayName \"Function extension\" -Function \"Invoke-Extension\"\n" + + "Register-EditorCommand -Name \"test.function\" -DisplayName \"Updated Function extension\" -Function \"Invoke-Extension\"")); // Wait for the add and update events await this.AssertExtensionEvent(EventType.Add, "test.function"); @@ -126,19 +128,19 @@ await extensionService.PowerShellContext.ExecuteScriptString( public async Task CanUnregisterCommand() { // Add the command and wait for the add event - await extensionService.PowerShellContext.ExecuteScriptString( + await extensionService.PowerShellContext.ExecuteScriptStringAsync( "Register-EditorCommand -Name \"test.scriptblock\" -DisplayName \"ScriptBlock extension\" -ScriptBlock { Write-Output \"Extension output!\" }"); await this.AssertExtensionEvent(EventType.Add, "test.scriptblock"); // Remove the command and wait for the remove event - await extensionService.PowerShellContext.ExecuteScriptString( + await extensionService.PowerShellContext.ExecuteScriptStringAsync( "Unregister-EditorCommand -Name \"test.scriptblock\""); await this.AssertExtensionEvent(EventType.Remove, "test.scriptblock"); // Ensure that the command has been unregistered await Assert.ThrowsAsync( typeof(KeyNotFoundException), - () => extensionService.InvokeCommand("test.scriptblock", this.commandContext)); + () => extensionService.InvokeCommandAsync("test.scriptblock", this.commandContext)); } private async Task AssertExtensionEvent(EventType expectedEventType, string expectedExtensionName) @@ -185,67 +187,67 @@ public string GetWorkspaceRelativePath(string filePath) throw new NotImplementedException(); } - public Task NewFile() + public Task NewFileAsync() { throw new NotImplementedException(); } - public Task OpenFile(string filePath) + public Task OpenFileAsync(string filePath) { throw new NotImplementedException(); } - public Task OpenFile(string filePath, bool preview) + public Task OpenFileAsync(string filePath, bool preview) { throw new NotImplementedException(); } - public Task CloseFile(string filePath) + public Task CloseFileAsync(string filePath) { throw new NotImplementedException(); } - public Task SaveFile(string filePath) + public Task SaveFileAsync(string filePath) { - return SaveFile(filePath, null); + return SaveFileAsync(filePath, null); } - public Task SaveFile(string filePath, string newSavePath) + public Task SaveFileAsync(string filePath, string newSavePath) { throw new NotImplementedException(); } - public Task InsertText(string filePath, string text, BufferRange insertRange) + public Task InsertTextAsync(string filePath, string text, BufferRange insertRange) { throw new NotImplementedException(); } - public Task SetSelection(BufferRange selectionRange) + public Task SetSelectionAsync(BufferRange selectionRange) { throw new NotImplementedException(); } - public Task GetEditorContext() + public Task GetEditorContextAsync() { throw new NotImplementedException(); } - public Task ShowInformationMessage(string message) + public Task ShowInformationMessageAsync(string message) { throw new NotImplementedException(); } - public Task ShowErrorMessage(string message) + public Task ShowErrorMessageAsync(string message) { throw new NotImplementedException(); } - public Task ShowWarningMessage(string message) + public Task ShowWarningMessageAsync(string message) { throw new NotImplementedException(); } - public Task SetStatusBarMessage(string message, int? timeout) + public Task SetStatusBarMessageAsync(string message, int? timeout) { throw new NotImplementedException(); } diff --git a/test/PowerShellEditorServices.Test/Language/LanguageServiceTests.cs b/test/PowerShellEditorServices.Test/Language/LanguageServiceTests.cs index a432fb73f..656e5153b 100644 --- a/test/PowerShellEditorServices.Test/Language/LanguageServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Language/LanguageServiceTests.cs @@ -16,6 +16,8 @@ using System.Threading.Tasks; using Xunit; using Microsoft.PowerShell.EditorServices.Utility; +using Microsoft.PowerShell.EditorServices.Test.Shared; +using System.Runtime.InteropServices; namespace Microsoft.PowerShell.EditorServices.Test.Language { @@ -24,7 +26,8 @@ public class LanguageServiceTests : IDisposable private Workspace workspace; private LanguageService languageService; private PowerShellContext powerShellContext; - private const string baseSharedScriptPath = @"..\..\..\..\PowerShellEditorServices.Test.Shared\"; + private static readonly string s_baseSharedScriptPath = + TestUtilities.NormalizePath("../../../../PowerShellEditorServices.Test.Shared/"); public LanguageServiceTests() { @@ -52,7 +55,7 @@ await this.GetCompletionResults( completionResults.Completions[0]); } - [Fact(Skip = "This test does not run correctly on AppVeyor, need to investigate.")] + [Fact] public async Task LanguageServiceCompletesCommandFromModule() { CompletionResults completionResults = @@ -99,9 +102,14 @@ await this.GetCompletionResults( CompleteFilePath.SourceDetails); Assert.NotEqual(0, completionResults.Completions.Length); - Assert.Equal( - CompleteFilePath.ExpectedRange, - completionResults.ReplacedRange); + // TODO: Since this is a path completion, this test will need to be + // platform specific. Probably something like: + // - Windows: C:\Program + // - macOS: /User + // - Linux: /hom + //Assert.Equal( + // CompleteFilePath.ExpectedRange, + // completionResults.ReplacedRange); } [Fact] @@ -183,7 +191,7 @@ await this.GetDefinition( FindsFunctionDefinitionInWorkspace.SourceDetails, new Workspace(this.powerShellContext.LocalPowerShellVersion.Version, Logging.NullLogger) { - WorkspacePath = Path.Combine(baseSharedScriptPath, @"References") + WorkspacePath = Path.Combine(s_baseSharedScriptPath, @"References") }); var definition = definitionResult.FoundDefinition; Assert.EndsWith("ReferenceFileE.ps1", definition.FilePath); @@ -234,9 +242,10 @@ public async Task LanguageServiceFindsReferencesOnCommandWithAlias() await this.GetReferences( FindsReferencesOnBuiltInCommandWithAlias.SourceDetails); - Assert.Equal(6, refsResult.FoundReferences.Count()); - Assert.Equal("Get-ChildItem", refsResult.FoundReferences.Last().SymbolName); - Assert.Equal("ls", refsResult.FoundReferences.ToArray()[1].SymbolName); + SymbolReference[] foundRefs = refsResult.FoundReferences.ToArray(); + Assert.Equal(4, foundRefs.Length); + Assert.Equal("gci", foundRefs[1].SymbolName); + Assert.Equal("Get-ChildItem", foundRefs[foundRefs.Length - 1].SymbolName); } [Fact] @@ -246,10 +255,9 @@ public async Task LanguageServiceFindsReferencesOnAlias() await this.GetReferences( FindsReferencesOnBuiltInCommandWithAlias.SourceDetails); - Assert.Equal(6, refsResult.FoundReferences.Count()); + Assert.Equal(4, refsResult.FoundReferences.Count()); + Assert.Equal("dir", refsResult.FoundReferences.ToArray()[2].SymbolName); Assert.Equal("Get-ChildItem", refsResult.FoundReferences.Last().SymbolName); - Assert.Equal("gci", refsResult.FoundReferences.ToArray()[2].SymbolName); - Assert.Equal("LS", refsResult.FoundReferences.ToArray()[4].SymbolName); } [Fact] @@ -275,7 +283,7 @@ await this.GetReferences( public async Task LanguageServiceFindsDetailsForBuiltInCommand() { SymbolDetails symbolDetails = - await this.languageService.FindSymbolDetailsAtLocation( + await this.languageService.FindSymbolDetailsAtLocationAsync( this.GetScriptFile(FindsDetailsForBuiltInCommand.SourceDetails), FindsDetailsForBuiltInCommand.SourceDetails.StartLineNumber, FindsDetailsForBuiltInCommand.SourceDetails.StartColumnNumber); @@ -346,7 +354,7 @@ private ScriptFile GetScriptFile(ScriptRegion scriptRegion) { string resolvedPath = Path.Combine( - baseSharedScriptPath, + s_baseSharedScriptPath, scriptRegion.File); return @@ -358,7 +366,7 @@ private async Task GetCompletionResults(ScriptRegion scriptRe { // Run the completions request return - await this.languageService.GetCompletionsInFile( + await this.languageService.GetCompletionsInFileAsync( GetScriptFile(scriptRegion), scriptRegion.StartLineNumber, scriptRegion.StartColumnNumber); @@ -367,7 +375,7 @@ await this.languageService.GetCompletionsInFile( private async Task GetParamSetSignatures(ScriptRegion scriptRegion) { return - await this.languageService.FindParameterSetsInFile( + await this.languageService.FindParameterSetsInFileAsync( GetScriptFile(scriptRegion), scriptRegion.StartLineNumber, scriptRegion.StartColumnNumber); @@ -386,7 +394,7 @@ private async Task GetDefinition(ScriptRegion scriptRegion, Assert.NotNull(symbolReference); return - await this.languageService.GetDefinitionOfSymbol( + await this.languageService.GetDefinitionOfSymbolAsync( scriptFile, symbolReference, workspace); @@ -410,7 +418,7 @@ private async Task GetReferences(ScriptRegion scriptRegion Assert.NotNull(symbolReference); return - await this.languageService.FindReferencesOfSymbol( + await this.languageService.FindReferencesOfSymbolAsync( symbolReference, this.workspace.ExpandScriptReferences(scriptFile), this.workspace); diff --git a/test/PowerShellEditorServices.Test/PowerShellContextFactory.cs b/test/PowerShellEditorServices.Test/PowerShellContextFactory.cs index 806a935b3..6b9848251 100644 --- a/test/PowerShellEditorServices.Test/PowerShellContextFactory.cs +++ b/test/PowerShellEditorServices.Test/PowerShellContextFactory.cs @@ -19,7 +19,7 @@ internal static class PowerShellContextFactory { public static PowerShellContext Create(ILogger logger) { - PowerShellContext powerShellContext = new PowerShellContext(logger); + PowerShellContext powerShellContext = new PowerShellContext(logger, isPSReadLineEnabled: false); powerShellContext.Initialize( PowerShellContextTests.TestProfilePaths, PowerShellContext.CreateRunspace( @@ -59,7 +59,7 @@ protected override InputPromptHandler OnCreateInputPromptHandler() throw new NotImplementedException(); } - protected override Task ReadCommandLine(CancellationToken cancellationToken) + protected override Task ReadCommandLineAsync(CancellationToken cancellationToken) { return Task.FromResult("USER COMMAND"); } diff --git a/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj b/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj index 579ee55e0..383baffae 100644 --- a/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj +++ b/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj @@ -1,37 +1,33 @@  - - netcoreapp2.0;net452 + netcoreapp2.1;net461 Microsoft.PowerShell.EditorServices.Test - + + true + true + - - - - 6.0.0-alpha13 - - - - + + - - - $(DefineConstants);CoreCLR - - - - - - - + + + + + + + + - + + $(DefineConstants);CoreCLR + diff --git a/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs b/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs index ef07ae730..88776c407 100644 --- a/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs +++ b/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs @@ -4,6 +4,7 @@ // using Microsoft.PowerShell.EditorServices.Session; +using Microsoft.PowerShell.EditorServices.Test.Shared; using Microsoft.PowerShell.EditorServices.Utility; using System; using System.Collections.Generic; @@ -20,8 +21,8 @@ public class PowerShellContextTests : IDisposable private PowerShellContext powerShellContext; private AsyncQueue stateChangeQueue; - private const string DebugTestFilePath = - @"..\..\..\..\PowerShellEditorServices.Test.Shared\Debugging\DebugTest.ps1"; + private static readonly string s_debugTestFilePath = + TestUtilities.NormalizePath("../../../../PowerShellEditorServices.Test.Shared/Debugging/DebugTest.ps1"); public static readonly HostDetails TestHostDetails = new HostDetails( @@ -36,9 +37,9 @@ public class PowerShellContextTests : IDisposable new ProfilePaths( TestHostDetails.ProfileId, Path.GetFullPath( - @"..\..\..\..\PowerShellEditorServices.Test.Shared\Profile"), + TestUtilities.NormalizePath("../../../../PowerShellEditorServices.Test.Shared/Profile")), Path.GetFullPath( - @"..\..\..\..\PowerShellEditorServices.Test.Shared")); + TestUtilities.NormalizePath("../../../../PowerShellEditorServices.Test.Shared"))); public PowerShellContextTests() { @@ -60,7 +61,7 @@ public async Task CanExecutePSCommand() psCommand.AddScript("$a = \"foo\"; $a"); var executeTask = - this.powerShellContext.ExecuteCommand(psCommand); + this.powerShellContext.ExecuteCommandAsync(psCommand); await this.AssertStateChange(PowerShellContextState.Running); await this.AssertStateChange(PowerShellContextState.Ready); @@ -73,14 +74,14 @@ public async Task CanExecutePSCommand() public async Task CanQueueParallelRunspaceRequests() { // Concurrently initiate 4 requests in the session - Task taskOne = this.powerShellContext.ExecuteScriptString("$x = 100"); - Task handleTask = this.powerShellContext.GetRunspaceHandle(); - Task taskTwo = this.powerShellContext.ExecuteScriptString("$x += 200"); - Task taskThree = this.powerShellContext.ExecuteScriptString("$x = $x / 100"); + Task taskOne = this.powerShellContext.ExecuteScriptStringAsync("$x = 100"); + Task handleTask = this.powerShellContext.GetRunspaceHandleAsync(); + Task taskTwo = this.powerShellContext.ExecuteScriptStringAsync("$x += 200"); + Task taskThree = this.powerShellContext.ExecuteScriptStringAsync("$x = $x / 100"); PSCommand psCommand = new PSCommand(); psCommand.AddScript("$x"); - Task> resultTask = this.powerShellContext.ExecuteCommand(psCommand); + Task> resultTask = this.powerShellContext.ExecuteCommandAsync(psCommand); // Wait for the requested runspace handle and then dispose it RunspaceHandle handle = await handleTask; @@ -104,7 +105,7 @@ public async Task CanAbortExecution() Task.Run( async () => { - var unusedTask = this.powerShellContext.ExecuteScriptWithArgs(DebugTestFilePath); + var unusedTask = this.powerShellContext.ExecuteScriptWithArgsAsync(s_debugTestFilePath); await Task.Delay(50); this.powerShellContext.AbortExecution(); }); @@ -129,7 +130,7 @@ public async Task CanResolveAndLoadProfilesForHostId() }; // Load the profiles for the test host name - await this.powerShellContext.LoadHostProfiles(); + await this.powerShellContext.LoadHostProfilesAsync(); // Ensure that all the paths are set in the correct variables // and that the current user's host profile got loaded @@ -142,7 +143,7 @@ public async Task CanResolveAndLoadProfilesForHostId() "$(Assert-ProfileLoaded)\""); var result = - await this.powerShellContext.ExecuteCommand( + await this.powerShellContext.ExecuteCommandAsync( psCommand); string expectedString = diff --git a/test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs b/test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs index 993e56999..15be76c9d 100644 --- a/test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs +++ b/test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs @@ -4,6 +4,7 @@ // using Microsoft.PowerShell.EditorServices; +using Microsoft.PowerShell.EditorServices.Test.Shared; using System; using System.IO; using System.Linq; @@ -13,7 +14,12 @@ namespace PSLanguageService.Test { public class ScriptFileChangeTests { - private static readonly Version PowerShellVersion = new Version("5.0"); + +#if CoreCLR + private static readonly Version PowerShellVersion = new Version(6, 1); +#else + private static readonly Version PowerShellVersion = new Version(5, 1); +#endif [Fact] public void CanApplySingleLineInsert() @@ -67,15 +73,15 @@ public void CanApplySingleLineDelete() public void CanApplyMultiLineInsert() { this.AssertFileChange( - "first\r\nsecond\r\nfifth", - "first\r\nsecond\r\nthird\r\nfourth\r\nfifth", + TestUtilities.NormalizeNewlines("first\nsecond\nfifth"), + TestUtilities.NormalizeNewlines("first\nsecond\nthird\nfourth\nfifth"), new FileChange { Line = 3, EndLine = 3, Offset = 1, EndOffset = 1, - InsertString = "third\r\nfourth\r\n" + InsertString = TestUtilities.NormalizeNewlines("third\nfourth\n") }); } @@ -83,15 +89,15 @@ public void CanApplyMultiLineInsert() public void CanApplyMultiLineReplace() { this.AssertFileChange( - "first\r\nsecoXX\r\nXXfth", - "first\r\nsecond\r\nthird\r\nfourth\r\nfifth", + TestUtilities.NormalizeNewlines("first\nsecoXX\nXXfth"), + TestUtilities.NormalizeNewlines("first\nsecond\nthird\nfourth\nfifth"), new FileChange { Line = 2, EndLine = 3, Offset = 5, EndOffset = 3, - InsertString = "nd\r\nthird\r\nfourth\r\nfi" + InsertString = TestUtilities.NormalizeNewlines("nd\nthird\nfourth\nfi") }); } @@ -99,15 +105,15 @@ public void CanApplyMultiLineReplace() public void CanApplyMultiLineReplaceWithRemovedLines() { this.AssertFileChange( - "first\r\nsecoXX\r\nREMOVE\r\nTHESE\r\nLINES\r\nXXfth", - "first\r\nsecond\r\nthird\r\nfourth\r\nfifth", + TestUtilities.NormalizeNewlines("first\nsecoXX\nREMOVE\nTHESE\nLINES\nXXfth"), + TestUtilities.NormalizeNewlines("first\nsecond\nthird\nfourth\nfifth"), new FileChange { Line = 2, EndLine = 6, Offset = 5, EndOffset = 3, - InsertString = "nd\r\nthird\r\nfourth\r\nfi" + InsertString = TestUtilities.NormalizeNewlines("nd\nthird\nfourth\nfi") }); } @@ -115,8 +121,8 @@ public void CanApplyMultiLineReplaceWithRemovedLines() public void CanApplyMultiLineDelete() { this.AssertFileChange( - "first\r\nsecond\r\nREMOVE\r\nTHESE\r\nLINES\r\nthird", - "first\r\nsecond\r\nthird", + TestUtilities.NormalizeNewlines("first\nsecond\nREMOVE\nTHESE\nLINES\nthird"), + TestUtilities.NormalizeNewlines("first\nsecond\nthird"), new FileChange { Line = 3, @@ -131,15 +137,15 @@ public void CanApplyMultiLineDelete() public void CanApplyEditsToEndOfFile() { this.AssertFileChange( - "line1\r\nline2\r\nline3\r\n\r\n", - "line1\r\nline2\r\nline3\r\n\r\n\r\n\r\n", + TestUtilities.NormalizeNewlines("line1\nline2\nline3\n\n"), + TestUtilities.NormalizeNewlines("line1\nline2\nline3\n\n\n\n"), new FileChange { Line = 5, EndLine = 5, Offset = 1, EndOffset = 1, - InsertString = "\r\n\r\n" + InsertString = Environment.NewLine + Environment.NewLine }); } @@ -147,15 +153,15 @@ public void CanApplyEditsToEndOfFile() public void CanAppendToEndOfFile() { this.AssertFileChange( - "line1\r\nline2\r\nline3", - "line1\r\nline2\r\nline3\r\nline4\r\nline5", + TestUtilities.NormalizeNewlines("line1\nline2\nline3"), + TestUtilities.NormalizeNewlines("line1\nline2\nline3\nline4\nline5"), new FileChange { Line = 4, EndLine = 5, Offset = 1, EndOffset = 1, - InsertString = "line4\r\nline5" + InsertString = $"line4{Environment.NewLine}line5" } ); } @@ -163,12 +169,12 @@ public void CanAppendToEndOfFile() [Fact] public void FindsDotSourcedFiles() { - string exampleScriptContents = - @". .\athing.ps1" + "\r\n" + - @". .\somefile.ps1" + "\r\n" + - @". .\somefile.ps1" + "\r\n" + - @"Do-Stuff $uri" + "\r\n" + - @". simpleps.ps1"; + string exampleScriptContents = TestUtilities.PlatformNormalize( + ". ./athing.ps1\n"+ + ". ./somefile.ps1\n"+ + ". ./somefile.ps1\n"+ + "Do-Stuff $uri\n"+ + ". simpleps.ps1"); using (StringReader stringReader = new StringReader(exampleScriptContents)) { @@ -181,7 +187,7 @@ public void FindsDotSourcedFiles() Assert.Equal(3, scriptFile.ReferencedFiles.Length); System.Console.Write("a" + scriptFile.ReferencedFiles[0]); - Assert.Equal(@".\athing.ps1", scriptFile.ReferencedFiles[0]); + Assert.Equal(TestUtilities.NormalizePath("./athing.ps1"), scriptFile.ReferencedFiles[0]); } } @@ -193,8 +199,8 @@ public void ThrowsExceptionWithEditOutsideOfRange() () => { this.AssertFileChange( - "first\r\nsecond\r\nREMOVE\r\nTHESE\r\nLINES\r\nthird", - "first\r\nsecond\r\nthird", + TestUtilities.NormalizeNewlines("first\nsecond\nREMOVE\nTHESE\nLINES\nthird"), + TestUtilities.NormalizeNewlines("first\nsecond\nthird"), new FileChange { Line = 3, @@ -210,8 +216,8 @@ public void ThrowsExceptionWithEditOutsideOfRange() public void CanDeleteFromEndOfFile() { this.AssertFileChange( - "line1\r\nline2\r\nline3\r\nline4", - "line1\r\nline2", + TestUtilities.NormalizeNewlines("line1\nline2\nline3\nline4"), + TestUtilities.NormalizeNewlines("line1\nline2"), new FileChange { Line = 3, @@ -255,11 +261,13 @@ private void AssertFileChange( public class ScriptFileGetLinesTests { - private const string TestString_NoTrailingNewline = "Line One\r\nLine Two\r\nLine Three\r\nLine Four\r\nLine Five"; + private static readonly string TestString_NoTrailingNewline = TestUtilities.NormalizeNewlines( + "Line One\nLine Two\nLine Three\nLine Four\nLine Five"); - private const string TestString_TrailingNewline = TestString_NoTrailingNewline + "\r\n"; + private static readonly string TestString_TrailingNewline = TestUtilities.NormalizeNewlines( + TestString_NoTrailingNewline + "\n"); - private static readonly string[] s_newLines = new string[] { "\r\n" }; + private static readonly string[] s_newLines = new string[] { Environment.NewLine }; private static readonly string[] s_testStringLines_noTrailingNewline = TestString_NoTrailingNewline.Split(s_newLines, StringSplitOptions.None); @@ -269,7 +277,6 @@ public class ScriptFileGetLinesTests private ScriptFile _scriptFile_noTrailingNewline; - public ScriptFileGetLinesTests() { _scriptFile_noTrailingNewline = ScriptFileChangeTests.CreateScriptFile( diff --git a/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs b/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs index b93d796d0..cb9203ca1 100644 --- a/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs +++ b/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs @@ -4,7 +4,10 @@ // using System; +using System.Collections.Generic; using System.IO; +using System.Runtime.InteropServices; +using Microsoft.PowerShell.EditorServices.Test.Shared; using Microsoft.PowerShell.EditorServices.Utility; using Xunit; @@ -17,31 +20,34 @@ public class WorkspaceTests [Fact] public void CanResolveWorkspaceRelativePath() { - string workspacePath = @"c:\Test\Workspace\"; - string testPathInside = @"c:\Test\Workspace\SubFolder\FilePath.ps1"; - string testPathOutside = @"c:\Test\PeerPath\FilePath.ps1"; - string testPathAnotherDrive = @"z:\TryAndFindMe\FilePath.ps1"; + string workspacePath = TestUtilities.NormalizePath("c:/Test/Workspace/"); + string testPathInside = TestUtilities.NormalizePath("c:/Test/Workspace/SubFolder/FilePath.ps1"); + string testPathOutside = TestUtilities.NormalizePath("c:/Test/PeerPath/FilePath.ps1"); + string testPathAnotherDrive = TestUtilities.NormalizePath("z:/TryAndFindMe/FilePath.ps1"); Workspace workspace = new Workspace(PowerShellVersion, Logging.NullLogger); // Test without a workspace path Assert.Equal(testPathOutside, workspace.GetRelativePath(testPathOutside)); + string expectedInsidePath = TestUtilities.NormalizePath("SubFolder/FilePath.ps1"); + string expectedOutsidePath = TestUtilities.NormalizePath("../PeerPath/FilePath.ps1"); + // Test with a workspace path workspace.WorkspacePath = workspacePath; - Assert.Equal(@"SubFolder\FilePath.ps1", workspace.GetRelativePath(testPathInside)); - Assert.Equal(@"..\PeerPath\FilePath.ps1", workspace.GetRelativePath(testPathOutside)); + Assert.Equal(expectedInsidePath, workspace.GetRelativePath(testPathInside)); + Assert.Equal(expectedOutsidePath, workspace.GetRelativePath(testPathOutside)); Assert.Equal(testPathAnotherDrive, workspace.GetRelativePath(testPathAnotherDrive)); } [Fact] public void CanDetermineIsPathInMemory() { - var tempDir = Environment.GetEnvironmentVariable("TEMP"); - var shortDirPath = Path.Combine(tempDir, "GitHub", "PowerShellEditorServices"); - var shortFilePath = Path.Combine(shortDirPath, "foo.ps1"); - var shortUriForm = "git:/c%3A/Users/Keith/GitHub/dahlbyk/posh-git/src/PoshGitTypes.ps1?%7B%22path%22%3A%22c%3A%5C%5CUsers%5C%5CKeith%5C%5CGitHub%5C%5Cdahlbyk%5C%5Cposh-git%5C%5Csrc%5C%5CPoshGitTypes.ps1%22%2C%22ref%22%3A%22~%22%7D"; - var longUriForm = "gitlens-git:c%3A%5CUsers%5CKeith%5CGitHub%5Cdahlbyk%5Cposh-git%5Csrc%5CPoshGitTypes%3Ae0022701.ps1?%7B%22fileName%22%3A%22src%2FPoshGitTypes.ps1%22%2C%22repoPath%22%3A%22c%3A%2FUsers%2FKeith%2FGitHub%2Fdahlbyk%2Fposh-git%22%2C%22sha%22%3A%22e0022701fa12e0bc22d0458673d6443c942b974a%22%7D"; + string tempDir = Path.GetTempPath(); + string shortDirPath = Path.Combine(tempDir, "GitHub", "PowerShellEditorServices"); + string shortFilePath = Path.Combine(shortDirPath, "foo.ps1"); + string shortUriForm = "git:/c%3A/Users/Keith/GitHub/dahlbyk/posh-git/src/PoshGitTypes.ps1?%7B%22path%22%3A%22c%3A%5C%5CUsers%5C%5CKeith%5C%5CGitHub%5C%5Cdahlbyk%5C%5Cposh-git%5C%5Csrc%5C%5CPoshGitTypes.ps1%22%2C%22ref%22%3A%22~%22%7D"; + string longUriForm = "gitlens-git:c%3A%5CUsers%5CKeith%5CGitHub%5Cdahlbyk%5Cposh-git%5Csrc%5CPoshGitTypes%3Ae0022701.ps1?%7B%22fileName%22%3A%22src%2FPoshGitTypes.ps1%22%2C%22repoPath%22%3A%22c%3A%2FUsers%2FKeith%2FGitHub%2Fdahlbyk%2Fposh-git%22%2C%22sha%22%3A%22e0022701fa12e0bc22d0458673d6443c942b974a%22%7D"; var testCases = new[] { // Test short file absolute paths @@ -52,7 +58,7 @@ public void CanDetermineIsPathInMemory() // Test short file relative paths - not sure we'll ever get these but just in case new { IsInMemory = false, Path = "foo.ps1" }, - new { IsInMemory = false, Path = ".." + Path.DirectorySeparatorChar + "foo.ps1" }, + new { IsInMemory = false, Path = Path.Combine(new [] { "..", "foo.ps1" }) }, // Test short non-file paths new { IsInMemory = true, Path = "untitled:untitled-1" }, @@ -72,15 +78,7 @@ public void CanDetermineIsPathInMemory() } [Theory()] - [InlineData("file:///C%3A/banana/", @"C:\banana\")] - [InlineData("file:///C%3A/banana/ex.ps1", @"C:\banana\ex.ps1")] - [InlineData("file:///E%3A/Path/to/awful%23path", @"E:\Path\to\awful#path")] - [InlineData("file:///path/with/no/drive", @"C:\path\with\no\drive")] - [InlineData("file:///path/wi[th]/squ[are/brackets/", @"C:\path\wi[th]\squ[are\brackets\")] - [InlineData("file:///Carrots/A%5Ere/Good/", @"C:\Carrots\A^re\Good\")] - [InlineData("file:///Users/barnaby/%E8%84%9A%E6%9C%AC/Reduce-Directory", @"C:\Users\barnaby\脚本\Reduce-Directory")] - [InlineData("file:///C%3A/Program%20Files%20%28x86%29/PowerShell/6/pwsh.exe", @"C:\Program Files (x86)\PowerShell\6\pwsh.exe")] - [InlineData("file:///home/maxim/test%20folder/%D0%9F%D0%B0%D0%BF%D0%BA%D0%B0/helloworld.ps1", @"C:\home\maxim\test folder\Папка\helloworld.ps1")] + [MemberData(nameof(PathsToResolve), parameters: 2)] public void CorrectlyResolvesPaths(string givenPath, string expectedPath) { Workspace workspace = new Workspace(PowerShellVersion, Logging.NullLogger); @@ -89,5 +87,41 @@ public void CorrectlyResolvesPaths(string givenPath, string expectedPath) Assert.Equal(expectedPath, resolvedPath); } + + public static IEnumerable PathsToResolve + { + get + { + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? s_winPathsToResolve + : s_unixPathsToResolve; + } + } + + private static object[][] s_winPathsToResolve = new object[][] + { + new object[] { "file:///C%3A/banana/", @"C:\banana\" }, + new object[] { "file:///C%3A/banana/ex.ps1", @"C:\banana\ex.ps1" }, + new object[] { "file:///E%3A/Path/to/awful%23path", @"E:\Path\to\awful#path" }, + new object[] { "file:///path/with/no/drive", @"C:\path\with\no\drive" }, + new object[] { "file:///path/wi[th]/squ[are/brackets/", @"C:\path\wi[th]\squ[are\brackets\" }, + new object[] { "file:///Carrots/A%5Ere/Good/", @"C:\Carrots\A^re\Good\" }, + new object[] { "file:///Users/barnaby/%E8%84%9A%E6%9C%AC/Reduce-Directory", @"C:\Users\barnaby\脚本\Reduce-Directory" }, + new object[] { "file:///C%3A/Program%20Files%20%28x86%29/PowerShell/6/pwsh.exe", @"C:\Program Files (x86)\PowerShell\6\pwsh.exe" }, + new object[] { "file:///home/maxim/test%20folder/%D0%9F%D0%B0%D0%BF%D0%BA%D0%B0/helloworld.ps1", @"C:\home\maxim\test folder\Папка\helloworld.ps1" } + }; + + private static object[][] s_unixPathsToResolve = new object[][] + { + new object[] { "file:///banana/", @"/banana/" }, + new object[] { "file:///banana/ex.ps1", @"/banana/ex.ps1" }, + new object[] { "file:///Path/to/awful%23path", @"/Path/to/awful#path" }, + new object[] { "file:///path/with/no/drive", @"/path/with/no/drive" }, + new object[] { "file:///path/wi[th]/squ[are/brackets/", @"/path/wi[th]/squ[are/brackets/" }, + new object[] { "file:///Carrots/A%5Ere/Good/", @"/Carrots/A^re/Good/" }, + new object[] { "file:///Users/barnaby/%E8%84%9A%E6%9C%AC/Reduce-Directory", @"/Users/barnaby/脚本/Reduce-Directory" }, + new object[] { "file:///Program%20Files%20%28x86%29/PowerShell/6/pwsh.exe", @"/Program Files (x86)/PowerShell/6/pwsh.exe" }, + new object[] { "file:///home/maxim/test%20folder/%D0%9F%D0%B0%D0%BF%D0%BA%D0%B0/helloworld.ps1", @"/home/maxim/test folder/Папка/helloworld.ps1" } + }; } } diff --git a/test/PowerShellEditorServices.Test/Utility/AsyncDebouncerTests.cs b/test/PowerShellEditorServices.Test/Utility/AsyncDebouncerTests.cs index c87d2e43b..9c40a351d 100644 --- a/test/PowerShellEditorServices.Test/Utility/AsyncDebouncerTests.cs +++ b/test/PowerShellEditorServices.Test/Utility/AsyncDebouncerTests.cs @@ -14,24 +14,24 @@ namespace Microsoft.PowerShell.EditorServices.Test.Utility { public class AsyncDebouncerTests { - [Fact(Skip = "TODO: This test fails in the new build system, need to investigate!")] + [Fact(Skip = "AsyncDebouncer not flushing within the interval")] public async Task AsyncDebouncerFlushesAfterInterval() { TestAsyncDebouncer debouncer = new TestAsyncDebouncer(); - await debouncer.Invoke(1); - await debouncer.Invoke(2); - await debouncer.Invoke(3); + await debouncer.InvokeAsync(1); + await debouncer.InvokeAsync(2); + await debouncer.InvokeAsync(3); await Task.Delay(TestAsyncDebouncer.Interval + 100); // Add a few more items to ensure they are added after the initial interval - await debouncer.Invoke(4); - await debouncer.Invoke(5); - await debouncer.Invoke(6); + await debouncer.InvokeAsync(4); + await debouncer.InvokeAsync(5); + await debouncer.InvokeAsync(6); Assert.Equal(new List { 1, 2, 3 }, debouncer.FlushedBuffer); Assert.True( - debouncer.TimeToFlush > + debouncer.TimeToFlush > TimeSpan.FromMilliseconds(TestAsyncDebouncer.Interval), "Debouncer flushed before interval lapsed."); @@ -40,19 +40,19 @@ public async Task AsyncDebouncerFlushesAfterInterval() Assert.Equal(new List { 4, 5, 6 }, debouncer.FlushedBuffer); } - [Fact(Skip = "TODO: This test fails in the new build system, need to investigate!")] - public async Task AsyncDebouncerRestartsAfterInvoke() + [Fact] + public async Task AsyncDebouncerRestartsAfterInvokeAsync() { TestAsyncRestartDebouncer debouncer = new TestAsyncRestartDebouncer(); // Invoke the debouncer and wait a bit between each // invoke to make sure the debouncer isn't flushed // until after the last invoke. - await debouncer.Invoke(1); + await debouncer.InvokeAsync(1); await Task.Delay(TestAsyncRestartDebouncer.Interval - 100); - await debouncer.Invoke(2); + await debouncer.InvokeAsync(2); await Task.Delay(TestAsyncRestartDebouncer.Interval - 100); - await debouncer.Invoke(3); + await debouncer.InvokeAsync(3); await Task.Delay(TestAsyncRestartDebouncer.Interval + 100); // The only item flushed should be 3 since its interval has lapsed @@ -77,7 +77,7 @@ public TestAsyncDebouncer() : base(Interval, false) { } - protected override Task OnInvoke(int args) + protected override Task OnInvokeAsync(int args) { if (!this.firstInvoke.HasValue) { @@ -89,7 +89,7 @@ protected override Task OnInvoke(int args) return Task.FromResult(true); } - protected override Task OnFlush() + protected override Task OnFlushAsync() { // Mark the flush time this.TimeToFlush = DateTime.Now - this.firstInvoke.Value; @@ -118,13 +118,13 @@ public TestAsyncRestartDebouncer() : base(Interval, true) { } - protected override Task OnInvoke(int args) + protected override Task OnInvokeAsync(int args) { this.lastInvokeInt = args; return Task.FromResult(true); } - protected override Task OnFlush() + protected override Task OnFlushAsync() { this.FlushedBuffer.Add(this.lastInvokeInt); diff --git a/test/PowerShellEditorServices.Test/Utility/AsyncQueueTests.cs b/test/PowerShellEditorServices.Test/Utility/AsyncQueueTests.cs index fe161c8ca..af67e88f3 100644 --- a/test/PowerShellEditorServices.Test/Utility/AsyncQueueTests.cs +++ b/test/PowerShellEditorServices.Test/Utility/AsyncQueueTests.cs @@ -26,11 +26,11 @@ public async Task AsyncQueueSynchronizesAccess() { // Start 5 consumers await Task.WhenAll( - Task.Run(() => ConsumeItems(inputQueue, outputItems, cancellationTokenSource.Token)), - Task.Run(() => ConsumeItems(inputQueue, outputItems, cancellationTokenSource.Token)), - Task.Run(() => ConsumeItems(inputQueue, outputItems, cancellationTokenSource.Token)), - Task.Run(() => ConsumeItems(inputQueue, outputItems, cancellationTokenSource.Token)), - Task.Run(() => ConsumeItems(inputQueue, outputItems, cancellationTokenSource.Token)), + Task.Run(() => ConsumeItemsAsync(inputQueue, outputItems, cancellationTokenSource.Token)), + Task.Run(() => ConsumeItemsAsync(inputQueue, outputItems, cancellationTokenSource.Token)), + Task.Run(() => ConsumeItemsAsync(inputQueue, outputItems, cancellationTokenSource.Token)), + Task.Run(() => ConsumeItemsAsync(inputQueue, outputItems, cancellationTokenSource.Token)), + Task.Run(() => ConsumeItemsAsync(inputQueue, outputItems, cancellationTokenSource.Token)), Task.Run( async () => { @@ -76,7 +76,7 @@ public async Task AsyncQueueSkipsCancelledTasks() Assert.Equal(1, taskTwo.Result); } - private async Task ConsumeItems( + private async Task ConsumeItemsAsync( AsyncQueue inputQueue, ConcurrentBag outputItems, CancellationToken cancellationToken) diff --git a/test/PowerShellEditorServices.Test/Utility/LoggerTests.cs b/test/PowerShellEditorServices.Test/Utility/LoggerTests.cs index cdb2e2237..ca0558a54 100644 --- a/test/PowerShellEditorServices.Test/Utility/LoggerTests.cs +++ b/test/PowerShellEditorServices.Test/Utility/LoggerTests.cs @@ -3,6 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using Microsoft.PowerShell.EditorServices.Test.Shared; using Microsoft.PowerShell.EditorServices.Utility; using System; using System.IO; @@ -128,7 +129,7 @@ private string ReadLogContents() { return string.Join( - "\r\n", + Environment.NewLine, File.ReadAllLines( logFilePath, Encoding.UTF8));