diff --git a/.editorconfig b/.editorconfig index 335885163..1dd0a2a9e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -21,3 +21,6 @@ trim_trailing_whitespace = true [*.{ps1xml,props,xml,yaml}] indent_size = 2 + +# CA1303: Do not pass literals as localized parameters +dotnet_diagnostic.CA1303.severity = none diff --git a/.gitignore b/.gitignore index 8d18bc259..219315a5b 100644 --- a/.gitignore +++ b/.gitignore @@ -37,7 +37,7 @@ docs/metadata/ *.zip # Generated build info file -src/PowerShellEditorServices/Hosting/BuildInfo.cs +src/PowerShellEditorServices.Hosting/BuildInfo.cs # quickbuild.exe /VersionGeneratingLogs/ diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index aeee99828..c7431abd1 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -19,149 +19,23 @@ param( #Requires -Modules @{ModuleName="InvokeBuild";ModuleVersion="3.2.1"} $script:IsUnix = $PSVersionTable.PSEdition -and $PSVersionTable.PSEdition -eq "Core" -and !$IsWindows -$script:TargetPlatform = "netstandard2.0" -$script:TargetFrameworksParam = "/p:TargetFrameworks=`"$script:TargetPlatform`"" $script:RequiredSdkVersion = (Get-Content (Join-Path $PSScriptRoot 'global.json') | ConvertFrom-Json).sdk.version -$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:BuildInfoPath = [System.IO.Path]::Combine($PSScriptRoot, "src", "PowerShellEditorServices", "Hosting", "BuildInfo.cs") - -$script:PSCoreModulePath = $null - -$script:TestRuntime = @{ - 'Core' = 'netcoreapp2.1' - 'Desktop' = 'net461' -} +$script:BuildInfoPath = [System.IO.Path]::Combine($PSScriptRoot, "src", "PowerShellEditorServices.Hosting", "BuildInfo.cs") -<# -Declarative specification of binary assets produced -in the build that need to be binplaced in the module. -Schema is: -{ - : { - : [ - - ] - } +$script:NetRuntime = @{ + Core = 'netcoreapp2.1' + Desktop = 'net461' + Standard = 'netstandard2.0' } -#> -$script:RequiredBuildAssets = @{ - $script:ModuleBinPath = @{ - 'PowerShellEditorServices' = @( - 'publish/Microsoft.Extensions.DependencyInjection.Abstractions.dll', - 'publish/Microsoft.Extensions.DependencyInjection.dll', - 'publish/Microsoft.Extensions.FileSystemGlobbing.dll', - 'publish/Microsoft.Extensions.Logging.Abstractions.dll', - 'publish/Microsoft.Extensions.Logging.dll', - 'publish/Microsoft.Extensions.Options.dll', - 'publish/Microsoft.Extensions.Primitives.dll', - 'publish/Microsoft.PowerShell.EditorServices.dll', - 'publish/Microsoft.PowerShell.EditorServices.pdb', - 'publish/Newtonsoft.Json.dll', - 'publish/OmniSharp.Extensions.JsonRpc.dll', - 'publish/OmniSharp.Extensions.LanguageProtocol.dll', - 'publish/OmniSharp.Extensions.LanguageServer.dll', - 'publish/OmniSharp.Extensions.DebugAdapter.dll', - 'publish/OmniSharp.Extensions.DebugAdapter.Server.dll', - 'publish/MediatR.dll', - 'publish/MediatR.Extensions.Microsoft.DependencyInjection.dll', - 'publish/runtimes/linux-64/native/libdisablekeyecho.so', - 'publish/runtimes/osx-64/native/libdisablekeyecho.dylib', - 'publish/Serilog.dll', - 'publish/Serilog.Extensions.Logging.dll', - 'publish/Serilog.Sinks.File.dll', - 'publish/System.Reactive.dll', - 'publish/UnixConsoleEcho.dll' - ) - } - $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' } - ) -} +$script:HostCoreOutput = "$PSScriptRoot/src/PowerShellEditorServices.Hosting/bin/$Configuration/$($script:NetRuntime.Core)/publish" +$script:HostDeskOutput = "$PSScriptRoot/src/PowerShellEditorServices.Hosting/bin/$Configuration/$($script:NetRuntime.Desktop)/publish" +$script:PsesOutput = "$PSScriptRoot/src/PowerShellEditorServices/bin/$Configuration/$($script:NetRuntime.Standard)/publish" +$script:VSCodeOutput = "$PSScriptRoot/src/PowerShellEditorServices.VSCode/bin/$Configuration/$($script:NetRuntime.Standard)/publish" if (Get-Command git -ErrorAction SilentlyContinue) { # ignore changes to this file - git update-index --assume-unchanged "$PSScriptRoot/src/PowerShellEditorServices.Host/BuildInfo/BuildInfo.cs" -} - -if ($PSVersionTable.PSEdition -ne "Core") { - Add-Type -Assembly System.IO.Compression.FileSystem -} - -function Restore-NugetAsmForRuntime { - param( - [ValidateNotNull()][string]$PackageName, - [ValidateNotNull()][string]$PackageVersion, - [string]$DllName, - [string]$DestinationPath, - [string]$TargetPlatform = $script:NetFrameworkPlatformId, - [string]$TargetRuntime = $script:WindowsPowerShellFrameworkTarget - ) - - $tmpDir = Join-Path $PSScriptRoot '.tmp' - if (-not (Test-Path $tmpDir)) { - New-Item -ItemType Directory -Path $tmpDir - } - - 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 + git update-index --assume-unchanged "$PSScriptRoot/src/PowerShellEditorServices.Hosting/BuildInfo.cs" } function Invoke-WithCreateDefaultHook { @@ -284,11 +158,17 @@ task CreateBuildInfo -Before Build { $buildVersion = "" $buildOrigin = "" + if ($propsBody.VersionSuffix) + { + $propsXml = [xml](Get-Content -Raw -LiteralPath "$PSScriptRoot/PowerShellEditorServices.Common.props") + $propsBody = $propsXml.Project.PropertyGroup + $buildVersion = $propsBody.VersionPrefix + $buildVersion += '-' + $propsBody.VersionSuffix + } + # Set build info fields on build platforms if ($env:TF_BUILD) { - $psd1Path = [System.IO.Path]::Combine($PSScriptRoot, "module", "PowerShellEditorServices", "PowerShellEditorServices.psd1") - $buildVersion = (Import-PowerShellDataFile -LiteralPath $psd1Path).Version $buildOrigin = "VSTS" } @@ -310,8 +190,8 @@ namespace Microsoft.PowerShell.EditorServices.Hosting { public static class BuildInfo { - public const string BuildVersion = "$buildVersion"; - public const string BuildOrigin = "$buildOrigin"; + public static readonly string BuildVersion = "$buildVersion"; + public static readonly string BuildOrigin = "$buildOrigin"; public static readonly System.DateTime? BuildTime = System.DateTime.Parse("$buildTime"); } } @@ -327,8 +207,15 @@ task SetupHelpForTests -Before Test { } task Build { - exec { & $script:dotnetExe publish -c $Configuration .\src\PowerShellEditorServices\PowerShellEditorServices.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 $script:NetRuntime.Standard } + exec { & $script:dotnetExe publish -c $Configuration .\src\PowerShellEditorServices.Hosting\PowerShellEditorServices.Hosting.csproj -f $script:NetRuntime.Core } + if (-not $script:IsUnix) + { + exec { & $script:dotnetExe publish -c $Configuration .\src\PowerShellEditorServices.Hosting\PowerShellEditorServices.Hosting.csproj -f $script:NetRuntime.Desktop } + } + + # Build PowerShellEditorServices.VSCode module + exec { & $script:dotnetExe publish -c $Configuration .\src\PowerShellEditorServices.VSCode\PowerShellEditorServices.VSCode.csproj -f $script:NetRuntime.Standard } } function DotNetTestFilter { @@ -343,11 +230,11 @@ task TestServer { Set-Location .\test\PowerShellEditorServices.Test\ if (-not $script:IsUnix) { - exec { & $script:dotnetExe test --logger trx -f $script:TestRuntime.Desktop (DotNetTestFilter) } + exec { & $script:dotnetExe test --logger trx -f $script:NetRuntime.Desktop (DotNetTestFilter) } } Invoke-WithCreateDefaultHook -NewModulePath $script:PSCoreModulePath { - exec { & $script:dotnetExe test --logger trx -f $script:TestRuntime.Core (DotNetTestFilter) } + exec { & $script:dotnetExe test --logger trx -f $script:NetRuntime.Core (DotNetTestFilter) } } } @@ -355,11 +242,11 @@ task TestProtocol { Set-Location .\test\PowerShellEditorServices.Test.Protocol\ if (-not $script:IsUnix) { - exec { & $script:dotnetExe test --logger trx -f $script:TestRuntime.Desktop (DotNetTestFilter) } + exec { & $script:dotnetExe test --logger trx -f $script:NetRuntime.Desktop (DotNetTestFilter) } } Invoke-WithCreateDefaultHook { - exec { & $script:dotnetExe test --logger trx -f $script:TestRuntime.Core (DotNetTestFilter) } + exec { & $script:dotnetExe test --logger trx -f $script:NetRuntime.Core (DotNetTestFilter) } } } @@ -367,56 +254,91 @@ task TestHost { Set-Location .\test\PowerShellEditorServices.Test.Host\ if (-not $script:IsUnix) { - exec { & $script:dotnetExe build -f $script:TestRuntime.Desktop } - exec { & $script:dotnetExe test -f $script:TestRuntime.Desktop (DotNetTestFilter) } + exec { & $script:dotnetExe build -f $script:NetRuntime.Desktop } + exec { & $script:dotnetExe test -f $script:NetRuntime.Desktop (DotNetTestFilter) } } - exec { & $script:dotnetExe build -c $Configuration -f $script:TestRuntime.Core } - exec { & $script:dotnetExe test -f $script:TestRuntime.Core (DotNetTestFilter) } + exec { & $script:dotnetExe build -c $Configuration -f $script:NetRuntime.Core } + exec { & $script:dotnetExe test -f $script:NetRuntime.Core (DotNetTestFilter) } } task TestE2E { Set-Location .\test\PowerShellEditorServices.Test.E2E\ $env:PWSH_EXE_NAME = if ($IsCoreCLR) { "pwsh" } else { "powershell" } - exec { & $script:dotnetExe test --logger trx -f $script:TestRuntime.Core (DotNetTestFilter) } + exec { & $script:dotnetExe test --logger trx -f $script:NetRuntime.Core (DotNetTestFilter) } } task LayoutModule -After Build { + $modulesDir = "$PSScriptRoot/module" + $psesVSCodeBinOutputPath = "$modulesDir/PowerShellEditorServices.VSCode/bin" + $psesOutputPath = "$modulesDir/PowerShellEditorServices" + $psesBinOutputPath = "$PSScriptRoot/module/PowerShellEditorServices/bin" + $psesDepsPath = "$psesBinOutputPath/Common" + $psesCoreHostPath = "$psesBinOutputPath/Core" + $psesDeskHostPath = "$psesBinOutputPath/Desktop" + + foreach ($dir in $psesDepsPath,$psesCoreHostPath,$psesDeskHostPath,$psesVSCodeBinOutputPath) + { + New-Item -Force -Path $dir -ItemType Directory + } + # 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 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 - } + Copy-Item -Force -Path "$PSScriptRoot\Third Party Notices.txt" -Destination $psesOutputPath + + # Copy UnixConsoleEcho native libraries + Copy-Item -Path "$script:PsesOutput/runtimes/osx-64/native/*" -Destination $psesDepsPath + Copy-Item -Path "$script:PsesOutput/runtimes/linux-64/native/*" -Destination $psesDepsPath + + # Assemble PSES module + + $includedDlls = [System.Collections.Generic.HashSet[string]]::new() + [void]$includedDlls.Add('System.Management.Automation.dll') + + # PSES/bin/Common + foreach ($psesComponent in Get-ChildItem $script:PsesOutput) + { + if ($psesComponent.Name -eq 'System.Management.Automation.dll' -or + $psesComponent.Name -eq 'System.Runtime.InteropServices.RuntimeInformation.dll') + { + continue + } + + if ($psesComponent.Extension) + { + [void]$includedDlls.Add($psesComponent.Name) + Copy-Item -Path $psesComponent.FullName -Destination $psesDepsPath + } + } + + # PSES/bin/Core + foreach ($hostComponent in Get-ChildItem $script:HostCoreOutput) + { + if (-not $includedDlls.Contains($hostComponent.Name)) + { + Copy-Item -Path $hostComponent.FullName -Destination $psesCoreHostPath } } - # 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 + # PSES/bin/Desktop + if (-not $script:IsUnix) + { + foreach ($hostComponent in Get-ChildItem $script:HostDeskOutput) + { + if (-not $includedDlls.Contains($hostComponent.Name)) + { + Copy-Item -Path $hostComponent.FullName -Destination $psesDeskHostPath + } } + } - foreach ($packageDetails in $script:RequiredNugetBinaries[$binDestinationDir]) { - Restore-NugetAsmForRuntime -DestinationPath $binDestPath @packageDetails + # Assemble the PowerShellEditorServices.VSCode module + + foreach ($vscodeComponent in Get-ChildItem $script:VSCodeOutput) + { + if (-not $includedDlls.Contains($vscodeComponent.Name)) + { + Copy-Item -Path $vscodeComponent.FullName -Destination $psesVSCodeBinOutputPath } } } diff --git a/PowerShellEditorServices.sln b/PowerShellEditorServices.sln index 30c954422..802579fef 100644 --- a/PowerShellEditorServices.sln +++ b/PowerShellEditorServices.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26430.12 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29505.145 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F594E7FD-1E72-4E51-A496-B019C2BA3180}" EndProject @@ -22,12 +22,17 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerShellEditorServices.Te EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerShellEditorServices.VSCode", "src\PowerShellEditorServices.VSCode\PowerShellEditorServices.VSCode.csproj", "{3B38E8DA-8BFF-4264-AF16-47929E6398A3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerShellEditorServices", "src\PowerShellEditorServices\PowerShellEditorServices.csproj", "{29EEDF03-0990-45F4-846E-2616970D1FA2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerShellEditorServices", "src\PowerShellEditorServices\PowerShellEditorServices.csproj", "{29EEDF03-0990-45F4-846E-2616970D1FA2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerShellEditorServices.Test.E2E", "test\PowerShellEditorServices.Test.E2E\PowerShellEditorServices.Test.E2E.csproj", "{2561F253-8F72-436A-BCC3-AA63AB82EDC0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerShellEditorServices.Test.E2E", "test\PowerShellEditorServices.Test.E2E\PowerShellEditorServices.Test.E2E.csproj", "{2561F253-8F72-436A-BCC3-AA63AB82EDC0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerShellEditorServices.Hosting", "src\PowerShellEditorServices.Hosting\PowerShellEditorServices.Hosting.csproj", "{3CC791E7-6FC9-4DDE-B4A2-547266977E4E}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution + CoreCLR|Any CPU = CoreCLR|Any CPU + CoreCLR|x64 = CoreCLR|x64 + CoreCLR|x86 = CoreCLR|x86 Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 @@ -36,6 +41,12 @@ Global Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028}.CoreCLR|Any CPU.ActiveCfg = Release|Any CPU + {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028}.CoreCLR|Any CPU.Build.0 = Release|Any CPU + {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028}.CoreCLR|x64.ActiveCfg = Release|Any CPU + {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028}.CoreCLR|x64.Build.0 = Release|Any CPU + {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028}.CoreCLR|x86.ActiveCfg = Release|Any CPU + {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028}.CoreCLR|x86.Build.0 = Release|Any CPU {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028}.Debug|Any CPU.Build.0 = Debug|Any CPU {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -48,6 +59,12 @@ Global {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028}.Release|x64.Build.0 = Release|Any CPU {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028}.Release|x86.ActiveCfg = Release|Any CPU {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028}.Release|x86.Build.0 = Release|Any CPU + {8ED116F4-9DDF-4C49-AB96-AE462E3D64C3}.CoreCLR|Any CPU.ActiveCfg = Release|Any CPU + {8ED116F4-9DDF-4C49-AB96-AE462E3D64C3}.CoreCLR|Any CPU.Build.0 = Release|Any CPU + {8ED116F4-9DDF-4C49-AB96-AE462E3D64C3}.CoreCLR|x64.ActiveCfg = Release|Any CPU + {8ED116F4-9DDF-4C49-AB96-AE462E3D64C3}.CoreCLR|x64.Build.0 = Release|Any CPU + {8ED116F4-9DDF-4C49-AB96-AE462E3D64C3}.CoreCLR|x86.ActiveCfg = Release|Any CPU + {8ED116F4-9DDF-4C49-AB96-AE462E3D64C3}.CoreCLR|x86.Build.0 = Release|Any CPU {8ED116F4-9DDF-4C49-AB96-AE462E3D64C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8ED116F4-9DDF-4C49-AB96-AE462E3D64C3}.Debug|Any CPU.Build.0 = Debug|Any CPU {8ED116F4-9DDF-4C49-AB96-AE462E3D64C3}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -60,6 +77,12 @@ Global {8ED116F4-9DDF-4C49-AB96-AE462E3D64C3}.Release|x64.Build.0 = Release|Any CPU {8ED116F4-9DDF-4C49-AB96-AE462E3D64C3}.Release|x86.ActiveCfg = Release|Any CPU {8ED116F4-9DDF-4C49-AB96-AE462E3D64C3}.Release|x86.Build.0 = Release|Any CPU + {6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA}.CoreCLR|Any CPU.ActiveCfg = Release|Any CPU + {6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA}.CoreCLR|Any CPU.Build.0 = Release|Any CPU + {6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA}.CoreCLR|x64.ActiveCfg = Release|Any CPU + {6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA}.CoreCLR|x64.Build.0 = Release|Any CPU + {6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA}.CoreCLR|x86.ActiveCfg = Release|Any CPU + {6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA}.CoreCLR|x86.Build.0 = Release|Any CPU {6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA}.Debug|Any CPU.Build.0 = Debug|Any CPU {6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -72,6 +95,12 @@ Global {6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA}.Release|x64.Build.0 = Release|Any CPU {6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA}.Release|x86.ActiveCfg = Release|Any CPU {6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA}.Release|x86.Build.0 = Release|Any CPU + {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4}.CoreCLR|Any CPU.ActiveCfg = Release|Any CPU + {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4}.CoreCLR|Any CPU.Build.0 = Release|Any CPU + {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4}.CoreCLR|x64.ActiveCfg = Release|Any CPU + {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4}.CoreCLR|x64.Build.0 = Release|Any CPU + {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4}.CoreCLR|x86.ActiveCfg = Release|Any CPU + {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4}.CoreCLR|x86.Build.0 = Release|Any CPU {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4}.Debug|Any CPU.Build.0 = Debug|Any CPU {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -84,6 +113,12 @@ Global {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4}.Release|x64.Build.0 = Release|Any CPU {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4}.Release|x86.ActiveCfg = Release|Any CPU {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4}.Release|x86.Build.0 = Release|Any CPU + {3B38E8DA-8BFF-4264-AF16-47929E6398A3}.CoreCLR|Any CPU.ActiveCfg = CoreCLR|Any CPU + {3B38E8DA-8BFF-4264-AF16-47929E6398A3}.CoreCLR|Any CPU.Build.0 = CoreCLR|Any CPU + {3B38E8DA-8BFF-4264-AF16-47929E6398A3}.CoreCLR|x64.ActiveCfg = CoreCLR|Any CPU + {3B38E8DA-8BFF-4264-AF16-47929E6398A3}.CoreCLR|x64.Build.0 = CoreCLR|Any CPU + {3B38E8DA-8BFF-4264-AF16-47929E6398A3}.CoreCLR|x86.ActiveCfg = CoreCLR|Any CPU + {3B38E8DA-8BFF-4264-AF16-47929E6398A3}.CoreCLR|x86.Build.0 = CoreCLR|Any CPU {3B38E8DA-8BFF-4264-AF16-47929E6398A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3B38E8DA-8BFF-4264-AF16-47929E6398A3}.Debug|Any CPU.Build.0 = Debug|Any CPU {3B38E8DA-8BFF-4264-AF16-47929E6398A3}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -96,6 +131,12 @@ Global {3B38E8DA-8BFF-4264-AF16-47929E6398A3}.Release|x64.Build.0 = Release|Any CPU {3B38E8DA-8BFF-4264-AF16-47929E6398A3}.Release|x86.ActiveCfg = Release|Any CPU {3B38E8DA-8BFF-4264-AF16-47929E6398A3}.Release|x86.Build.0 = Release|Any CPU + {29EEDF03-0990-45F4-846E-2616970D1FA2}.CoreCLR|Any CPU.ActiveCfg = Release|Any CPU + {29EEDF03-0990-45F4-846E-2616970D1FA2}.CoreCLR|Any CPU.Build.0 = Release|Any CPU + {29EEDF03-0990-45F4-846E-2616970D1FA2}.CoreCLR|x64.ActiveCfg = Release|Any CPU + {29EEDF03-0990-45F4-846E-2616970D1FA2}.CoreCLR|x64.Build.0 = Release|Any CPU + {29EEDF03-0990-45F4-846E-2616970D1FA2}.CoreCLR|x86.ActiveCfg = Release|Any CPU + {29EEDF03-0990-45F4-846E-2616970D1FA2}.CoreCLR|x86.Build.0 = Release|Any CPU {29EEDF03-0990-45F4-846E-2616970D1FA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {29EEDF03-0990-45F4-846E-2616970D1FA2}.Debug|Any CPU.Build.0 = Debug|Any CPU {29EEDF03-0990-45F4-846E-2616970D1FA2}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -108,6 +149,12 @@ Global {29EEDF03-0990-45F4-846E-2616970D1FA2}.Release|x64.Build.0 = Release|Any CPU {29EEDF03-0990-45F4-846E-2616970D1FA2}.Release|x86.ActiveCfg = Release|Any CPU {29EEDF03-0990-45F4-846E-2616970D1FA2}.Release|x86.Build.0 = Release|Any CPU + {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.CoreCLR|Any CPU.ActiveCfg = Release|Any CPU + {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.CoreCLR|Any CPU.Build.0 = Release|Any CPU + {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.CoreCLR|x64.ActiveCfg = Release|Any CPU + {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.CoreCLR|x64.Build.0 = Release|Any CPU + {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.CoreCLR|x86.ActiveCfg = Release|Any CPU + {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.CoreCLR|x86.Build.0 = Release|Any CPU {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.Debug|Any CPU.Build.0 = Debug|Any CPU {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -120,6 +167,24 @@ Global {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.Release|x64.Build.0 = Release|Any CPU {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.Release|x86.ActiveCfg = Release|Any CPU {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.Release|x86.Build.0 = Release|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.CoreCLR|Any CPU.ActiveCfg = Release|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.CoreCLR|Any CPU.Build.0 = Release|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.CoreCLR|x64.ActiveCfg = Release|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.CoreCLR|x64.Build.0 = Release|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.CoreCLR|x86.ActiveCfg = Release|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.CoreCLR|x86.Build.0 = Release|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.Debug|x64.ActiveCfg = Debug|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.Debug|x64.Build.0 = Debug|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.Debug|x86.ActiveCfg = Debug|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.Debug|x86.Build.0 = Debug|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.Release|Any CPU.Build.0 = Release|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.Release|x64.ActiveCfg = Release|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.Release|x64.Build.0 = Release|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.Release|x86.ActiveCfg = Release|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -132,5 +197,9 @@ Global {3B38E8DA-8BFF-4264-AF16-47929E6398A3} = {F594E7FD-1E72-4E51-A496-B019C2BA3180} {29EEDF03-0990-45F4-846E-2616970D1FA2} = {F594E7FD-1E72-4E51-A496-B019C2BA3180} {2561F253-8F72-436A-BCC3-AA63AB82EDC0} = {422E561A-8118-4BE7-A54F-9309E4F03AAE} + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E} = {F594E7FD-1E72-4E51-A496-B019C2BA3180} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {3B9E8987-D4AC-426B-86F6-889126243A9A} EndGlobalSection EndGlobal diff --git a/module/PowerShellEditorServices/PowerShellEditorServices.psd1 b/module/PowerShellEditorServices/PowerShellEditorServices.psd1 index 7a03e12be..4c5887f4f 100644 --- a/module/PowerShellEditorServices/PowerShellEditorServices.psd1 +++ b/module/PowerShellEditorServices/PowerShellEditorServices.psd1 @@ -9,7 +9,14 @@ @{ # Script module or binary module file associated with this manifest. -RootModule = 'PowerShellEditorServices.psm1' +RootModule = if ($PSEdition -eq 'Core') + { + 'bin/Core/Microsoft.PowerShell.EditorServices.Hosting.dll' + } + else + { + 'bin/Desktop/Microsoft.PowerShell.EditorServices.Hosting.dll' + } # Version number of this module. ModuleVersion = '2.0.0' @@ -66,10 +73,10 @@ Copyright = '(c) 2017 Microsoft. All rights reserved.' # NestedModules = @() # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. - FunctionsToExport = @('Start-EditorServicesHost', 'Get-PowerShellEditorServicesVersion', 'Compress-LogDir') +FunctionsToExport = @() # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. -CmdletsToExport = @() +CmdletsToExport = @('Start-EditorServices') # Variables to export from this module VariablesToExport = @() diff --git a/module/PowerShellEditorServices/PowerShellEditorServices.psm1 b/module/PowerShellEditorServices/PowerShellEditorServices.psm1 deleted file mode 100644 index 770e516f8..000000000 --- a/module/PowerShellEditorServices/PowerShellEditorServices.psm1 +++ /dev/null @@ -1,238 +0,0 @@ -# -# Copyright (c) Microsoft. All rights reserved. -# Licensed under the MIT license. See LICENSE file in the project root for full license information. -# - -# 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" -} - -Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Microsoft.PowerShell.EditorServices.dll" - -function Start-EditorServicesHost { - [CmdletBinding()] - param( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [string] - $HostName, - - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [string] - $HostProfileId, - - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [string] - $HostVersion, - - [Parameter(ParameterSetName="Stdio",Mandatory=$true)] - [switch] - $Stdio, - - [Parameter(ParameterSetName="NamedPipe",Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [string] - $LanguageServiceNamedPipe, - - [Parameter(ParameterSetName="NamedPipe")] - [string] - $DebugServiceNamedPipe, - - [Parameter(ParameterSetName="NamedPipeSimplex",Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [string] - $LanguageServiceInNamedPipe, - - [Parameter(ParameterSetName="NamedPipeSimplex",Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [string] - $LanguageServiceOutNamedPipe, - - [Parameter(ParameterSetName="NamedPipeSimplex")] - [string] - $DebugServiceInNamedPipe, - - [Parameter(ParameterSetName="NamedPipeSimplex")] - [string] - $DebugServiceOutNamedPipe, - - [ValidateNotNullOrEmpty()] - [string] - $BundledModulesPath, - - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - $LogPath, - - [ValidateSet("Normal", "Verbose", "Error", "Diagnostic")] - $LogLevel = "Normal", - - [switch] - $EnableConsoleRepl, - - [switch] - $UseLegacyReadLine, - - [switch] - $DebugServiceOnly, - - [string[]] - $AdditionalModules = @(), - - [string[]] - [ValidateNotNull()] - $FeatureFlags = @(), - - [switch] - $WaitForDebugger - ) - - $editorServicesHost = $null - $hostDetails = - Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Hosting.HostDetails @( - $HostName, - $HostProfileId, - (Microsoft.PowerShell.Utility\New-Object System.Version @($HostVersion))) - - $editorServicesHost = - Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Hosting.EditorServicesHost @( - $hostDetails, - $BundledModulesPath, - $EnableConsoleRepl.IsPresent, - $UseLegacyReadLine.IsPresent, - $WaitForDebugger.IsPresent, - $AdditionalModules, - $FeatureFlags, - $Host) - - # Build the profile paths using the root paths of the current $profile variable - $profilePaths = - Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Hosting.ProfilePaths @( - $hostDetails.ProfileId, - [System.IO.Path]::GetDirectoryName($profile.AllUsersAllHosts), - [System.IO.Path]::GetDirectoryName($profile.CurrentUserAllHosts)) - - $editorServicesHost.StartLogging($LogPath, $LogLevel); - - $languageServiceConfig = - Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Hosting.EditorServiceTransportConfig - - $debugServiceConfig = - Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Hosting.EditorServiceTransportConfig - - switch ($PSCmdlet.ParameterSetName) { - "Stdio" { - $languageServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Hosting.EditorServiceTransportType]::Stdio - $debugServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Hosting.EditorServiceTransportType]::Stdio - break - } - "NamedPipe" { - $languageServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Hosting.EditorServiceTransportType]::NamedPipe - $languageServiceConfig.InOutPipeName = "$LanguageServiceNamedPipe" - if ($DebugServiceNamedPipe) { - $debugServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Hosting.EditorServiceTransportType]::NamedPipe - $debugServiceConfig.InOutPipeName = "$DebugServiceNamedPipe" - } - break - } - "NamedPipeSimplex" { - $languageServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Hosting.EditorServiceTransportType]::NamedPipe - $languageServiceConfig.InPipeName = $LanguageServiceInNamedPipe - $languageServiceConfig.OutPipeName = $LanguageServiceOutNamedPipe - if ($DebugServiceInNamedPipe -and $DebugServiceOutNamedPipe) { - $debugServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Hosting.EditorServiceTransportType]::NamedPipe - $debugServiceConfig.InPipeName = $DebugServiceInNamedPipe - $debugServiceConfig.OutPipeName = $DebugServiceOutNamedPipe - } - break - } - } - - if ($DebugServiceOnly.IsPresent) { - $editorServicesHost.StartDebugService($debugServiceConfig, $profilePaths, $true); - } elseif($Stdio.IsPresent) { - $editorServicesHost.StartLanguageService($languageServiceConfig, $profilePaths); - } else { - $editorServicesHost.StartLanguageService($languageServiceConfig, $profilePaths); - $editorServicesHost.StartDebugService($debugServiceConfig, $profilePaths, $false); - } - - return $editorServicesHost -} - -function Compress-LogDir { - [CmdletBinding(SupportsShouldProcess=$true)] - param ( - [Parameter(Mandatory=$true, Position=0, HelpMessage="Literal path to a log directory.")] - [ValidateNotNullOrEmpty()] - [string] - $Path - ) - - begin { - function LegacyZipFolder($Path, $ZipPath) { - if (!(Microsoft.PowerShell.Management\Test-Path($ZipPath))) { - $zipMagicHeader = "PK" + [char]5 + [char]6 + ("$([char]0)" * 18) - Microsoft.PowerShell.Management\Set-Content -LiteralPath $ZipPath -Value $zipMagicHeader - (Microsoft.PowerShell.Management\Get-Item $ZipPath).IsReadOnly = $false - } - - $shellApplication = Microsoft.PowerShell.Utility\New-Object -ComObject Shell.Application - $zipPackage = $shellApplication.NameSpace($ZipPath) - - foreach ($file in (Microsoft.PowerShell.Management\Get-ChildItem -LiteralPath $Path)) { - $zipPackage.CopyHere($file.FullName) - Start-Sleep -MilliSeconds 500 - } - } - } - - end { - $zipPath = ((Microsoft.PowerShell.Management\Convert-Path $Path) -replace '(\\|/)$','') + ".zip" - - if (Get-Command Microsoft.PowerShell.Archive\Compress-Archive) { - if ($PSCmdlet.ShouldProcess($zipPath, "Create ZIP")) { - Microsoft.PowerShell.Archive\Compress-Archive -LiteralPath $Path -DestinationPath $zipPath -Force -CompressionLevel Optimal - $zipPath - } - } - else { - if ($PSCmdlet.ShouldProcess($zipPath, "Create Legacy ZIP")) { - LegacyZipFolder $Path $zipPath - $zipPath - } - } - } -} - -function Get-PowerShellEditorServicesVersion { - $nl = [System.Environment]::NewLine - - $versionInfo = "PSES module version: $($MyInvocation.MyCommand.Module.Version)$nl" - - $versionInfo += "PSVersion: $($PSVersionTable.PSVersion)$nl" - if ($PSVersionTable.PSEdition) { - $versionInfo += "PSEdition: $($PSVersionTable.PSEdition)$nl" - } - $versionInfo += "PSBuildVersion: $($PSVersionTable.BuildVersion)$nl" - $versionInfo += "CLRVersion: $($PSVersionTable.CLRVersion)$nl" - - $versionInfo += "Operating system: " - if ($IsLinux) { - $versionInfo += "Linux $(lsb_release -d -s)$nl" - } - elseif ($IsOSX) { - $versionInfo += "macOS $(lsb_release -d -s)$nl" - } - else { - $osInfo = CimCmdlets\Get-CimInstance Win32_OperatingSystem - $versionInfo += "Windows $($osInfo.OSArchitecture) $($osInfo.Version)$nl" - } - - $versionInfo -} diff --git a/module/PowerShellEditorServices/Start-EditorServices.ps1 b/module/PowerShellEditorServices/Start-EditorServices.ps1 index 5f828a905..cbc0fc92f 100644 --- a/module/PowerShellEditorServices/Start-EditorServices.ps1 +++ b/module/PowerShellEditorServices/Start-EditorServices.ps1 @@ -108,343 +108,5 @@ param( $DebugServiceOutPipeName = $null ) -$DEFAULT_USER_MODE = "600" - -if ($LogLevel -eq "Diagnostic") { - if (!$Stdio.IsPresent) { - $VerbosePreference = 'Continue' - } - $scriptName = [System.IO.Path]::GetFileNameWithoutExtension($MyInvocation.MyCommand.Name) - $logFileName = [System.IO.Path]::GetFileName($LogPath) - Start-Transcript (Join-Path (Split-Path $LogPath -Parent) "$scriptName-$logFileName") -Force | Out-Null -} - -function LogSection([string]$msg) { - Write-Verbose "`n#-- $msg $('-' * ([Math]::Max(0, 73 - $msg.Length)))" -} - -function Log([string[]]$msg) { - $msg | Write-Verbose -} - -function ExitWithError($errorString) { - Write-Host -ForegroundColor Red "`n`n$errorString" - - # Sleep for a while to make sure the user has time to see and copy the - # error message - Start-Sleep -Seconds 300 - - exit 1; -} - -function WriteSessionFile($sessionInfo) { - $sessionInfoJson = Microsoft.PowerShell.Utility\ConvertTo-Json -InputObject $sessionInfo -Compress - Log "Writing session file with contents:" - Log $sessionInfoJson - $sessionInfoJson | Microsoft.PowerShell.Management\Set-Content -Force -Path "$SessionDetailsPath" -ErrorAction Stop -} - -# Are we running in PowerShell 2 or earlier? -$version = $PSVersionTable.PSVersion -if (($version.Major -le 2) -or ($version.Major -eq 6 -and $version.Minor -eq 0)) { - # No ConvertTo-Json on PSv2 and below, so write out the JSON manually - "{`"status`": `"failed`", `"reason`": `"unsupported`", `"powerShellVersion`": `"$($PSVersionTable.PSVersion.ToString())`"}" | - Microsoft.PowerShell.Management\Set-Content -Force -Path "$SessionDetailsPath" -ErrorAction Stop - - ExitWithError "Unsupported PowerShell version $($PSVersionTable.PSVersion), language features are disabled." -} - - -if ($host.Runspace.LanguageMode -eq 'ConstrainedLanguage') { - WriteSessionFile @{ - "status" = "failed" - "reason" = "languageMode" - "detail" = $host.Runspace.LanguageMode.ToString() - } - - ExitWithError "PowerShell is configured with an unsupported LanguageMode (ConstrainedLanguage), language features are disabled." -} - -# net451 and lower are not supported, only net452 and up -if ($PSVersionTable.PSVersion.Major -le 5) { - $net452Version = 379893 - $dotnetVersion = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\").Release - if ($dotnetVersion -lt $net452Version) { - Write-SessionFile @{ - status = failed - reason = "netversion" - detail = "$netVersion" - } - - ExitWithError "Your .NET version is too low. Upgrade to net452 or higher to run the PowerShell extension." - } -} - -# If PSReadline is present in the session, remove it so that runspace -# management is easier -if ((Microsoft.PowerShell.Core\Get-Module PSReadline).Count -gt 0) { - LogSection "Removing PSReadLine module" - Microsoft.PowerShell.Core\Remove-Module PSReadline -ErrorAction SilentlyContinue -} - -# This variable will be assigned later to contain information about -# what happened while attempting to launch the PowerShell Editor -# Services host -$resultDetails = $null; - -function Test-ModuleAvailable($ModuleName, $ModuleVersion) { - Log "Testing module availability $ModuleName $ModuleVersion" - - $modules = Microsoft.PowerShell.Core\Get-Module -ListAvailable $moduleName - if ($null -ne $modules) { - if ($null -ne $ModuleVersion) { - foreach ($module in $modules) { - if ($module.Version.Equals($moduleVersion)) { - Log "$ModuleName $ModuleVersion found" - return $true; - } - } - } - else { - Log "$ModuleName $ModuleVersion found" - return $true; - } - } - - Log "$ModuleName $ModuleVersion NOT found" - return $false; -} - -function New-NamedPipeName { - # We try 10 times to find a valid pipe name - for ($i = 0; $i -lt 10; $i++) { - $PipeName = "PSES_$([System.IO.Path]::GetRandomFileName())" - - if ((Test-NamedPipeName -PipeName $PipeName)) { - return $PipeName - } - } - - ExitWithError "Could not find valid a pipe name." -} - -function Get-NamedPipePath { - param( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [string] - $PipeName - ) - - if (($PSVersionTable.PSVersion.Major -le 5) -or $IsWindows) { - return "\\.\pipe\$PipeName"; - } - else { - # Windows uses NamedPipes where non-Windows platforms use Unix Domain Sockets. - # the Unix Domain Sockets live in the tmp directory and are prefixed with "CoreFxPipe_" - return (Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath "CoreFxPipe_$PipeName") - } -} - -# Returns True if it's a valid pipe name -# A valid pipe name is a file that does not exist either -# in the temp directory (macOS & Linux) or in the pipe directory (Windows) -function Test-NamedPipeName { - param( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [string] - $PipeName - ) - - $path = Get-NamedPipePath -PipeName $PipeName - return !(Test-Path $path) -} - -LogSection "Console Encoding" -Log $OutputEncoding - -function Get-ValidatedNamedPipeName { - param( - [string] - $PipeName - ) - - # If no PipeName is passed in, then we create one that's guaranteed to be valid - if (!$PipeName) { - $PipeName = New-NamedPipeName - } - elseif (!(Test-NamedPipeName -PipeName $PipeName)) { - ExitWithError "Pipe name supplied is already in use: $PipeName" - } - - return $PipeName -} - -function Set-PipeFileResult { - param ( - [Hashtable] - $ResultTable, - - [string] - $PipeNameKey, - - [string] - $PipeNameValue - ) - - $ResultTable[$PipeNameKey] = Get-NamedPipePath -PipeName $PipeNameValue -} - -# Add BundledModulesPath to $env:PSModulePath -if ($BundledModulesPath) { - $env:PSModulePath = $env:PSModulePath.TrimEnd([System.IO.Path]::PathSeparator) + [System.IO.Path]::PathSeparator + $BundledModulesPath - LogSection "Updated PSModulePath to:" - Log ($env:PSModulePath -split [System.IO.Path]::PathSeparator) -} - -LogSection "Check required modules available" -# Check if PowerShellGet module is available -if ((Test-ModuleAvailable "PowerShellGet") -eq $false) { - Log "Failed to find PowerShellGet module" - # TODO: WRITE ERROR -} - -try { - LogSection "Start up PowerShellEditorServices" - Log "Importing PowerShellEditorServices" - - Microsoft.PowerShell.Core\Import-Module PowerShellEditorServices -ErrorAction Stop - - if ($EnableConsoleRepl) { - Write-Host "PowerShell Integrated Console`n" - } - - $resultDetails = @{ - "status" = "not started"; - "languageServiceTransport" = $PSCmdlet.ParameterSetName; - "debugServiceTransport" = $PSCmdlet.ParameterSetName; - } - - # Create the Editor Services host - Log "Invoking Start-EditorServicesHost" - # There could be only one service on Stdio channel - # Locate available port numbers for services - switch ($PSCmdlet.ParameterSetName) { - "Stdio" { - $editorServicesHost = Start-EditorServicesHost ` - -HostName $HostName ` - -HostProfileId $HostProfileId ` - -HostVersion $HostVersion ` - -LogPath $LogPath ` - -LogLevel $LogLevel ` - -AdditionalModules $AdditionalModules ` - -Stdio ` - -BundledModulesPath $BundledModulesPath ` - -EnableConsoleRepl:$EnableConsoleRepl.IsPresent ` - -DebugServiceOnly:$DebugServiceOnly.IsPresent ` - -WaitForDebugger:$WaitForDebugger.IsPresent ` - -FeatureFlags $FeatureFlags - break - } - - "NamedPipeSimplex" { - $LanguageServiceInPipeName = Get-ValidatedNamedPipeName $LanguageServiceInPipeName - $LanguageServiceOutPipeName = Get-ValidatedNamedPipeName $LanguageServiceOutPipeName - $DebugServiceInPipeName = Get-ValidatedNamedPipeName $DebugServiceInPipeName - $DebugServiceOutPipeName = Get-ValidatedNamedPipeName $DebugServiceOutPipeName - - $editorServicesHost = Start-EditorServicesHost ` - -HostName $HostName ` - -HostProfileId $HostProfileId ` - -HostVersion $HostVersion ` - -LogPath $LogPath ` - -LogLevel $LogLevel ` - -AdditionalModules $AdditionalModules ` - -LanguageServiceInNamedPipe $LanguageServiceInPipeName ` - -LanguageServiceOutNamedPipe $LanguageServiceOutPipeName ` - -DebugServiceInNamedPipe $DebugServiceInPipeName ` - -DebugServiceOutNamedPipe $DebugServiceOutPipeName ` - -BundledModulesPath $BundledModulesPath ` - -UseLegacyReadLine:$UseLegacyReadLine.IsPresent ` - -EnableConsoleRepl:$EnableConsoleRepl.IsPresent ` - -DebugServiceOnly:$DebugServiceOnly.IsPresent ` - -WaitForDebugger:$WaitForDebugger.IsPresent ` - -FeatureFlags $FeatureFlags - - Set-PipeFileResult $resultDetails "languageServiceReadPipeName" $LanguageServiceInPipeName - Set-PipeFileResult $resultDetails "languageServiceWritePipeName" $LanguageServiceOutPipeName - Set-PipeFileResult $resultDetails "debugServiceReadPipeName" $DebugServiceInPipeName - Set-PipeFileResult $resultDetails "debugServiceWritePipeName" $DebugServiceOutPipeName - break - } - - Default { - $LanguageServicePipeName = Get-ValidatedNamedPipeName $LanguageServicePipeName - $DebugServicePipeName = Get-ValidatedNamedPipeName $DebugServicePipeName - - $editorServicesHost = Start-EditorServicesHost ` - -HostName $HostName ` - -HostProfileId $HostProfileId ` - -HostVersion $HostVersion ` - -LogPath $LogPath ` - -LogLevel $LogLevel ` - -AdditionalModules $AdditionalModules ` - -LanguageServiceNamedPipe $LanguageServicePipeName ` - -DebugServiceNamedPipe $DebugServicePipeName ` - -BundledModulesPath $BundledModulesPath ` - -UseLegacyReadLine:$UseLegacyReadLine.IsPresent ` - -EnableConsoleRepl:$EnableConsoleRepl.IsPresent ` - -DebugServiceOnly:$DebugServiceOnly.IsPresent ` - -WaitForDebugger:$WaitForDebugger.IsPresent ` - -FeatureFlags $FeatureFlags - - Set-PipeFileResult $resultDetails "languageServicePipeName" $LanguageServicePipeName - Set-PipeFileResult $resultDetails "debugServicePipeName" $DebugServicePipeName - break - } - } - - # TODO: Verify that the service is started - Log "Start-EditorServicesHost returned $editorServicesHost" - - $resultDetails["status"] = "started" - - # Notify the client that the services have started - WriteSessionFile $resultDetails - - Log "Wrote out session file" -} -catch [System.Exception] { - $e = $_.Exception; - $errorString = "" - - Log "ERRORS caught starting up EditorServicesHost" - - while ($null -ne $e) { - $errorString = $errorString + ($e.Message + "`r`n" + $e.StackTrace + "`r`n") - $e = $e.InnerException; - Log $errorString - } - - ExitWithError ("An error occurred while starting PowerShell Editor Services:`r`n`r`n" + $errorString) -} - -try { - # Wait for the host to complete execution before exiting - LogSection "Waiting for EditorServicesHost to complete execution" - $editorServicesHost.WaitForCompletion() - Log "EditorServicesHost has completed execution" -} -catch [System.Exception] { - $e = $_.Exception; - $errorString = "" - - Log "ERRORS caught while waiting for EditorServicesHost to complete execution" - - while ($null -ne $e) { - $errorString = $errorString + ($e.Message + "`r`n" + $e.StackTrace + "`r`n") - $e = $e.InnerException; - Log $errorString - } -} +Import-Module -Name "$PSScriptRoot/PowerShellEditorServices.psd1" +Start-EditorServices @PSBoundParameters diff --git a/src/PowerShellEditorServices.Hosting/BuildInfo.cs b/src/PowerShellEditorServices.Hosting/BuildInfo.cs new file mode 100644 index 000000000..386ac95a3 --- /dev/null +++ b/src/PowerShellEditorServices.Hosting/BuildInfo.cs @@ -0,0 +1,9 @@ +namespace Microsoft.PowerShell.EditorServices.Hosting +{ + public static class BuildInfo + { + public static readonly string BuildVersion = ""; + public static readonly string BuildOrigin = ""; + public static readonly System.DateTime? BuildTime = System.DateTime.Parse("2019-12-06T21:43:41"); + } +} diff --git a/src/PowerShellEditorServices.Hosting/Commands/StartEditorServicesCommand.cs b/src/PowerShellEditorServices.Hosting/Commands/StartEditorServicesCommand.cs new file mode 100644 index 000000000..e328308ba --- /dev/null +++ b/src/PowerShellEditorServices.Hosting/Commands/StartEditorServicesCommand.cs @@ -0,0 +1,441 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.Commands; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Management.Automation; +using System.Reflection; +using SMA = System.Management.Automation; +using Microsoft.PowerShell.EditorServices.Hosting; + +#if DEBUG +using System.Diagnostics; +using System.Threading; + +using Debugger = System.Diagnostics.Debugger; +#endif + +namespace Microsoft.PowerShell.EditorServices.Commands +{ + /// + /// The Start-EditorServices command, the conventional entrypoint for PowerShell Editor Services. + /// + [Cmdlet(VerbsLifecycle.Start, "EditorServices", DefaultParameterSetName = "NamedPipe")] + public sealed class StartEditorServicesCommand : PSCmdlet + { + private readonly List _disposableResources; + + private readonly List _loggerUnsubscribers; + + private HostLogger _logger; + + public StartEditorServicesCommand() + { + _disposableResources = new List(); + _loggerUnsubscribers = new List(); + } + + /// + /// The name of the EditorServices host to report. + /// + [Parameter(Mandatory = true)] + [ValidateNotNullOrEmpty] + public string HostName { get; set; } + + /// + /// The ID to give to the host's profile. + /// + [Parameter(Mandatory = true)] + [ValidateNotNullOrEmpty] + public string HostProfileId { get; set; } + + /// + /// The version to report for the host. + /// + [Parameter(Mandatory = true)] + [ValidateNotNullOrEmpty] + public Version HostVersion { get; set; } + + /// + /// Path to the session file to create on startup or startup failure. + /// + [Parameter(Mandatory = true)] + [ValidateNotNullOrEmpty] + public string SessionDetailsPath { get; set; } + + /// + /// The name of the named pipe to use for the LSP transport. + /// If left unset and named pipes are used as transport, a new name will be generated. + /// + [Parameter(ParameterSetName = "NamedPipe")] + public string LanguageServicePipeName { get; set; } + + /// + /// The name of the named pipe to use for the debug adapter transport. + /// If left unset and named pipes are used as a transport, a new name will be generated. + /// + [Parameter(ParameterSetName = "NamedPipe")] + public string DebugServicePipeName { get; set; } + + /// + /// The name of the input named pipe to use for the LSP transport. + /// + [Parameter(ParameterSetName = "NamedPipeSimplex")] + public string LanguageServiceInPipeName { get; set; } + + /// + /// The name of the output named pipe to use for the LSP transport. + /// + [Parameter(ParameterSetName = "NamedPipeSimplex")] + public string LanguageServiceOutPipeName { get; set; } + + /// + /// The name of the input pipe to use for the debug adapter transport. + /// + [Parameter(ParameterSetName = "NamedPipeSimplex")] + public string DebugServiceInPipeName { get; set; } + + /// + /// The name of the output pipe to use for the debug adapter transport. + /// + [Parameter(ParameterSetName = "NamedPipeSimplex")] + public string DebugServiceOutPipeName { get; set; } + + /// + /// If set, uses standard input/output as the LSP transport. + /// When is set with this, standard input/output + /// is used as the debug adapter transport. + /// + [Parameter(ParameterSetName = "Stdio")] + public SwitchParameter Stdio { get; set; } + + /// + /// The path to where PowerShellEditorServices and its bundled modules are. + /// + [Parameter] + [ValidateNotNullOrEmpty] + public string BundledModulesPath { get; set; } + + /// + /// The absolute path to the where the editor services log file should be created and logged to. + /// + [Parameter] + [ValidateNotNullOrEmpty] + public string LogPath { get; set; } + + /// + /// The minimum log level that should be emitted. + /// + [Parameter] + public PsesLogLevel LogLevel { get; set; } = PsesLogLevel.Normal; + + /// + /// Paths to additional PowerShell modules to be imported at startup. + /// + [Parameter] + public string[] AdditionalModules { get; set; } + + /// + /// Any feature flags to enable in EditorServices. + /// + [Parameter] + public string[] FeatureFlags { get; set; } + + /// + /// When set, enables the integrated console. + /// + [Parameter] + public SwitchParameter EnableConsoleRepl { get; set; } + + /// + /// When set and the console is enabled, the legacy lightweight + /// readline implementation will be used instead of PSReadLine. + /// + [Parameter] + public SwitchParameter UseLegacyReadLine { get; set; } + + /// + /// When set, do not enable LSP service, only the debug adapter. + /// + [Parameter] + public SwitchParameter DebugServiceOnly { get; set; } + + /// + /// When set with a debug build, startup will wait for a debugger to attach. + /// + [Parameter] + public SwitchParameter WaitForDebugger { get; set; } + + /// + /// When set, will generate two simplex named pipes using a single named pipe name. + /// + [Parameter] + public SwitchParameter SplitInOutPipes { get; set; } + + protected override void BeginProcessing() + { +#if DEBUG + if (WaitForDebugger) + { + while (!Debugger.IsAttached) + { + Console.WriteLine($"PID: {Process.GetCurrentProcess().Id}"); + Thread.Sleep(1000); + } + } +#endif + + // Set up logging now for use throughout startup + StartLogging(); + } + + protected override void EndProcessing() + { + _logger.Log(PsesLogLevel.Diagnostic, "Beginning EndProcessing block"); + + try + { + // First try to remove PSReadLine to decomplicate startup + // If PSReadLine is enabled, it will be re-imported later + RemovePSReadLineForStartup(); + + // Create the configuration from parameters + EditorServicesConfig editorServicesConfig = CreateConfigObject(); + + var sessionFileWriter = new SessionFileWriter(_logger, SessionDetailsPath); + _logger.Log(PsesLogLevel.Diagnostic, "Session file writer created"); + + using (var psesLoader = EditorServicesLoader.Create(_logger, editorServicesConfig, sessionFileWriter, _loggerUnsubscribers)) + { + _logger.Log(PsesLogLevel.Verbose, "Loading EditorServices"); + psesLoader.LoadAndRunEditorServicesAsync().Wait(); + } + } + catch (Exception e) + { + _logger.LogException("Exception encountered starting EditorServices", e); + + // Give the user a chance to read the message if they have a console + if (!Stdio) + { + Host.UI.WriteLine("\n== Press any key to close terminal =="); + Console.ReadKey(); + } + + ThrowTerminatingError(new ErrorRecord(e, "PowerShellEditorServicesError", ErrorCategory.NotSpecified, this)); + } + finally + { + foreach (IDisposable disposableResource in _disposableResources) + { + disposableResource.Dispose(); + } + } + } + + private void StartLogging() + { + _logger = new HostLogger(LogLevel); + + // We need to not write log messages to Stdio + // if it's being used as a protocol transport + if (!Stdio) + { + var hostLogger = new PSHostLogger(Host.UI); + _loggerUnsubscribers.Add(_logger.Subscribe(hostLogger)); + } + + string logPath = Path.Combine(GetLogDirPath(), "StartEditorServices.log"); + var fileLogger = StreamLogger.CreateWithNewFile(logPath); + _disposableResources.Add(fileLogger); + IDisposable fileLoggerUnsubscriber = _logger.Subscribe(fileLogger); + fileLogger.AddUnsubscriber(fileLoggerUnsubscriber); + _loggerUnsubscribers.Add(fileLoggerUnsubscriber); + + _logger.Log(PsesLogLevel.Diagnostic, "Logging started"); + } + + private string GetLogDirPath() + { + if (!string.IsNullOrEmpty(LogPath)) + { + return Path.GetDirectoryName(LogPath); + } + + return Path.GetDirectoryName( + Path.GetDirectoryName( + Assembly.GetExecutingAssembly().Location)); + } + + private void RemovePSReadLineForStartup() + { + _logger.Log(PsesLogLevel.Verbose, "Removing PSReadLine"); + using (var pwsh = SMA.PowerShell.Create(RunspaceMode.CurrentRunspace)) + { + bool hasPSReadLine = pwsh.AddCommand(new CmdletInfo("Microsoft.PowerShell.Core\\Get-Module", typeof(GetModuleCommand))) + .AddParameter("Name", "PSReadLine") + .Invoke() + .Any(); + + if (hasPSReadLine) + { + pwsh.Commands.Clear(); + + pwsh.AddCommand(new CmdletInfo("Microsoft.PowerShell.Core\\Remove-Module", typeof(RemoveModuleCommand))) + .AddParameter("Name", "PSReadLine") + .AddParameter("ErrorAction", "SilentlyContinue"); + + _logger.Log(PsesLogLevel.Verbose, "Removed PSReadLine"); + } + } + } + + private EditorServicesConfig CreateConfigObject() + { + _logger.Log(PsesLogLevel.Diagnostic, "Creating host configuration"); + + string bundledModulesPath = BundledModulesPath; + if (!Path.IsPathRooted(bundledModulesPath)) + { + // For compatibility, the bundled modules path is relative to the PSES bin directory + // Ideally it should be one level up, the PSES module root + bundledModulesPath = Path.GetFullPath( + Path.Combine( + Assembly.GetExecutingAssembly().Location, + "..", + bundledModulesPath)); + } + + var profile = (PSObject)GetVariableValue("profile"); + + var hostInfo = new HostInfo(HostName, HostProfileId, HostVersion); + var editorServicesConfig = new EditorServicesConfig(hostInfo, Host, SessionDetailsPath, bundledModulesPath, LogPath) + { + FeatureFlags = FeatureFlags, + LogLevel = LogLevel, + ConsoleRepl = GetReplKind(), + AdditionalModules = AdditionalModules, + LanguageServiceTransport = GetLanguageServiceTransport(), + DebugServiceTransport = GetDebugServiceTransport(), + ProfilePaths = new ProfilePathConfig + { + AllUsersAllHosts = GetProfilePathFromProfileObject(profile, ProfileUserKind.AllUsers, ProfileHostKind.AllHosts), + AllUsersCurrentHost = GetProfilePathFromProfileObject(profile, ProfileUserKind.AllUsers, ProfileHostKind.CurrentHost), + CurrentUserAllHosts = GetProfilePathFromProfileObject(profile, ProfileUserKind.CurrentUser, ProfileHostKind.AllHosts), + CurrentUserCurrentHost = GetProfilePathFromProfileObject(profile, ProfileUserKind.CurrentUser, ProfileHostKind.CurrentHost), + }, + }; + + return editorServicesConfig; + } + + private string GetProfilePathFromProfileObject(PSObject profileObject, ProfileUserKind userKind, ProfileHostKind hostKind) + { + string profilePathName = $"{userKind}{hostKind}"; + + string pwshProfilePath = (string)profileObject.Properties[profilePathName].Value; + + if (hostKind == ProfileHostKind.AllHosts) + { + return pwshProfilePath; + } + + return Path.Combine( + Path.GetDirectoryName(pwshProfilePath), + $"{HostProfileId}_profile.ps1"); + } + + private ConsoleReplKind GetReplKind() + { + _logger.Log(PsesLogLevel.Diagnostic, "Determining REPL kind"); + + if (Stdio || !EnableConsoleRepl) + { + _logger.Log(PsesLogLevel.Diagnostic, "REPL configured as None"); + return ConsoleReplKind.None; + } + + if (UseLegacyReadLine) + { + _logger.Log(PsesLogLevel.Diagnostic, "REPL configured as Legacy"); + return ConsoleReplKind.LegacyReadLine; + } + + _logger.Log(PsesLogLevel.Diagnostic, "REPL configured as PSReadLine"); + return ConsoleReplKind.PSReadLine; + } + + private ITransportConfig GetLanguageServiceTransport() + { + _logger.Log(PsesLogLevel.Diagnostic, "Configuring LSP transport"); + + if (DebugServiceOnly) + { + _logger.Log(PsesLogLevel.Diagnostic, "No LSP transport: PSES is debug only"); + return null; + } + + if (Stdio) + { + return new StdioTransportConfig(); + } + + if (LanguageServiceInPipeName != null && LanguageServiceOutPipeName != null) + { + return SimplexNamedPipeTransportConfig.Create(LanguageServiceInPipeName, LanguageServiceOutPipeName); + } + + if (SplitInOutPipes) + { + return SimplexNamedPipeTransportConfig.Create(LanguageServicePipeName); + } + + return DuplexNamedPipeTransportConfig.Create(LanguageServicePipeName); + } + + private ITransportConfig GetDebugServiceTransport() + { + _logger.Log(PsesLogLevel.Diagnostic, "Configuring debug transport"); + + if (Stdio) + { + if (DebugServiceOnly) + { + return new StdioTransportConfig(); + } + + _logger.Log(PsesLogLevel.Diagnostic, "No debug transport: Transport is Stdio with debug disabled"); + return null; + } + + if (DebugServiceInPipeName != null && DebugServiceOutPipeName != null) + { + return SimplexNamedPipeTransportConfig.Create(DebugServiceInPipeName, DebugServiceOutPipeName); + } + + if (SplitInOutPipes) + { + return SimplexNamedPipeTransportConfig.Create(DebugServicePipeName); + } + + return DuplexNamedPipeTransportConfig.Create(DebugServicePipeName); + } + + private enum ProfileHostKind + { + AllHosts, + CurrentHost, + } + + private enum ProfileUserKind + { + AllUsers, + CurrentUser, + } + } +} diff --git a/src/PowerShellEditorServices.Hosting/Configuration/EditorServicesConfig.cs b/src/PowerShellEditorServices.Hosting/Configuration/EditorServicesConfig.cs new file mode 100644 index 000000000..05630324b --- /dev/null +++ b/src/PowerShellEditorServices.Hosting/Configuration/EditorServicesConfig.cs @@ -0,0 +1,140 @@ +// +// 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.Host; + +namespace Microsoft.PowerShell.EditorServices.Hosting +{ + /// + /// Describes the desired console REPL for the integrated console. + /// + public enum ConsoleReplKind + { + /// No console REPL - there will be no interactive console available. + None = 0, + /// Use a REPL with the legacy readline implementation. This is generally used when PSReadLine is unavailable. + LegacyReadLine = 1, + /// Use a REPL with the PSReadLine module for console interaction. + PSReadLine = 2, + } + + /// + /// Configuration for editor services startup. + /// + public sealed class EditorServicesConfig + { + /// + /// Create a new editor services config object, + /// with all required fields. + /// + /// The host description object. + /// The PowerShell host to use in Editor Services. + /// The path to use for the session details file. + /// The path to the modules bundled with Editor Services. + /// The path to be used for Editor Services' logging. + public EditorServicesConfig( + HostInfo hostInfo, + PSHost psHost, + string sessionDetailsPath, + string bundledModulePath, + string logPath) + { + HostInfo = hostInfo; + PSHost = psHost; + SessionDetailsPath = sessionDetailsPath; + BundledModulePath = bundledModulePath; + LogPath = logPath; + } + + /// + /// The host description object. + /// + public HostInfo HostInfo { get; } + + /// + /// The PowerShell host used by Editor Services. + /// + public PSHost PSHost { get; } + + /// + /// The path to use for the session details file. + /// + public string SessionDetailsPath { get; } + + /// + /// The path to the modules bundled with EditorServices. + /// + public string BundledModulePath { get; } + + /// + /// The path to use for logging for Editor Services. + /// + public string LogPath { get; } + + /// + /// Names of or paths to any additional modules to load on startup. + /// + public IReadOnlyList AdditionalModules { get; set; } = null; + + /// + /// Flags of features to enable on startup. + /// + public IReadOnlyList FeatureFlags { get; set; } = null; + + /// + /// The console REPL experience to use in the integrated console + /// (including none to disable the integrated console). + /// + public ConsoleReplKind ConsoleRepl { get; set; } = ConsoleReplKind.None; + + /// + /// The minimum log level to log events with. + /// + public PsesLogLevel LogLevel { get; set; } = PsesLogLevel.Normal; + + /// + /// Configuration for the language server protocol transport to use. + /// + public ITransportConfig LanguageServiceTransport { get; set; } = null; + + /// + /// Configuration for the debug adapter protocol transport to use. + /// + public ITransportConfig DebugServiceTransport { get; set; } = null; + + /// + /// PowerShell profile locations for Editor Services to use for its profiles. + /// If none are provided, these will be generated from the hosting PowerShell's profile paths. + /// + public ProfilePathConfig ProfilePaths { get; set; } + } + + /// + /// Configuration for Editor Services' PowerShell profile paths. + /// + public struct ProfilePathConfig + { + /// + /// The path to the profile shared by all users across all PowerShell hosts. + /// + public string AllUsersAllHosts { get; set; } + + /// + /// The path to the profile shared by all users specific to this PSES host. + /// + public string AllUsersCurrentHost { get; set; } + + /// + /// The path to the profile specific to the current user across all hosts. + /// + public string CurrentUserAllHosts { get; set; } + + /// + /// The path to the profile specific to the current user and to this PSES host. + /// + public string CurrentUserCurrentHost { get; set; } + } +} diff --git a/src/PowerShellEditorServices.Hosting/Configuration/HostInfo.cs b/src/PowerShellEditorServices.Hosting/Configuration/HostInfo.cs new file mode 100644 index 000000000..465f7ef0d --- /dev/null +++ b/src/PowerShellEditorServices.Hosting/Configuration/HostInfo.cs @@ -0,0 +1,43 @@ +// +// 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.Hosting +{ + /// + /// A simple readonly object to describe basic host metadata. + /// + public class HostInfo + { + /// + /// Create a new host info object. + /// + /// The name of the host. + /// The profile ID of the host. + /// The version of the host. + public HostInfo(string name, string profileId, Version version) + { + Name = name; + ProfileId = profileId; + Version = version; + } + + /// + /// The name of the host. + /// + public string Name { get; } + + /// + /// The profile ID of the host. + /// + public string ProfileId { get; } + + /// + /// The version of the host. + /// + public Version Version { get; } + } +} diff --git a/src/PowerShellEditorServices.Hosting/Configuration/HostLogger.cs b/src/PowerShellEditorServices.Hosting/Configuration/HostLogger.cs new file mode 100644 index 000000000..6882783e7 --- /dev/null +++ b/src/PowerShellEditorServices.Hosting/Configuration/HostLogger.cs @@ -0,0 +1,370 @@ +// +// 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.Concurrent; +using System.IO; +using System.Management.Automation.Host; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Hosting +{ + /// + /// User-facing log level for editor services configuration. + /// The underlying values of this enum align to both Microsoft.Logging.Extensions.LogLevel + /// and Serilog.Events.LogEventLevel. + /// + public enum PsesLogLevel + { + Diagnostic = 0, + Verbose = 1, + Normal = 2, + Warning = 3, + Error = 4, + } + + /// + /// A logging front-end for host startup allowing handover to the backend + /// and decoupling from the host's particular logging sink. + /// + public class HostLogger : + IObservable<(PsesLogLevel logLevel, string message)>, + IObservable<(int logLevel, string message)> + { + /// + /// A simple translation struct to convert PsesLogLevel to an int for backend passthrough. + /// + private class LogObserver : IObserver<(PsesLogLevel logLevel, string message)> + { + private readonly IObserver<(int logLevel, string message)> _observer; + + public LogObserver(IObserver<(int logLevel, string message)> observer) + { + _observer = observer; + } + + public void OnCompleted() + { + _observer.OnCompleted(); + } + + public void OnError(Exception error) + { + _observer.OnError(error); + } + + public void OnNext((PsesLogLevel logLevel, string message) value) + { + _observer.OnNext(((int)value.logLevel, value.message)); + } + } + + /// + /// Simple unsubscriber that allows subscribers to remove themselves from the observer list later. + /// + private class Unsubscriber : IDisposable + { + private readonly ConcurrentDictionary, bool> _subscribedObservers; + + private readonly IObserver<(PsesLogLevel, string)> _thisSubscriber; + + + public Unsubscriber(ConcurrentDictionary, bool> subscribedObservers, IObserver<(PsesLogLevel, string)> thisSubscriber) + { + _subscribedObservers = subscribedObservers; + _thisSubscriber = thisSubscriber; + } + + public void Dispose() + { + _subscribedObservers.TryRemove(_thisSubscriber, out bool _); + } + } + + private readonly PsesLogLevel _minimumLogLevel; + + private readonly ConcurrentQueue<(PsesLogLevel logLevel, string message)> _logMessages; + + // The bool value here is meaningless and ignored, + // the ConcurrentDictionary just provides a way to efficiently keep track of subscribers across threads + private readonly ConcurrentDictionary, bool> _observers; + + /// + /// Construct a new logger in the host. + /// + /// The minimum log level to log. + public HostLogger(PsesLogLevel minimumLogLevel) + { + _minimumLogLevel = minimumLogLevel; + _logMessages = new ConcurrentQueue<(PsesLogLevel logLevel, string message)>(); + _observers = new ConcurrentDictionary, bool>(); + } + + /// + /// Subscribe a new log sink. + /// + /// The log sink to subscribe. + /// A disposable unsubscribe object. + public IDisposable Subscribe(IObserver<(PsesLogLevel logLevel, string message)> observer) + { + if (observer == null) + { + throw new ArgumentNullException(nameof(observer)); + } + + _observers[observer] = true; + + // Catch up a late subscriber to messages already logged + foreach ((PsesLogLevel logLevel, string message) entry in _logMessages) + { + observer.OnNext(entry); + } + + return new Unsubscriber(_observers, observer); + } + + /// + /// Subscribe a new log sink. + /// + /// The log sink to subscribe. + /// A disposable unsubscribe object. + public IDisposable Subscribe(IObserver<(int logLevel, string message)> observer) + { + if (observer == null) + { + throw new ArgumentNullException(nameof(observer)); + } + + return Subscribe(new LogObserver(observer)); + } + + /// + /// Log a message to log sinks. + /// + /// The log severity level of message to log. + /// The message to log. + public void Log(PsesLogLevel logLevel, string message) + { + // Do nothing if the severity is lower than the minimum + if (logLevel < _minimumLogLevel) + { + return; + } + + // Remember this for later subscriptions + _logMessages.Enqueue((logLevel, message)); + + // Send this log to all observers + foreach (IObserver<(PsesLogLevel logLevel, string message)> observer in _observers.Keys) + { + observer.OnNext((logLevel, message)); + } + } + + /// + /// Convenience method for logging exceptions. + /// + /// The human-directed message to accompany the exception. + /// The actual execption to log. + /// The name of the calling method. + /// The name of the file where this is logged. + /// The line in the file where this is logged. + public void LogException( + string message, + Exception exception, + [CallerMemberName] string callerName = null, + [CallerFilePath] string callerSourceFile = null, + [CallerLineNumber] int callerLineNumber = -1) + { + Log(PsesLogLevel.Error, $"{message}. Exception logged in {callerSourceFile} on line {callerLineNumber} in {callerName}:\n{exception}"); + } + + } + + /// + /// A log sink to direct log messages back to the PowerShell host. + /// + /// + /// Note that calling this through the cmdlet causes an error, + /// so instead we log directly to the host. + /// Since it's likely that the process will end when PSES shuts down, + /// there's no good reason to need objects rather than writing directly to the host. + /// + internal class PSHostLogger : IObserver<(PsesLogLevel logLevel, string message)> + { + private readonly PSHostUserInterface _ui; + + /// + /// Create a new PowerShell host logger. + /// + /// The PowerShell host user interface object to log output to. + public PSHostLogger(PSHostUserInterface ui) + { + _ui = ui; + } + + public void OnCompleted() + { + // No-op since there's nothing to close or dispose, + // we just stop writing to the host + } + + public void OnError(Exception error) + { + OnNext((PsesLogLevel.Error, $"Error occurred while logging: {error}")); + } + + public void OnNext((PsesLogLevel logLevel, string message) value) + { + switch (value.logLevel) + { + case PsesLogLevel.Diagnostic: + _ui.WriteDebugLine(value.message); + return; + + case PsesLogLevel.Verbose: + _ui.WriteVerboseLine(value.message); + return; + + case PsesLogLevel.Normal: + _ui.WriteLine(value.message); + return; + + case PsesLogLevel.Warning: + _ui.WriteWarningLine(value.message); + return; + + case PsesLogLevel.Error: + _ui.WriteErrorLine(value.message); + return; + + default: + _ui.WriteLine(value.message); + return; + } + } + } + + internal class StreamLogger : IObserver<(PsesLogLevel logLevel, string message)>, IDisposable + { + public static StreamLogger CreateWithNewFile(string path) + { + var fileStream = new FileStream( + path, + FileMode.Create, + FileAccess.Write, + FileShare.Read, + bufferSize: 4096, + FileOptions.SequentialScan); + + return new StreamLogger(new StreamWriter(fileStream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true))); + } + + private readonly StreamWriter _fileWriter; + + private readonly BlockingCollection _messageQueue; + + private readonly CancellationTokenSource _cancellationSource; + + private readonly Task _writerTask; + + // This cannot be a bool + // See https://stackoverflow.com/q/6164751 + private int _hasCompleted; + + private IDisposable _unsubscriber; + + public StreamLogger(StreamWriter streamWriter) + { + streamWriter.AutoFlush = true; + _fileWriter = streamWriter; + _hasCompleted = 0; + _cancellationSource = new CancellationTokenSource(); + _messageQueue = new BlockingCollection(); + + // Start writer listening to queue + _writerTask = Task.Run(RunWriter); + } + + public void OnCompleted() + { + // Ensure we only complete once + if (Interlocked.Exchange(ref _hasCompleted, 1) != 0) + { + return; + } + + _cancellationSource.Cancel(); + + _writerTask.Wait(); + + _unsubscriber.Dispose(); + _fileWriter.Flush(); + _fileWriter.Close(); + _fileWriter.Dispose(); + } + + public void OnError(Exception error) + { + OnNext((PsesLogLevel.Error, $"Error occurred while logging: {error}")); + } + + public void OnNext((PsesLogLevel logLevel, string message) value) + { + string message = null; + switch (value.logLevel) + { + case PsesLogLevel.Diagnostic: + message = $"[DBG]: {value.message}"; + break; + + case PsesLogLevel.Verbose: + message = $"[VRB]: {value.message}"; + break; + + case PsesLogLevel.Normal: + message = $"[INF]: {value.message}"; + break; + + case PsesLogLevel.Warning: + message = $"[WRN]: {value.message}"; + break; + + case PsesLogLevel.Error: + message = $"[ERR]: {value.message}"; + break; + }; + + _messageQueue.Add(message); + } + + public void AddUnsubscriber(IDisposable unsubscriber) + { + _unsubscriber = unsubscriber; + } + + public void Dispose() + { + OnCompleted(); + } + + private void RunWriter() + { + try + { + foreach (string logMessage in _messageQueue.GetConsumingEnumerable(_cancellationSource.Token)) + { + _fileWriter.WriteLine(logMessage); + } + } + catch (TaskCanceledException) + { + } + } + } +} diff --git a/src/PowerShellEditorServices.Hosting/Configuration/SessionFileWriter.cs b/src/PowerShellEditorServices.Hosting/Configuration/SessionFileWriter.cs new file mode 100644 index 000000000..12842d618 --- /dev/null +++ b/src/PowerShellEditorServices.Hosting/Configuration/SessionFileWriter.cs @@ -0,0 +1,151 @@ +// +// 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.IO; +using System.Management.Automation; +using System.Text; +using SMA = System.Management.Automation; + +namespace Microsoft.PowerShell.EditorServices.Hosting +{ + /// + /// Writes the session file when the server is ready for a connection, + /// so that the client can connect. + /// + public interface ISessionFileWriter + { + /// + /// Write a session file describing a failed startup. + /// + /// The reason for the startup failure. + /// Any details to accompany the reason. + void WriteSessionFailure(string reason, object details); + + /// + /// Write a session file describing a successful startup. + /// + /// The transport configuration for the LSP service. + /// The transport configuration for the debug adapter service. + void WriteSessionStarted(ITransportConfig languageServiceTransport, ITransportConfig debugAdapterTransport); + } + + /// + /// The default session file writer, which uses PowerShell to write a session file. + /// + public class SessionFileWriter : ISessionFileWriter + { + // Use BOM-less UTF-8 for session file + private static readonly Encoding s_sessionFileEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + + private readonly HostLogger _logger; + + private readonly string _sessionFilePath; + + /// + /// Construct a new session file writer for the given session file path. + /// + /// The logger to log actions with. + /// The path to write the session file path to. + public SessionFileWriter(HostLogger logger, string sessionFilePath) + { + _logger = logger; + _sessionFilePath = sessionFilePath; + } + + /// + /// Write a startup failure to the session file. + /// + /// The reason for the startup failure. + /// Any extra details, which will be serialized as JSON. + public void WriteSessionFailure(string reason, object details) + { + _logger.Log(PsesLogLevel.Diagnostic, "Writing session failure"); + + var sessionObject = new Dictionary + { + { "status", "failed" }, + { "reason", reason }, + }; + + if (details != null) + { + sessionObject["details"] = details; + } + + WriteSessionObject(sessionObject); + } + + /// + /// Write a successful server startup to the session file. + /// + /// The LSP service transport configuration. + /// The debug adapter transport configuration. + public void WriteSessionStarted(ITransportConfig languageServiceTransport, ITransportConfig debugAdapterTransport) + { + _logger.Log(PsesLogLevel.Diagnostic, "Writing session started"); + + var sessionObject = new Dictionary + { + { "status", "started" }, + }; + + if (languageServiceTransport != null) + { + sessionObject["languageServiceTransport"] = languageServiceTransport.SessionFileTransportName; + + if (languageServiceTransport.SessionFileEntries != null) + { + foreach (KeyValuePair sessionEntry in languageServiceTransport.SessionFileEntries) + { + sessionObject[$"languageService{sessionEntry.Key}"] = sessionEntry.Value; + } + } + } + + if (debugAdapterTransport != null) + { + sessionObject["debugServiceTransport"] = debugAdapterTransport.SessionFileTransportName; + + if (debugAdapterTransport.SessionFileEntries != null) + { + foreach (KeyValuePair sessionEntry in debugAdapterTransport.SessionFileEntries) + { + sessionObject[$"debugService{sessionEntry.Key}"] = sessionEntry.Value; + } + } + } + + WriteSessionObject(sessionObject); + } + + /// + /// Write the object representing the session file to the file by serializing it as JSON. + /// + /// The dictionary representing the session file. + private void WriteSessionObject(Dictionary sessionObject) + { + string psModulePath = Environment.GetEnvironmentVariable("PSModulePath"); + string content = null; + using (var pwsh = SMA.PowerShell.Create(RunspaceMode.NewRunspace)) + { + content = pwsh.AddCommand("ConvertTo-Json") + .AddParameter("InputObject", sessionObject) + .AddParameter("Depth", 10) + .AddParameter("Compress") + .Invoke()[0]; + + // Runspace creation has a bug where it resets the PSModulePath, + // which we must correct for + Environment.SetEnvironmentVariable("PSModulePath", psModulePath); + + File.WriteAllText(_sessionFilePath, content, s_sessionFileEncoding); + } + + _logger.Log(PsesLogLevel.Verbose, $"Session file written to {_sessionFilePath} with content:\n{content}"); + } + } +} diff --git a/src/PowerShellEditorServices.Hosting/Configuration/TransportConfig.cs b/src/PowerShellEditorServices.Hosting/Configuration/TransportConfig.cs new file mode 100644 index 000000000..072f2dc6d --- /dev/null +++ b/src/PowerShellEditorServices.Hosting/Configuration/TransportConfig.cs @@ -0,0 +1,185 @@ +// +// 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.IO; +using System.IO.Pipes; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Hosting +{ + /// + /// Configuration specifying an editor services protocol transport stream configuration. + /// + public interface ITransportConfig + { + /// + /// Create, connect and return the configured transport streams. + /// + /// The connected transport streams. inStream and outStream may be the same stream for duplex streams. + Task<(Stream inStream, Stream outStream)> ConnectStreamsAsync(); + + /// + /// The name of the transport endpoint for logging. + /// + string EndpointDetails { get; } + + /// + /// The name of the transport to record in the session file. + /// + string SessionFileTransportName { get; } + + /// + /// Extra entries to record in the session file. + /// + IReadOnlyDictionary SessionFileEntries { get; } + } + + /// + /// Configuration for the standard input/output transport. + /// + public class StdioTransportConfig : ITransportConfig + { + public string EndpointDetails => ""; + + public string SessionFileTransportName => "Stdio"; + + public IReadOnlyDictionary SessionFileEntries { get; } = null; + + public Task<(Stream inStream, Stream outStream)> ConnectStreamsAsync() + { + return Task.FromResult((Console.OpenStandardInput(), Console.OpenStandardOutput())); + } + } + + /// + /// Configuration for a full duplex named pipe. + /// + public class DuplexNamedPipeTransportConfig : ITransportConfig + { + /// + /// Create a duplex named pipe transport config with an automatically generated pipe name. + /// + /// A new duplex named pipe transport configuration. + public static DuplexNamedPipeTransportConfig Create() + { + return new DuplexNamedPipeTransportConfig(NamedPipeUtils.GenerateValidNamedPipeName()); + } + + /// + /// Create a duplex named pipe transport config with the given pipe name. + /// + /// A new duplex named pipe transport configuration. + public static DuplexNamedPipeTransportConfig Create(string pipeName) + { + if (pipeName == null) + { + return DuplexNamedPipeTransportConfig.Create(); + } + + return new DuplexNamedPipeTransportConfig(pipeName); + } + + private readonly string _pipeName; + + private DuplexNamedPipeTransportConfig(string pipeName) + { + _pipeName = pipeName; + SessionFileEntries = new Dictionary{ { "PipeName", NamedPipeUtils.GetNamedPipePath(pipeName) } }; + } + + public string EndpointDetails => $"InOut pipe: {_pipeName}"; + + public string SessionFileTransportName => "NamedPipe"; + + public IReadOnlyDictionary SessionFileEntries { get; } + + public async Task<(Stream inStream, Stream outStream)> ConnectStreamsAsync() + { + NamedPipeServerStream namedPipe = NamedPipeUtils.CreateNamedPipe(_pipeName, PipeDirection.InOut); + await namedPipe.WaitForConnectionAsync().ConfigureAwait(false); + return (namedPipe, namedPipe); + } + } + + /// + /// Configuration for two simplex named pipes. + /// + public class SimplexNamedPipeTransportConfig : ITransportConfig + { + private const string InPipePrefix = "in"; + private const string OutPipePrefix = "out"; + + /// + /// Create a pair of simplex named pipes using generated names. + /// + /// A new simplex named pipe transport config. + public static SimplexNamedPipeTransportConfig Create() + { + return SimplexNamedPipeTransportConfig.Create(NamedPipeUtils.GenerateValidNamedPipeName(new[] { InPipePrefix, OutPipePrefix })); + } + + /// + /// Create a pair of simplex named pipes using the given name as a base. + /// + /// A new simplex named pipe transport config. + public static SimplexNamedPipeTransportConfig Create(string pipeNameBase) + { + if (pipeNameBase == null) + { + return SimplexNamedPipeTransportConfig.Create(); + } + + string inPipeName = $"{InPipePrefix}_{pipeNameBase}"; + string outPipeName = $"{OutPipePrefix}_{pipeNameBase}"; + + return SimplexNamedPipeTransportConfig.Create(inPipeName, outPipeName); + } + + /// + /// Create a pair of simplex named pipes using the given names. + /// + /// A new simplex named pipe transport config. + public static SimplexNamedPipeTransportConfig Create(string inPipeName, string outPipeName) + { + return new SimplexNamedPipeTransportConfig(inPipeName, outPipeName); + } + + private readonly string _inPipeName; + private readonly string _outPipeName; + + private SimplexNamedPipeTransportConfig(string inPipeName, string outPipeName) + { + _inPipeName = inPipeName; + _outPipeName = outPipeName; + + SessionFileEntries = new Dictionary + { + { "ReadPipeName", NamedPipeUtils.GetNamedPipePath(inPipeName) }, + { "WritePipeName", NamedPipeUtils.GetNamedPipePath(outPipeName) }, + }; + } + + public string EndpointDetails => $"In pipe: {_inPipeName} Out pipe: {_outPipeName}"; + + public string SessionFileTransportName => "NamedPipeSimplex"; + + public IReadOnlyDictionary SessionFileEntries { get; } + + public async Task<(Stream inStream, Stream outStream)> ConnectStreamsAsync() + { + NamedPipeServerStream inPipe = NamedPipeUtils.CreateNamedPipe(_inPipeName, PipeDirection.InOut); + Task inPipeConnected = inPipe.WaitForConnectionAsync(); + + NamedPipeServerStream outPipe = NamedPipeUtils.CreateNamedPipe(_outPipeName, PipeDirection.Out); + Task outPipeConnected = outPipe.WaitForConnectionAsync(); + + await Task.WhenAll(inPipeConnected, outPipeConnected).ConfigureAwait(false); + + return (inPipe, outPipe); + } + } +} diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs new file mode 100644 index 000000000..32919433d --- /dev/null +++ b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs @@ -0,0 +1,383 @@ +// +// 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.IO; +using System.Reflection; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +using SMA = System.Management.Automation; + +#if CoreCLR +using System.Runtime.Loader; +#else +using Microsoft.Win32; +#endif + +namespace Microsoft.PowerShell.EditorServices.Hosting +{ + /// + /// Class to contain the loading behavior of Editor Services. + /// In particular, this class wraps the point where Editor Services is safely loaded + /// in a way that separates its dependencies from the calling context. + /// + public sealed class EditorServicesLoader : IDisposable + { + private const int Net461Version = 394254; + + private static readonly string s_psesDependencyDirPath = Path.GetFullPath( + Path.Combine( + Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), + "..", + "Common")); + + /// + /// Create a new Editor Services loader. + /// + /// The host logger to use. + /// The host configuration to start editor services with. + /// The session file writer to write the session file with. + /// + public static EditorServicesLoader Create( + HostLogger logger, + EditorServicesConfig hostConfig, + ISessionFileWriter sessionFileWriter) => Create(logger, hostConfig, sessionFileWriter, loggersToUnsubscribe: null); + + /// + /// Create a new Editor Services loader. + /// + /// The host logger to use. + /// The host configuration to start editor services with. + /// The session file writer to write the session file with. + /// + public static EditorServicesLoader Create( + HostLogger logger, + EditorServicesConfig hostConfig, + ISessionFileWriter sessionFileWriter, + IReadOnlyCollection loggersToUnsubscribe) + { + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } + + if (hostConfig == null) + { + throw new ArgumentNullException(nameof(hostConfig)); + } + +#if CoreCLR + // In .NET Core, we add an event here to redirect dependency loading to the new AssemblyLoadContext we load PSES' dependencies into + + logger.Log(PsesLogLevel.Verbose, "Adding AssemblyResolve event handler for new AssemblyLoadContext dependency loading"); + + var psesLoadContext = new PsesLoadContext(s_psesDependencyDirPath); + + if (hostConfig.LogLevel == PsesLogLevel.Diagnostic) + { + AppDomain.CurrentDomain.AssemblyLoad += (object sender, AssemblyLoadEventArgs args) => + { + logger.Log( + PsesLogLevel.Diagnostic, + $"Loaded into load context {AssemblyLoadContext.GetLoadContext(args.LoadedAssembly)}: {args.LoadedAssembly}"); + }; + } + + AssemblyLoadContext.Default.Resolving += (AssemblyLoadContext defaultLoadContext, AssemblyName asmName) => + { + logger.Log(PsesLogLevel.Diagnostic, $"Assembly resolve event fired for {asmName}"); + + // We only want the Editor Services DLL; the new ALC will lazily load its dependencies automatically + if (!string.Equals(asmName.Name, "Microsoft.PowerShell.EditorServices", StringComparison.Ordinal)) + { + return null; + } + + string asmPath = Path.Combine(s_psesDependencyDirPath, $"{asmName.Name}.dll"); + + logger.Log(PsesLogLevel.Verbose, "Loading PSES DLL using new assembly load context"); + + return psesLoadContext.LoadFromAssemblyPath(asmPath); + }; +#else + // In .NET Framework we add an event here to redirect dependency loading in the current AppDomain for PSES' dependencies + logger.Log(PsesLogLevel.Verbose, "Adding AssemblyResolve event handler for dependency loading"); + + if (hostConfig.LogLevel == PsesLogLevel.Diagnostic) + { + AppDomain.CurrentDomain.AssemblyLoad += (object sender, AssemblyLoadEventArgs args) => + { + logger.Log( + PsesLogLevel.Diagnostic, + $"Loaded {args.LoadedAssembly.GetName()}"); + }; + } + + // Unlike in .NET Core, we need to be look for all dependencies in .NET Framework, not just PSES.dll + AppDomain.CurrentDomain.AssemblyResolve += (object sender, ResolveEventArgs args) => + { + logger.Log(PsesLogLevel.Diagnostic, $"Assembly resolve event fired for {args.Name}"); + + var asmName = new AssemblyName(args.Name); + string asmPath = Path.Combine(s_psesDependencyDirPath, $"{asmName.Name}.dll"); + if (!File.Exists(asmPath)) + { + return null; + } + + logger.Log(PsesLogLevel.Diagnostic, $"Loading {args.Name} from PSES dependency dir into LoadFrom context"); + return Assembly.LoadFrom(asmPath); + }; +#endif + + return new EditorServicesLoader(logger, hostConfig, sessionFileWriter, loggersToUnsubscribe); + } + + private readonly EditorServicesConfig _hostConfig; + + private readonly ISessionFileWriter _sessionFileWriter; + + private readonly HostLogger _logger; + + private readonly IReadOnlyCollection _loggersToUnsubscribe; + + private EditorServicesLoader( + HostLogger logger, + EditorServicesConfig hostConfig, + ISessionFileWriter sessionFileWriter, + IReadOnlyCollection loggersToUnsubscribe) + { + _logger = logger; + _hostConfig = hostConfig; + _sessionFileWriter = sessionFileWriter; + _loggersToUnsubscribe = loggersToUnsubscribe; + } + + /// + /// Load Editor Services and its dependencies in an isolated way and start it. + /// This method's returned task will end when Editor Services shuts down. + /// + /// + public async Task LoadAndRunEditorServicesAsync() + { + // Log important host information here + LogHostInformation(); + +#if !CoreCLR + // Make sure the .NET Framework version supports .NET Standard 2.0 + CheckNetFxVersion(); +#endif + // Ensure the language mode allows us to run + CheckLanguageMode(); + + // Add the bundled modules to the PSModulePath + UpdatePSModulePath(); + + // Check to see if the configuration we have is valid + ValidateConfiguration(); + + // Method with no implementation that forces the PSES assembly to load, triggering an AssemblyResolve event + _logger.Log(PsesLogLevel.Verbose, "Loading PowerShell Editor Services"); + LoadEditorServices(); + + _logger.Log(PsesLogLevel.Verbose, "Starting EditorServices"); + using (var editorServicesRunner = new EditorServicesRunner(_logger, _hostConfig, _sessionFileWriter, _loggersToUnsubscribe)) + { + // The trigger method for Editor Services + // We will wait here until Editor Services shuts down + await editorServicesRunner.RunUntilShutdown().ConfigureAwait(false); + } + } + + public void Dispose() + { + // TODO: Remove assembly resolve events + // This is not high priority, since the PSES process shouldn't be reused + } + + private void LoadEditorServices() + { + EditorServicesLoading.LoadEditorServicesForHost(); + } + +#if !CoreCLR + private void CheckNetFxVersion() + { + _logger.Log(PsesLogLevel.Diagnostic, "Checking that .NET Framework version is at least 4.6.1"); + using (RegistryKey key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Net Framework Setup\NDP\v4\Full")) + { + object netFxValue = key?.GetValue("Release"); + if (netFxValue == null || !(netFxValue is int netFxVersion)) + { + return; + } + + _logger.Log(PsesLogLevel.Verbose, $".NET registry version: {netFxVersion}"); + + if (netFxVersion < Net461Version) + { + _logger.Log(PsesLogLevel.Warning, $".NET Framework version {netFxVersion} lower than .NET 4.6.1. This runtime is not supported and you may experience errors. Please update your .NET runtime version."); + } + } + } +#endif + + /// + /// PSES currently does not work in Constrained Language Mode, because PSReadLine script invocations won't work in it. + /// Ideally we can find a better way so that PSES will work in CLM. + /// + private void CheckLanguageMode() + { + _logger.Log(PsesLogLevel.Diagnostic, "Checking that PSES is running in FullLanguage mode"); + using (var pwsh = SMA.PowerShell.Create()) + { + if (pwsh.Runspace.SessionStateProxy.LanguageMode != SMA.PSLanguageMode.FullLanguage) + { + throw new InvalidOperationException("Cannot start PowerShell Editor Services in Constrained Language Mode"); + } + } + } + + private void UpdatePSModulePath() + { + if (string.IsNullOrEmpty(_hostConfig.BundledModulePath)) + { + _logger.Log(PsesLogLevel.Diagnostic, "BundledModulePath not set, skipping"); + return; + } + + string psModulePath = Environment.GetEnvironmentVariable("PSModulePath").TrimEnd(Path.PathSeparator); + psModulePath = $"{psModulePath}{Path.PathSeparator}{_hostConfig.BundledModulePath}"; + Environment.SetEnvironmentVariable("PSModulePath", psModulePath); + + _logger.Log(PsesLogLevel.Verbose, $"Updated PSModulePath to: '{psModulePath}'"); + } + + private void LogHostInformation() + { + _logger.Log(PsesLogLevel.Diagnostic, "Logging host information"); + + _logger.Log(PsesLogLevel.Verbose, $@" +== Build Details == +- Editor Services version: {BuildInfo.BuildVersion} +- Build origin: {BuildInfo.BuildOrigin} +- Build time: {BuildInfo.BuildTime} +"); + + _logger.Log(PsesLogLevel.Verbose, $@" +== Host Startup Configuration Details == + - Host name: {_hostConfig.HostInfo.Name} + - Host version: {_hostConfig.HostInfo.Version} + - Host profile ID: {_hostConfig.HostInfo.ProfileId} + - PowerShell host type: {_hostConfig.PSHost.GetType()} + + - REPL setting: {_hostConfig.ConsoleRepl} + - Session details path: {_hostConfig.SessionDetailsPath} + - Bundled modules path: {_hostConfig.BundledModulePath} + - Additional modules: {(_hostConfig.AdditionalModules == null ? "" : string.Join(", ", _hostConfig.AdditionalModules))} + - Feature flags: {(_hostConfig.FeatureFlags == null ? "" : string.Join(", ", _hostConfig.FeatureFlags))} + + - Log path: {_hostConfig.LogPath} + - Minimum log level: {_hostConfig.LogLevel} + + - Profile paths: + + AllUsersAllHosts: {_hostConfig.ProfilePaths.AllUsersAllHosts ?? ""} + + AllUsersCurrentHost: {_hostConfig.ProfilePaths.AllUsersCurrentHost ?? ""} + + CurrentUserAllHosts: {_hostConfig.ProfilePaths.CurrentUserAllHosts ?? ""} + + CurrentUserCurrentHost: {_hostConfig.ProfilePaths.CurrentUserCurrentHost ?? ""} +"); + + _logger.Log(PsesLogLevel.Verbose, $@" +== Console Details == + - Console input encoding: {Console.InputEncoding.EncodingName} + - Console output encoding: {Console.OutputEncoding.EncodingName} + - PowerShell output encoding: {GetPSOutputEncoding()} +"); + + LogPowerShellDetails(); + + LogOperatingSystemDetails(); + } + + private string GetPSOutputEncoding() + { + using (var pwsh = SMA.PowerShell.Create()) + { + return pwsh.AddScript("$OutputEncoding.EncodingName").Invoke()[0]; + } + } + + private void LogPowerShellDetails() + { + using (var pwsh = SMA.PowerShell.Create(SMA.RunspaceMode.CurrentRunspace)) + { + string psVersion = pwsh.AddScript("$PSVersionTable.PSVersion").Invoke()[0].ToString(); + + _logger.Log(PsesLogLevel.Verbose, $@" +== PowerShell Details == +- PowerShell version: {psVersion} +- Language mode: {pwsh.Runspace.SessionStateProxy.LanguageMode} +"); + } + } + + private void LogOperatingSystemDetails() + { + _logger.Log(PsesLogLevel.Verbose, $@" +== Environment Details == + - OS description: {RuntimeInformation.OSDescription} + - OS architecture: {GetOSArchitecture()} + - Process bitness: {(Environment.Is64BitProcess ? "64" : "32")} +"); + } + + private string GetOSArchitecture() + { +#if CoreCLR + if (Environment.OSVersion.Platform != PlatformID.Win32NT) + { + return RuntimeInformation.OSArchitecture.ToString(); + } +#endif + + // If on win7 (version 6.1.x), avoid System.Runtime.InteropServices.RuntimeInformation + if (Environment.OSVersion.Version < new Version(6, 2)) + { + return Environment.Is64BitProcess + ? "X64" + : "X86"; + } + + return RuntimeInformation.OSArchitecture.ToString(); + } + + private void ValidateConfiguration() + { + _logger.Log(PsesLogLevel.Diagnostic, "Validating configuration"); + + bool lspUsesStdio = _hostConfig.LanguageServiceTransport is StdioTransportConfig; + bool debugUsesStdio = _hostConfig.DebugServiceTransport is StdioTransportConfig; + + // Ensure LSP and Debug are not both Stdio + if (lspUsesStdio && debugUsesStdio) + { + throw new ArgumentException("LSP and Debug transports cannot both use Stdio"); + } + + if (_hostConfig.ConsoleRepl != ConsoleReplKind.None + && (lspUsesStdio || debugUsesStdio)) + { + throw new ArgumentException("Cannot use the REPL with a Stdio protocol transport"); + } + + if (_hostConfig.PSHost == null) + { + throw new ArgumentNullException(nameof(_hostConfig.PSHost)); + } + } + } +} diff --git a/src/PowerShellEditorServices.Hosting/Internal/EditorServicesRunner.cs b/src/PowerShellEditorServices.Hosting/Internal/EditorServicesRunner.cs new file mode 100644 index 000000000..73b869d5a --- /dev/null +++ b/src/PowerShellEditorServices.Hosting/Internal/EditorServicesRunner.cs @@ -0,0 +1,272 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Server; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Hosting +{ + /// + /// Class to manage the startup of PowerShell Editor Services. + /// This should be called by only after Editor Services has been loaded. + /// + internal class EditorServicesRunner : IDisposable + { + private readonly HostLogger _logger; + + private readonly EditorServicesConfig _config; + + private readonly ISessionFileWriter _sessionFileWriter; + + private readonly EditorServicesServerFactory _serverFactory; + + private readonly IReadOnlyCollection _loggersToUnsubscribe; + + private bool _alreadySubscribedDebug; + + public EditorServicesRunner( + HostLogger logger, + EditorServicesConfig config, + ISessionFileWriter sessionFileWriter, + IReadOnlyCollection loggersToUnsubscribe) + { + _logger = logger; + _config = config; + _sessionFileWriter = sessionFileWriter; + _serverFactory = EditorServicesServerFactory.Create(_config.LogPath, (int)_config.LogLevel, logger); + _alreadySubscribedDebug = false; + _loggersToUnsubscribe = loggersToUnsubscribe; + } + + /// + /// Start and run Editor Services and then wait for shutdown. + /// + /// A task that ends when Editor Services shuts down. + public async Task RunUntilShutdown() + { + // Start Editor Services + Task runAndAwaitShutdown = CreateEditorServicesAndRunUntilShutdown(); + + // Now write the session file + _logger.Log(PsesLogLevel.Diagnostic, "Writing session file"); + _sessionFileWriter.WriteSessionStarted(_config.LanguageServiceTransport, _config.DebugServiceTransport); + + // Finally, wait for Editor Services to shut down + await runAndAwaitShutdown.ConfigureAwait(false); + } + + public void Dispose() + { + _serverFactory.Dispose(); + } + + /// + /// Master method for instantiating, running and waiting for the LSP and debug servers at the heart of Editor Services. + /// + /// A task that ends when Editor Services shuts down. + private async Task CreateEditorServicesAndRunUntilShutdown() + { + try + { + bool creatingLanguageServer = _config.LanguageServiceTransport != null; + bool creatingDebugServer = _config.DebugServiceTransport != null; + bool isTempDebugSession = creatingDebugServer && !creatingLanguageServer; + + // Set up information required to instantiate servers + HostStartupInfo hostStartupInfo = CreateHostStartupInfo(); + + // If we just want a temp debug session, run that and do nothing else + if (isTempDebugSession) + { + await RunTempDebugSessionAsync(hostStartupInfo).ConfigureAwait(false); + return; + } + + // We want LSP and maybe debugging + // To do that we: + // - Create the LSP server + // - Possibly kick off the debug server creation + // - Start the LSP server + // - Possibly start the debug server + // - Wait for the LSP server to finish + + // Unsubscribe the host logger here so that the integrated console is not polluted with input after the first prompt + _logger.Log(PsesLogLevel.Verbose, "Starting server, deregistering host logger and registering shutdown listener"); + if (_loggersToUnsubscribe != null) + { + foreach (IDisposable loggerToUnsubscribe in _loggersToUnsubscribe) + { + loggerToUnsubscribe.Dispose(); + } + } + + WriteStartupBanner(); + + PsesLanguageServer languageServer = await CreateLanguageServerAsync(hostStartupInfo).ConfigureAwait(false); + + Task debugServerCreation = null; + if (creatingDebugServer) + { + debugServerCreation = CreateDebugServerWithLanguageServerAsync(languageServer, usePSReadLine: _config.ConsoleRepl == ConsoleReplKind.PSReadLine); + } + + languageServer.StartAsync(); + + if (creatingDebugServer) + { + StartDebugServer(debugServerCreation); + } + + await languageServer.WaitForShutdown().ConfigureAwait(false); + } + finally + { + // Resubscribe host logger to log shutdown events to the console + _logger.Subscribe(new PSHostLogger(_config.PSHost.UI)); + } + } + + private async Task RunTempDebugSessionAsync(HostStartupInfo hostDetails) + { + _logger.Log(PsesLogLevel.Diagnostic, "Running temp debug session"); + PsesDebugServer debugServer = await CreateDebugServerForTempSessionAsync(hostDetails).ConfigureAwait(false); + _logger.Log(PsesLogLevel.Verbose, "Debug server created"); + await debugServer.StartAsync().ConfigureAwait(false); + _logger.Log(PsesLogLevel.Verbose, "Debug server started"); + await debugServer.WaitForShutdown().ConfigureAwait(false); + } + + private async Task StartDebugServer(Task debugServerCreation) + { + PsesDebugServer debugServer = await debugServerCreation.ConfigureAwait(false); + + // When the debug server shuts down, we want it to automatically restart + // To do this, we set an event to allow it to create a new debug server as its session ends + if (!_alreadySubscribedDebug) + { + _logger.Log(PsesLogLevel.Diagnostic, "Subscribing debug server for session ended event"); + _alreadySubscribedDebug = true; + debugServer.SessionEnded += DebugServer_OnSessionEnded; + } + + _logger.Log(PsesLogLevel.Diagnostic, "Starting debug server"); + debugServer.StartAsync(); + } + + private Task RestartDebugServerAsync(PsesDebugServer debugServer, bool usePSReadLine) + { + _logger.Log(PsesLogLevel.Diagnostic, "Restarting debug server"); + Task debugServerCreation = RecreateDebugServerAsync(debugServer, usePSReadLine); + return StartDebugServer(debugServerCreation); + } + + private async Task CreateLanguageServerAsync(HostStartupInfo hostDetails) + { + _logger.Log(PsesLogLevel.Verbose, $"Creating LSP transport with endpoint {_config.LanguageServiceTransport.EndpointDetails}"); + (Stream inStream, Stream outStream) = await _config.LanguageServiceTransport.ConnectStreamsAsync().ConfigureAwait(false); + + _logger.Log(PsesLogLevel.Diagnostic, "Creating language server"); + return _serverFactory.CreateLanguageServer(inStream, outStream, hostDetails); + } + + private async Task CreateDebugServerWithLanguageServerAsync(PsesLanguageServer languageServer, bool usePSReadLine) + { + _logger.Log(PsesLogLevel.Verbose, $"Creating debug adapter transport with endpoint {_config.DebugServiceTransport.EndpointDetails}"); + (Stream inStream, Stream outStream) = await _config.DebugServiceTransport.ConnectStreamsAsync().ConfigureAwait(false); + + _logger.Log(PsesLogLevel.Diagnostic, "Creating debug adapter"); + return _serverFactory.CreateDebugServerWithLanguageServer(inStream, outStream, languageServer, usePSReadLine); + } + + private async Task RecreateDebugServerAsync(PsesDebugServer debugServer, bool usePSReadLine) + { + _logger.Log(PsesLogLevel.Diagnostic, "Recreating debug adapter transport"); + (Stream inStream, Stream outStream) = await _config.DebugServiceTransport.ConnectStreamsAsync().ConfigureAwait(false); + + _logger.Log(PsesLogLevel.Diagnostic, "Recreating debug adapter"); + return _serverFactory.RecreateDebugServer(inStream, outStream, debugServer, usePSReadLine); + } + + private async Task CreateDebugServerForTempSessionAsync(HostStartupInfo hostDetails) + { + (Stream inStream, Stream outStream) = await _config.DebugServiceTransport.ConnectStreamsAsync().ConfigureAwait(false); + + return _serverFactory.CreateDebugServerForTempSession(inStream, outStream, hostDetails); + } + + private HostStartupInfo CreateHostStartupInfo() + { + _logger.Log(PsesLogLevel.Diagnostic, "Creating startup info object"); + + ProfilePathInfo profilePaths = null; + if (_config.ProfilePaths.AllUsersAllHosts != null + || _config.ProfilePaths.AllUsersCurrentHost != null + || _config.ProfilePaths.CurrentUserAllHosts != null + || _config.ProfilePaths.CurrentUserCurrentHost != null) + { + profilePaths = new ProfilePathInfo( + _config.ProfilePaths.CurrentUserAllHosts, + _config.ProfilePaths.CurrentUserCurrentHost, + _config.ProfilePaths.AllUsersAllHosts, + _config.ProfilePaths.AllUsersCurrentHost); + } + + return new HostStartupInfo( + _config.HostInfo.Name, + _config.HostInfo.ProfileId, + _config.HostInfo.Version, + _config.PSHost, + profilePaths, + _config.FeatureFlags, + _config.AdditionalModules, + _config.LogPath, + (int)_config.LogLevel, + consoleReplEnabled: _config.ConsoleRepl != ConsoleReplKind.None, + usesLegacyReadLine: _config.ConsoleRepl == ConsoleReplKind.LegacyReadLine); + } + + private void WriteStartupBanner() + { + if (_config.ConsoleRepl == ConsoleReplKind.None) + { + return; + } + + _config.PSHost.UI.WriteLine(@" + + +__/\\\\\\\\\\\\\_______/\\\\\\\\\\\____/\\\\\\\\\\\________/\\\\\\\\\_ + _\/\\\/////////\\\___/\\\/////////\\\_\/////\\\///______/\\\////////__ + _\/\\\_______\/\\\__\//\\\______\///______\/\\\_______/\\\/___________ + _\/\\\\\\\\\\\\\/____\////\\\_____________\/\\\______/\\\_____________ + _\/\\\/////////_________\////\\\__________\/\\\_____\/\\\_____________ + _\/\\\_____________________\////\\\_______\/\\\_____\//\\\____________ + _\/\\\______________/\\\______\//\\\______\/\\\______\///\\\__________ + _\/\\\_____________\///\\\\\\\\\\\/____/\\\\\\\\\\\____\////\\\\\\\\\_ + _\///________________\///////////_____\///////////________\/////////__ + + + + =====> PowerShell Integrated Console <===== + +"); + } + + private void DebugServer_OnSessionEnded(object sender, EventArgs args) + { + _logger.Log(PsesLogLevel.Verbose, "Debug session ended. Restarting debug service"); + var oldServer = (PsesDebugServer)sender; + oldServer.Dispose(); + _alreadySubscribedDebug = false; + Task.Run(() => + { + RestartDebugServerAsync(oldServer, usePSReadLine: _config.ConsoleRepl == ConsoleReplKind.PSReadLine); + }); + } + } +} diff --git a/src/PowerShellEditorServices.Hosting/Internal/NamedPipeUtils.cs b/src/PowerShellEditorServices.Hosting/Internal/NamedPipeUtils.cs new file mode 100644 index 000000000..01dd713c1 --- /dev/null +++ b/src/PowerShellEditorServices.Hosting/Internal/NamedPipeUtils.cs @@ -0,0 +1,154 @@ +// +// 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.IO; +using System.IO.Pipes; +using System.Runtime.InteropServices; + +#if !CoreCLR +using System.Security.Principal; +using System.Security.AccessControl; +#endif + +namespace Microsoft.PowerShell.EditorServices.Hosting +{ + /// + /// Utility class for handling named pipe creation in .NET Core and .NET Framework. + /// + internal static class NamedPipeUtils + { +#if !CoreCLR + // .NET Framework requires the buffer size to be specified + private const int PipeBufferSize = 1024; +#endif + + internal static NamedPipeServerStream CreateNamedPipe( + string pipeName, + PipeDirection pipeDirection) + { +#if CoreCLR + return new NamedPipeServerStream( + pipeName: pipeName, + direction: pipeDirection, + maxNumberOfServerInstances: 1, + transmissionMode: PipeTransmissionMode.Byte, + options: PipeOptions.CurrentUserOnly | PipeOptions.Asynchronous); +#else + + // In .NET Framework, we must manually ACL the named pipes we create + + var pipeSecurity = new PipeSecurity(); + + WindowsIdentity identity = WindowsIdentity.GetCurrent(); + WindowsPrincipal principal = new WindowsPrincipal(identity); + + if (principal.IsInRole(WindowsBuiltInRole.Administrator)) + { + // Allow the Administrators group full access to the pipe. + pipeSecurity.AddAccessRule( + new PipeAccessRule( + new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, domainSid: null).Translate(typeof(NTAccount)), + PipeAccessRights.FullControl, AccessControlType.Allow)); + } + else + { + // Allow the current user read/write access to the pipe. + pipeSecurity.AddAccessRule(new PipeAccessRule( + WindowsIdentity.GetCurrent().User, + PipeAccessRights.ReadWrite, AccessControlType.Allow)); + } + + return new NamedPipeServerStream( + pipeName, + pipeDirection, + maxNumberOfServerInstances: 1, + PipeTransmissionMode.Byte, + PipeOptions.Asynchronous, + inBufferSize: PipeBufferSize, + outBufferSize: PipeBufferSize, + pipeSecurity); +#endif + } + + /// + /// Generate a named pipe name known to not already be in use. + /// + /// Prefix variants of the pipename to test, if any. + /// A named pipe name or name suffix that is safe to you. + public static string GenerateValidNamedPipeName(IReadOnlyCollection prefixes = null) + { + int tries = 0; + do + { + string pipeName = $"PSES_{Path.GetRandomFileName()}"; + + // In the simple prefix-less case, just test the pipe name + if (prefixes == null) + { + if (!IsPipeNameValid(pipeName)) + { + continue; + } + + return pipeName; + } + + // If we have prefixes, test that all prefix/pipename combinations are valid + bool allPipeNamesValid = true; + foreach (string prefix in prefixes) + { + string prefixedPipeName = $"{prefix}_{pipeName}"; + if (!IsPipeNameValid(prefixedPipeName)) + { + allPipeNamesValid = false; + break; + } + } + + if (allPipeNamesValid) + { + return pipeName; + } + + } while (tries < 10); + + throw new IOException("Unable to create named pipe; no available names"); + } + + /// + /// Validate that a named pipe file name is a legitimate named pipe file name and is not already in use. + /// + /// The named pipe name to validate. This should be a simple name rather than a path. + /// True if the named pipe name is valid, false otherwise. + public static bool IsPipeNameValid(string pipeName) + { + if (string.IsNullOrEmpty(pipeName)) + { + return false; + } + + return !File.Exists(GetNamedPipePath(pipeName)); + } + + /// + /// Get the path of a named pipe given its name. + /// + /// The simple name of the named pipe. + /// The full path of the named pipe. + public static string GetNamedPipePath(string pipeName) + { +#if CoreCLR + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return Path.Combine(Path.GetTempPath(), $"CoreFxPipe_{pipeName}"); + } +#endif + + return $@"\\.\pipe\{pipeName}"; + } + } +} diff --git a/src/PowerShellEditorServices.Hosting/Internal/PsesLoadContext.cs b/src/PowerShellEditorServices.Hosting/Internal/PsesLoadContext.cs new file mode 100644 index 000000000..7ed121e05 --- /dev/null +++ b/src/PowerShellEditorServices.Hosting/Internal/PsesLoadContext.cs @@ -0,0 +1,77 @@ +// +// 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.IO; +using System.Reflection; +using System.Runtime.Loader; + +namespace Microsoft.PowerShell.EditorServices.Hosting +{ + /// + /// An AssemblyLoadContext (ALC) designed to find PSES' dependencies in the given directory. + /// This class only exists in .NET Core, where the ALC is used to isolate PSES' dependencies + /// from the PowerShell assembly load context so that modules can import their own dependencies + /// without issue in PSES. + /// + internal class PsesLoadContext : AssemblyLoadContext + { + private static readonly string s_psHome = Path.GetDirectoryName( + Assembly.GetEntryAssembly().Location); + + private readonly string _dependencyDirPath; + + public PsesLoadContext(string dependencyDirPath) + { + _dependencyDirPath = dependencyDirPath; + + // Try and set our name in .NET Core 3+ for logging niceness + TrySetName("PsesLoadContext"); + } + + protected override Assembly Load(AssemblyName assemblyName) + { + // Since this class is responsible for loading any DLLs in .NET Core, + // we must restrict the code in here to only use core types, + // otherwise we may depend on assembly that we are trying to load and cause a StackOverflowException + + string psHomeAsmPath = Path.Join(s_psHome, $"{assemblyName.Name}.dll"); + + if (File.Exists(psHomeAsmPath)) + { + return null; + } + + string asmPath = Path.Join(_dependencyDirPath, $"{assemblyName.Name}.dll"); + + if (File.Exists(asmPath)) + { + return LoadFromAssemblyPath(asmPath); + } + + return null; + } + + private void TrySetName(string name) + { + try + { + // This field only exists in .NET Core 3+, but helps logging + FieldInfo nameBackingField = typeof(AssemblyLoadContext).GetField( + "_name", + BindingFlags.NonPublic | BindingFlags.Instance); + + if (nameBackingField != null) + { + nameBackingField.SetValue(this, name); + } + } + catch + { + // Do nothing -- we did our best + } + } + } +} diff --git a/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj b/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj new file mode 100644 index 000000000..80759709e --- /dev/null +++ b/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj @@ -0,0 +1,39 @@ + + + + netcoreapp2.1;net461 + Microsoft.PowerShell.EditorServices.Hosting + latest + + + + $(DefineConstants);CoreCLR + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + diff --git a/src/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.csproj b/src/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.csproj index e1138cbe3..fc5fc85dd 100644 --- a/src/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.csproj +++ b/src/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.csproj @@ -6,6 +6,7 @@ Provides added functionality to PowerShell Editor Services for the Visual Studio Code editor. netstandard2.0 Microsoft.PowerShell.EditorServices.VSCode + Debug;Release;CoreCLR diff --git a/src/PowerShellEditorServices/Hosting/EditorServicesHost.cs b/src/PowerShellEditorServices/Hosting/EditorServicesHost.cs deleted file mode 100644 index eed6555fa..000000000 --- a/src/PowerShellEditorServices/Hosting/EditorServicesHost.cs +++ /dev/null @@ -1,530 +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.Diagnostics; -using System.IO.Pipes; -using System.Linq; -using System.Management.Automation; -using System.Management.Automation.Host; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Security.AccessControl; -using System.Security.Principal; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Server; -using Microsoft.PowerShell.EditorServices.Services; -using Microsoft.PowerShell.EditorServices.Utility; -using OmniSharp.Extensions.LanguageServer.Protocol.Server; -using Serilog; - -namespace Microsoft.PowerShell.EditorServices.Hosting -{ - public enum EditorServicesHostStatus - { - Started, - Failed, - Ended - } - - public enum EditorServiceTransportType - { - NamedPipe, - Stdio - } - - public class EditorServiceTransportConfig - { - public EditorServiceTransportType TransportType { get; set; } - /// - /// Configures the endpoint of the transport. - /// For Stdio it's ignored. - /// For NamedPipe it's the pipe name. - /// - public string InOutPipeName { get; set; } - - public string OutPipeName { get; set; } - - public string InPipeName { get; set; } - - internal string Endpoint => OutPipeName != null && InPipeName != null ? $"In pipe: {InPipeName} Out pipe: {OutPipeName}" : $" InOut pipe: {InOutPipeName}"; - } - - /// - /// Provides a simplified interface for hosting the language and debug services - /// over the named pipe server protocol. - /// - public class EditorServicesHost - { - #region Private Fields - - // This int will be casted to a PipeOptions enum that only exists in .NET Core 2.1 and up which is why it's not available to us in .NET Standard. - private const int CurrentUserOnly = 0x20000000; - - // In .NET Framework, NamedPipeServerStream has a constructor that takes in a PipeSecurity object. We will use reflection to call the constructor, - // since .NET Framework doesn't have the `CurrentUserOnly` PipeOption. - // doc: https://docs.microsoft.com/en-us/dotnet/api/system.io.pipes.namedpipeserverstream.-ctor?view=netframework-4.7.2#System_IO_Pipes_NamedPipeServerStream__ctor_System_String_System_IO_Pipes_PipeDirection_System_Int32_System_IO_Pipes_PipeTransmissionMode_System_IO_Pipes_PipeOptions_System_Int32_System_Int32_System_IO_Pipes_PipeSecurity_ - private static readonly ConstructorInfo s_netFrameworkPipeServerConstructor = - typeof(NamedPipeServerStream).GetConstructor(new[] { typeof(string), typeof(PipeDirection), typeof(int), typeof(PipeTransmissionMode), typeof(PipeOptions), typeof(int), typeof(int), typeof(PipeSecurity) }); - - private readonly HostDetails _hostDetails; - - private readonly PSHost _internalHost; - - private readonly bool _enableConsoleRepl; - - private readonly bool _useLegacyReadLine; - - private readonly HashSet _featureFlags; - - private readonly string[] _additionalModules; - - private PsesLanguageServer _languageServer; - private PsesDebugServer _debugServer; - - private Microsoft.Extensions.Logging.ILogger _logger; - - private ILoggerFactory _factory; - - #endregion - - #region Properties - - public EditorServicesHostStatus Status { get; private set; } - - #endregion - - #region Constructors - - /// - /// Initializes a new instance of the EditorServicesHost class and waits for - /// the debugger to attach if waitForDebugger is true. - /// - /// The details of the host which is launching PowerShell Editor Services. - /// Provides a path to PowerShell modules bundled with the host, if any. Null otherwise. - /// If true, causes the host to wait for the debugger to attach before proceeding. - /// Modules to be loaded when initializing the new runspace. - /// Features to enable for this instance. - public EditorServicesHost( - HostDetails hostDetails, - string bundledModulesPath, - bool enableConsoleRepl, - bool useLegacyReadLine, - bool waitForDebugger, - string[] additionalModules, - string[] featureFlags) - : this( - hostDetails, - bundledModulesPath, - enableConsoleRepl, - useLegacyReadLine, - waitForDebugger, - additionalModules, - featureFlags, - GetInternalHostFromDefaultRunspace()) - { - } - - /// - /// Initializes a new instance of the EditorServicesHost class and waits for - /// the debugger to attach if waitForDebugger is true. - /// - /// The details of the host which is launching PowerShell Editor Services. - /// Provides a path to PowerShell modules bundled with the host, if any. Null otherwise. - /// If true, causes the host to wait for the debugger to attach before proceeding. - /// Modules to be loaded when initializing the new runspace. - /// Features to enable for this instance. - /// The value of the $Host variable in the original runspace. - public EditorServicesHost( - HostDetails hostDetails, - string bundledModulesPath, - bool enableConsoleRepl, - bool useLegacyReadLine, - bool waitForDebugger, - string[] additionalModules, - string[] featureFlags, - PSHost internalHost) - { - Validate.IsNotNull(nameof(hostDetails), hostDetails); - Validate.IsNotNull(nameof(internalHost), internalHost); - - _hostDetails = hostDetails; - - _enableConsoleRepl = enableConsoleRepl; - _useLegacyReadLine = useLegacyReadLine; - _additionalModules = additionalModules ?? Array.Empty(); - _featureFlags = new HashSet(featureFlags ?? Array.Empty()); - _internalHost = internalHost; - -#if DEBUG - if (waitForDebugger) - { - if (System.Diagnostics.Debugger.IsAttached) - { - System.Diagnostics.Debugger.Break(); - } - else - { - System.Diagnostics.Debugger.Launch(); - } - } -#endif - - // Catch unhandled exceptions for logging purposes - AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; - } - - #endregion - - #region Public Methods - - /// - /// Starts the Logger for the specified file path and log level. - /// - /// The path of the log file to be written. - /// The minimum level of log messages to be written. - public void StartLogging(string logFilePath, PsesLogLevel logLevel) - { - Log.Logger = new LoggerConfiguration().Enrich.FromLogContext() - .WriteTo.File(logFilePath) - .MinimumLevel.Verbose() - .CreateLogger(); - _factory = new LoggerFactory().AddSerilog(Log.Logger); - _logger = _factory.CreateLogger(); - - FileVersionInfo fileVersionInfo = - FileVersionInfo.GetVersionInfo(this.GetType().GetTypeInfo().Assembly.Location); - - string osVersion = RuntimeInformation.OSDescription; - - string osArch = GetOSArchitecture(); - - string buildTime = BuildInfo.BuildTime?.ToString("s", System.Globalization.CultureInfo.InvariantCulture) ?? ""; - - string logHeader = $@" -PowerShell Editor Services Host v{fileVersionInfo.FileVersion} starting (PID {Process.GetCurrentProcess().Id}) - - Host application details: - - Name: {_hostDetails.Name} - Version: {_hostDetails.Version} - ProfileId: {_hostDetails.ProfileId} - Arch: {osArch} - - Operating system details: - - Version: {osVersion} - Arch: {osArch} - - Build information: - - Version: {BuildInfo.BuildVersion} - Origin: {BuildInfo.BuildOrigin} - Date: {buildTime} -"; - - _logger.LogInformation(logHeader); - } - - /// - /// Starts the language service with the specified config. - /// - /// The config that contains information on the communication protocol that will be used. - /// The profiles that will be loaded in the session. - public void StartLanguageService( - EditorServiceTransportConfig config, - ProfilePaths profilePaths) - { - _logger.LogInformation($"LSP NamedPipe: {config.InOutPipeName}\nLSP OutPipe: {config.OutPipeName}"); - - switch (config.TransportType) - { - case EditorServiceTransportType.NamedPipe: - _languageServer = new NamedPipePsesLanguageServer( - _factory, - LogLevel.Trace, - _enableConsoleRepl, - _useLegacyReadLine, - _featureFlags, - _hostDetails, - _additionalModules, - _internalHost, - profilePaths, - config.InOutPipeName ?? config.InPipeName, - config.OutPipeName); - break; - - case EditorServiceTransportType.Stdio: - _languageServer = new StdioPsesLanguageServer( - _factory, - LogLevel.Trace, - _featureFlags, - _hostDetails, - _additionalModules, - _internalHost, - profilePaths); - break; - } - - _logger.LogInformation("Starting language server"); - - Task.Run(_languageServer.StartAsync); - - _logger.LogInformation( - string.Format( - "Language service started, type = {0}, endpoint = {1}", - config.TransportType, config.Endpoint)); - } - - - private bool alreadySubscribedDebug; - /// - /// Starts the debug service with the specified config. - /// - /// The config that contains information on the communication protocol that will be used. - /// The profiles that will be loaded in the session. - /// Determines if we will make a new session typically used for temporary console debugging. - public void StartDebugService( - EditorServiceTransportConfig config, - ProfilePaths profilePaths, - bool useTempSession) - { - _logger.LogInformation($"Debug NamedPipe: {config.InOutPipeName}\nDebug OutPipe: {config.OutPipeName}"); - - IServiceProvider serviceProvider = null; - if (useTempSession) - { - serviceProvider = new ServiceCollection() - .AddLogging(builder => builder - .ClearProviders() - .AddSerilog() - .SetMinimumLevel(LogLevel.Trace)) - .AddSingleton(provider => null) - .AddPsesLanguageServices( - profilePaths, - _featureFlags, - _enableConsoleRepl, - _useLegacyReadLine, - _internalHost, - _hostDetails, - _additionalModules) - .BuildServiceProvider(); - } - - switch (config.TransportType) - { - case EditorServiceTransportType.NamedPipe: - NamedPipeServerStream inNamedPipe = CreateNamedPipe( - config.InOutPipeName ?? config.InPipeName, - config.OutPipeName, - out NamedPipeServerStream outNamedPipe); - - _debugServer = new PsesDebugServer( - _factory, - inNamedPipe, - outNamedPipe ?? inNamedPipe); - - Task[] tasks = outNamedPipe != null - ? new[] { inNamedPipe.WaitForConnectionAsync(), outNamedPipe.WaitForConnectionAsync() } - : new[] { inNamedPipe.WaitForConnectionAsync() }; - Task.WhenAll(tasks) - .ContinueWith(async task => - { - _logger.LogInformation("Starting debug server"); - await _debugServer.StartAsync(serviceProvider ?? _languageServer.LanguageServer.Services, useTempSession); - _logger.LogInformation( - $"Debug service started, type = {config.TransportType}, endpoint = {config.Endpoint}"); - }); - - break; - - case EditorServiceTransportType.Stdio: - _debugServer = new PsesDebugServer( - _factory, - Console.OpenStandardInput(), - Console.OpenStandardOutput()); - - _logger.LogInformation("Starting debug server"); - Task.Run(async () => - { - - await _debugServer.StartAsync(serviceProvider ?? _languageServer.LanguageServer.Services, useTempSession); - _logger.LogInformation( - $"Debug service started, type = {config.TransportType}, endpoint = {config.Endpoint}"); - }); - break; - - default: - throw new NotSupportedException($"The transport {config.TransportType} is not supported"); - } - - // If the instance of PSES is being used for debugging only, then we don't want to allow automatic restarting - // because the user can simply spin up a new PSES if they need to. - // This design decision was done since this "debug-only PSES" is used in the "Temporary Integrated Console debugging" - // feature which does not want PSES to be restarted so that the user can see the output of the last debug - // session. - if(!alreadySubscribedDebug && !useTempSession) - { - alreadySubscribedDebug = true; - _debugServer.SessionEnded += (sender, eventArgs) => - { - _debugServer.Dispose(); - alreadySubscribedDebug = false; - StartDebugService(config, profilePaths, useTempSession); - }; - } - } - - /// - /// Stops the language or debug services if either were started. - /// - public void StopServices() - { - // TODO: Need a new way to shut down the services - } - - /// - /// Waits for either the language or debug service to shut down. - /// - public void WaitForCompletion() - { - // If _languageServer is not null, then we are either using: - // Stdio - that only uses a LanguageServer so we return when that has shutdown. - // NamedPipes - that uses both LanguageServer and DebugServer, but LanguageServer - // is the core of PowerShell Editor Services and if that shuts down, - // we want the whole process to shutdown. - if (_languageServer != null) - { - _languageServer.WaitForShutdown().GetAwaiter().GetResult(); - return; - } - - // If there is no LanguageServer, then we must be running with the DebugServiceOnly switch - // (used in Temporary console debugging) and we need to wait for the DebugServer to shutdown. - _debugServer.WaitForShutdown().GetAwaiter().GetResult(); - } - - #endregion - - #region Private Methods - - private static PSHost GetInternalHostFromDefaultRunspace() - { - using (var pwsh = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace)) - { - return pwsh.AddScript("$Host").Invoke().First(); - } - } - - /// - /// Gets the OSArchitecture for logging. Cannot use System.Runtime.InteropServices.RuntimeInformation.OSArchitecture - /// directly, since this tries to load API set DLLs in win7 and crashes. - /// - private string GetOSArchitecture() - { - // 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)) - { - if (Environment.Is64BitProcess) - { - return "X64"; - } - - return "X86"; - } - - return RuntimeInformation.OSArchitecture.ToString(); - } - - private void CurrentDomain_UnhandledException( - object sender, - UnhandledExceptionEventArgs e) - { - // Log the exception - _logger.LogError($"FATAL UNHANDLED EXCEPTION: {e.ExceptionObject}"); - } - - private static NamedPipeServerStream CreateNamedPipe( - string inOutPipeName, - string outPipeName, - out NamedPipeServerStream outPipe) - { - // .NET Core implementation is simplest so try that first - if (VersionUtils.IsNetCore) - { - outPipe = outPipeName == null - ? null - : new NamedPipeServerStream( - pipeName: outPipeName, - direction: PipeDirection.Out, - maxNumberOfServerInstances: 1, - transmissionMode: PipeTransmissionMode.Byte, - options: (PipeOptions)CurrentUserOnly); - - return new NamedPipeServerStream( - pipeName: inOutPipeName, - direction: PipeDirection.InOut, - maxNumberOfServerInstances: 1, - transmissionMode: PipeTransmissionMode.Byte, - options: PipeOptions.Asynchronous | (PipeOptions)CurrentUserOnly); - } - - // Now deal with Windows PowerShell - // We need to use reflection to get a nice constructor - - var pipeSecurity = new PipeSecurity(); - - WindowsIdentity identity = WindowsIdentity.GetCurrent(); - WindowsPrincipal principal = new WindowsPrincipal(identity); - - if (principal.IsInRole(WindowsBuiltInRole.Administrator)) - { - // Allow the Administrators group full access to the pipe. - pipeSecurity.AddAccessRule(new PipeAccessRule( - new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null).Translate(typeof(NTAccount)), - PipeAccessRights.FullControl, AccessControlType.Allow)); - } - else - { - // Allow the current user read/write access to the pipe. - pipeSecurity.AddAccessRule(new PipeAccessRule( - WindowsIdentity.GetCurrent().User, - PipeAccessRights.ReadWrite, AccessControlType.Allow)); - } - - outPipe = outPipeName == null - ? null - : (NamedPipeServerStream)s_netFrameworkPipeServerConstructor.Invoke( - new object[] { - outPipeName, - PipeDirection.InOut, - 1, // maxNumberOfServerInstances - PipeTransmissionMode.Byte, - PipeOptions.Asynchronous, - 1024, // inBufferSize - 1024, // outBufferSize - pipeSecurity - }); - - return (NamedPipeServerStream)s_netFrameworkPipeServerConstructor.Invoke( - new object[] { - inOutPipeName, - PipeDirection.InOut, - 1, // maxNumberOfServerInstances - PipeTransmissionMode.Byte, - PipeOptions.Asynchronous, - 1024, // inBufferSize - 1024, // outBufferSize - pipeSecurity - }); - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Hosting/EditorServicesLoading.cs b/src/PowerShellEditorServices/Hosting/EditorServicesLoading.cs new file mode 100644 index 000000000..5669b084b --- /dev/null +++ b/src/PowerShellEditorServices/Hosting/EditorServicesLoading.cs @@ -0,0 +1,19 @@ +// +// 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.Hosting +{ + /// + /// Implementation-free class designed to safely allow PowerShell Editor Services to be loaded in an obvious way. + /// Referencing this class will force looking for and loading the PSES assembly if it's not already loaded. + /// + internal static class EditorServicesLoading + { + internal static void LoadEditorServicesForHost() + { + // No op that forces loading this assembly + } + } +} diff --git a/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs b/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs new file mode 100644 index 000000000..af783abf6 --- /dev/null +++ b/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs @@ -0,0 +1,143 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Server; +using PowerShellEditorServices.Logging; +using Serilog; +using Serilog.Events; +using System; +using System.Diagnostics; +using System.IO; +using Microsoft.Extensions.DependencyInjection; +using OmniSharp.Extensions.LanguageServer.Server; + +#if DEBUG +using Serilog.Debugging; +#endif + +namespace Microsoft.PowerShell.EditorServices.Hosting +{ + /// + /// Factory class for hiding dependencies of Editor Services. + /// In particular, dependency injection and logging are wrapped by factory methods on this class + /// so that the host assembly can construct the LSP and debug servers + /// without taking logging or dependency injection dependencies directly. + /// + internal class EditorServicesServerFactory : IDisposable + { + /// + /// Create a new Editor Services factory. + /// This method will instantiate logging. + /// + /// The path of the log file to use. + /// The minimum log level to use. + /// + public static EditorServicesServerFactory Create(string logPath, int minimumLogLevel, IObservable<(int logLevel, string message)> hostLogger) + { + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Async(config => config.File(logPath)) + .MinimumLevel.Is((LogEventLevel)minimumLogLevel) + .CreateLogger(); + +#if DEBUG + SelfLog.Enable(msg => Debug.WriteLine(msg)); +#endif + + ILoggerFactory loggerFactory = new LoggerFactory().AddSerilog(); + + // Hook up logging from the host so that its recorded in the log file + hostLogger.Subscribe(new HostLoggerAdapter(loggerFactory)); + + return new EditorServicesServerFactory(loggerFactory, (LogLevel)minimumLogLevel); + } + + private readonly ILoggerFactory _loggerFactory; + + private readonly Extensions.Logging.ILogger _logger; + + private readonly LogLevel _minimumLogLevel; + + private EditorServicesServerFactory(ILoggerFactory loggerFactory, LogLevel minimumLogLevel) + { + _loggerFactory = loggerFactory; + _logger = loggerFactory.CreateLogger(); + _minimumLogLevel = minimumLogLevel; + } + + /// + /// Create the LSP server. + /// + /// The protocol transport input stream. + /// The protocol transport output stream. + /// The host details configuration for Editor Services instantation. + /// A new, unstarted language server instance. + public PsesLanguageServer CreateLanguageServer( + Stream inputStream, + Stream outputStream, + HostStartupInfo hostStartupInfo) + { + return new PsesLanguageServer(_loggerFactory, inputStream, outputStream, hostStartupInfo); + } + + /// + /// Create the debug server given a language server instance. + /// + /// The protocol transport input stream. + /// The protocol transport output stream. + /// + /// A new, unstarted debug server instance. + public PsesDebugServer CreateDebugServerWithLanguageServer(Stream inputStream, Stream outputStream, PsesLanguageServer languageServer, bool usePSReadLine) + { + return new PsesDebugServer(_loggerFactory, inputStream, outputStream, languageServer.LanguageServer.Services, useTempSession: false, usePSReadLine); + } + + /// + /// Create a new debug server based on an old one in an ended session. + /// + /// The protocol transport input stream. + /// The protocol transport output stream. + /// The old debug server to recreate. + /// + public PsesDebugServer RecreateDebugServer(Stream inputStream, Stream outputStream, PsesDebugServer debugServer, bool usePSReadLine) + { + return new PsesDebugServer(_loggerFactory, inputStream, outputStream, debugServer.ServiceProvider, useTempSession: false, usePSReadLine); + } + + /// + /// Create a standalone debug server for temp sessions. + /// + /// The protocol transport input stream. + /// The protocol transport output stream. + /// The host startup configuration to create the server session with. + /// + public PsesDebugServer CreateDebugServerForTempSession(Stream inputStream, Stream outputStream, HostStartupInfo hostStartupInfo) + { + var serviceProvider = new ServiceCollection() + .AddLogging(builder => builder + .ClearProviders() + .AddSerilog() + .SetMinimumLevel(LogLevel.Trace)) + .AddSingleton(provider => null) + .AddPsesLanguageServices(hostStartupInfo) + .BuildServiceProvider(); + + return new PsesDebugServer( + _loggerFactory, + inputStream, + outputStream, + serviceProvider, + useTempSession: true, + usePSReadLine: hostStartupInfo.ConsoleReplEnabled && !hostStartupInfo.UsesLegacyReadLine); + } + + public void Dispose() + { + Log.CloseAndFlush(); + _loggerFactory.Dispose(); + } + } +} diff --git a/src/PowerShellEditorServices/Hosting/HostDetails.cs b/src/PowerShellEditorServices/Hosting/HostDetails.cs deleted file mode 100644 index cdbe96f94..000000000 --- a/src/PowerShellEditorServices/Hosting/HostDetails.cs +++ /dev/null @@ -1,92 +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; - -namespace Microsoft.PowerShell.EditorServices.Hosting -{ - /// - /// Contains details about the current host application (most - /// likely the editor which is using the host process). - /// - public class HostDetails - { - #region Constants - - /// - /// The default host name for PowerShell Editor Services. Used - /// if no host name is specified by the host application. - /// - public const string DefaultHostName = "PowerShell Editor Services Host"; - - /// - /// The default host ID for PowerShell Editor Services. Used - /// for the host-specific profile path if no host ID is specified. - /// - public const string DefaultHostProfileId = "Microsoft.PowerShellEditorServices"; - - /// - /// The default host version for PowerShell Editor Services. If - /// no version is specified by the host application, we use 0.0.0 - /// to indicate a lack of version. - /// - public static readonly Version DefaultHostVersion = new Version("0.0.0"); - - /// - /// The default host details in a HostDetails object. - /// - public static readonly HostDetails Default = new HostDetails(null, null, null); - - #endregion - - #region Properties - - /// - /// Gets the name of the host. - /// - public string Name { get; private set; } - - /// - /// Gets the profile ID of the host, used to determine the - /// host-specific profile path. - /// - public string ProfileId { get; private set; } - - /// - /// Gets the version of the host. - /// - public Version Version { get; private set; } - - #endregion - - #region Constructors - - /// - /// Creates an instance of the HostDetails class. - /// - /// - /// The display name for the host, typically in the form of - /// "[Application Name] Host". - /// - /// - /// The identifier of the PowerShell host to use for its profile path. - /// loaded. Used to resolve a profile path of the form 'X_profile.ps1' - /// where 'X' represents the value of hostProfileId. If null, a default - /// will be used. - /// - /// The host application's version. - public HostDetails( - string name, - string profileId, - Version version) - { - this.Name = name ?? DefaultHostName; - this.ProfileId = profileId ?? DefaultHostProfileId; - this.Version = version ?? DefaultHostVersion; - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs b/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs new file mode 100644 index 000000000..a83ff2850 --- /dev/null +++ b/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs @@ -0,0 +1,175 @@ +// +// 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.Host; + +namespace Microsoft.PowerShell.EditorServices.Hosting +{ + /// + /// Contains details about the host as well as any other information + /// needed by Editor Services at startup time. + /// + public class HostStartupInfo + { + #region Constants + + /// + /// The default host name for PowerShell Editor Services. Used + /// if no host name is specified by the host application. + /// + private const string DefaultHostName = "PowerShell Editor Services Host"; + + /// + /// The default host ID for PowerShell Editor Services. Used + /// for the host-specific profile path if no host ID is specified. + /// + private const string DefaultHostProfileId = "Microsoft.PowerShellEditorServices"; + + /// + /// The default host version for PowerShell Editor Services. If + /// no version is specified by the host application, we use 0.0.0 + /// to indicate a lack of version. + /// + private static readonly Version s_defaultHostVersion = new Version(0, 0, 0); + + #endregion + + #region Properties + + /// + /// Gets the name of the host. + /// + public string Name { get; } + + /// + /// Gets the profile ID of the host, used to determine the + /// host-specific profile path. + /// + public string ProfileId { get; } + + /// + /// Gets the version of the host. + /// + public Version Version { get; } + + public ProfilePathInfo ProfilePaths { get; } + + /// + /// Any feature flags enabled at startup. + /// + public IReadOnlyList FeatureFlags { get; } + + /// + /// Names or paths of any additional modules to import on startup. + /// + public IReadOnlyList AdditionalModules { get; } + + /// + /// True if the integrated console is to be enabled. + /// + public bool ConsoleReplEnabled { get; } + + /// + /// If true, the legacy PSES readline implementation will be used. Otherwise PSReadLine will be used. + /// If the console REPL is not enabled, this setting will be ignored. + /// + public bool UsesLegacyReadLine { get; } + + /// + /// The PowerShell host to use with Editor Services. + /// + public PSHost PSHost { get; } + + /// + /// The path of the log file Editor Services should log to. + /// + public string LogPath { get; } + + /// + /// The minimum log level of log events to be logged. + /// + public int LogLevel { get; } + + #endregion + + #region Constructors + + /// + /// Creates an instance of the HostDetails class. + /// + /// + /// The display name for the host, typically in the form of + /// "[Application Name] Host". + /// + /// + /// The identifier of the PowerShell host to use for its profile path. + /// loaded. Used to resolve a profile path of the form 'X_profile.ps1' + /// where 'X' represents the value of hostProfileId. If null, a default + /// will be used. + /// + /// The host application's version. + /// The PowerShell host to use. + /// The path to the shared profile. + /// The path to the user specific profile. + /// Flags of features to enable. + /// Names or paths of additional modules to import. + /// The path to log to. + /// The minimum log event level. + /// Enable console if true. + /// Use PSReadLine if false, otherwise use the legacy readline implementation. + public HostStartupInfo( + string name, + string profileId, + Version version, + PSHost psHost, + ProfilePathInfo profilePaths, + IReadOnlyList featureFlags, + IReadOnlyList additionalModules, + string logPath, + int logLevel, + bool consoleReplEnabled, + bool usesLegacyReadLine) + { + Name = name ?? DefaultHostName; + ProfileId = profileId ?? DefaultHostProfileId; + Version = version ?? s_defaultHostVersion; + PSHost = psHost; + ProfilePaths = profilePaths; + FeatureFlags = featureFlags ?? Array.Empty(); + AdditionalModules = additionalModules ?? Array.Empty(); + LogPath = logPath; + LogLevel = logLevel; + ConsoleReplEnabled = consoleReplEnabled; + UsesLegacyReadLine = usesLegacyReadLine; + } + + #endregion + } + + public class ProfilePathInfo + { + public ProfilePathInfo( + string currentUserAllHosts, + string currentUserCurrentHost, + string allUsersAllHosts, + string allUsersCurrentHost) + { + CurrentUserAllHosts = currentUserAllHosts; + CurrentUserCurrentHost = currentUserCurrentHost; + AllUsersAllHosts = allUsersAllHosts; + AllUsersCurrentHost = allUsersCurrentHost; + } + + public string CurrentUserAllHosts { get; } + + public string CurrentUserCurrentHost { get; } + + public string AllUsersAllHosts { get; } + + public string AllUsersCurrentHost { get; } + } +} diff --git a/src/PowerShellEditorServices/Hosting/ProfilePaths.cs b/src/PowerShellEditorServices/Hosting/ProfilePaths.cs deleted file mode 100644 index 53eb714f1..000000000 --- a/src/PowerShellEditorServices/Hosting/ProfilePaths.cs +++ /dev/null @@ -1,108 +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.Collections.Generic; -using System.IO; -using System.Linq; - -namespace Microsoft.PowerShell.EditorServices.Hosting -{ - /// - /// Provides profile path resolution behavior relative to the name - /// of a particular PowerShell host. - /// - public class ProfilePaths - { - #region Constants - - /// - /// The file name for the "all hosts" profile. Also used as the - /// suffix for the host-specific profile filenames. - /// - public const string AllHostsProfileName = "profile.ps1"; - - #endregion - - #region Properties - - /// - /// Gets the profile path for all users, all hosts. - /// - public string AllUsersAllHosts { get; private set; } - - /// - /// Gets the profile path for all users, current host. - /// - public string AllUsersCurrentHost { get; private set; } - - /// - /// Gets the profile path for the current user, all hosts. - /// - public string CurrentUserAllHosts { get; private set; } - - /// - /// Gets the profile path for the current user and host. - /// - public string CurrentUserCurrentHost { get; private set; } - - #endregion - - #region Public Methods - - /// - /// Creates a new instance of the ProfilePaths class. - /// - /// - /// The identifier of the host used in the host-specific X_profile.ps1 filename. - /// - /// The base path to use for constructing AllUsers profile paths. - /// The base path to use for constructing CurrentUser profile paths. - public ProfilePaths( - string hostProfileId, - string baseAllUsersPath, - string baseCurrentUserPath) - { - this.Initialize(hostProfileId, baseAllUsersPath, baseCurrentUserPath); - } - - private void Initialize( - string hostProfileId, - string baseAllUsersPath, - string baseCurrentUserPath) - { - string currentHostProfileName = - string.Format( - "{0}_{1}", - hostProfileId, - AllHostsProfileName); - - this.AllUsersCurrentHost = Path.Combine(baseAllUsersPath, currentHostProfileName); - this.CurrentUserCurrentHost = Path.Combine(baseCurrentUserPath, currentHostProfileName); - this.AllUsersAllHosts = Path.Combine(baseAllUsersPath, AllHostsProfileName); - this.CurrentUserAllHosts = Path.Combine(baseCurrentUserPath, AllHostsProfileName); - } - - /// - /// Gets the list of profile paths that exist on the filesystem. - /// - /// An IEnumerable of profile path strings to be loaded. - public IEnumerable GetLoadableProfilePaths() - { - var profilePaths = - new string[] - { - this.AllUsersAllHosts, - this.AllUsersCurrentHost, - this.CurrentUserAllHosts, - this.CurrentUserCurrentHost - }; - - return profilePaths.Where(p => File.Exists(p)); - } - - #endregion - } -} - diff --git a/src/PowerShellEditorServices/Hosting/PsesLogLevel.cs b/src/PowerShellEditorServices/Hosting/PsesLogLevel.cs deleted file mode 100644 index d1b85077d..000000000 --- a/src/PowerShellEditorServices/Hosting/PsesLogLevel.cs +++ /dev/null @@ -1,45 +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 Microsoft.Extensions.Logging; - -namespace Microsoft.PowerShell.EditorServices.Hosting -{ - public enum PsesLogLevel - { - Diagnostic, - Verbose, - Normal, - Warning, - Error, - } - - internal static class PsesLogLevelExtensions - { - public static LogLevel ToExtensionsLogLevel(this PsesLogLevel logLevel) - { - switch (logLevel) - { - case PsesLogLevel.Diagnostic: - return LogLevel.Trace; - - case PsesLogLevel.Verbose: - return LogLevel.Debug; - - case PsesLogLevel.Normal: - return LogLevel.Information; - - case PsesLogLevel.Warning: - return LogLevel.Warning; - - case PsesLogLevel.Error: - return LogLevel.Error; - - default: - return LogLevel.Information; - } - } - } -} diff --git a/src/PowerShellEditorServices/Logging/HostLoggerAdapter.cs b/src/PowerShellEditorServices/Logging/HostLoggerAdapter.cs new file mode 100644 index 000000000..6b65e5d9a --- /dev/null +++ b/src/PowerShellEditorServices/Logging/HostLoggerAdapter.cs @@ -0,0 +1,39 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Text; + +namespace PowerShellEditorServices.Logging +{ + /// + /// Adapter class to allow logging events sent by the host to be recorded by PSES' logging infrastructure. + /// + internal class HostLoggerAdapter : IObserver<(int logLevel, string message)> + { + private readonly ILogger _logger; + + /// + /// Create a new host logger adapter. + /// + /// Factory to create logger instances with. + public HostLoggerAdapter(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger("HostLogs"); + } + + public void OnCompleted() + { + // Nothing to do; we simply don't send more log messages + } + + public void OnError(Exception error) + { + _logger.LogError(error, "Error in host logger"); + } + + public void OnNext((int logLevel, string message) value) + { + _logger.Log((LogLevel)value.logLevel, value.message); + } + } +} diff --git a/src/PowerShellEditorServices/PowerShellEditorServices.csproj b/src/PowerShellEditorServices/PowerShellEditorServices.csproj index b7e1c6ce5..bf7e9062b 100644 --- a/src/PowerShellEditorServices/PowerShellEditorServices.csproj +++ b/src/PowerShellEditorServices/PowerShellEditorServices.csproj @@ -1,4 +1,4 @@ - + @@ -8,23 +8,33 @@ netstandard2.0 Microsoft.PowerShell.EditorServices Latest + Debug;Release latest - - + latest + + + + <_Parameter1>Microsoft.PowerShell.EditorServices.Hosting + + + + + + diff --git a/src/PowerShellEditorServices/Server/NamedPipePsesLanguageServer.cs b/src/PowerShellEditorServices/Server/NamedPipePsesLanguageServer.cs deleted file mode 100644 index 16f9bc749..000000000 --- a/src/PowerShellEditorServices/Server/NamedPipePsesLanguageServer.cs +++ /dev/null @@ -1,155 +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.Collections.Generic; -using System.IO; -using System.IO.Pipes; -using System.Management.Automation.Host; -using System.Reflection; -using System.Security.AccessControl; -using System.Security.Principal; -using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Hosting; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.Server -{ - internal class NamedPipePsesLanguageServer : PsesLanguageServer - { - // This int will be casted to a PipeOptions enum that only exists in .NET Core 2.1 and up which is why it's not available to us in .NET Standard. - private const int CurrentUserOnly = 0x20000000; - - // In .NET Framework, NamedPipeServerStream has a constructor that takes in a PipeSecurity object. We will use reflection to call the constructor, - // since .NET Framework doesn't have the `CurrentUserOnly` PipeOption. - // doc: https://docs.microsoft.com/en-us/dotnet/api/system.io.pipes.namedpipeserverstream.-ctor?view=netframework-4.7.2#System_IO_Pipes_NamedPipeServerStream__ctor_System_String_System_IO_Pipes_PipeDirection_System_Int32_System_IO_Pipes_PipeTransmissionMode_System_IO_Pipes_PipeOptions_System_Int32_System_Int32_System_IO_Pipes_PipeSecurity_ - private static readonly ConstructorInfo s_netFrameworkPipeServerConstructor = - typeof(NamedPipeServerStream).GetConstructor(new[] { typeof(string), typeof(PipeDirection), typeof(int), typeof(PipeTransmissionMode), typeof(PipeOptions), typeof(int), typeof(int), typeof(PipeSecurity) }); - - private readonly string _namedPipeName; - private readonly string _outNamedPipeName; - - internal NamedPipePsesLanguageServer( - ILoggerFactory factory, - LogLevel minimumLogLevel, - bool enableConsoleRepl, - bool useLegacyReadLine, - HashSet featureFlags, - HostDetails hostDetails, - string[] additionalModules, - PSHost internalHost, - ProfilePaths profilePaths, - string namedPipeName, - string outNamedPipeName) : base( - factory, - minimumLogLevel, - enableConsoleRepl, - useLegacyReadLine, - featureFlags, - hostDetails, - additionalModules, - internalHost, - profilePaths) - { - _namedPipeName = namedPipeName; - _outNamedPipeName = outNamedPipeName; - } - - protected override (Stream input, Stream output) GetInputOutputStreams() - { - NamedPipeServerStream namedPipe = CreateNamedPipe( - _namedPipeName, - _outNamedPipeName, - out NamedPipeServerStream outNamedPipe); - - var logger = LoggerFactory.CreateLogger("NamedPipeConnection"); - - logger.LogInformation("Waiting for connection"); - namedPipe.WaitForConnection(); - if (outNamedPipe != null) - { - outNamedPipe.WaitForConnection(); - } - - logger.LogInformation("Connected"); - - return (namedPipe, outNamedPipe ?? namedPipe); - } - - private static NamedPipeServerStream CreateNamedPipe( - string inOutPipeName, - string outPipeName, - out NamedPipeServerStream outPipe) - { - // .NET Core implementation is simplest so try that first - if (VersionUtils.IsNetCore) - { - outPipe = outPipeName == null - ? null - : new NamedPipeServerStream( - pipeName: outPipeName, - direction: PipeDirection.Out, - maxNumberOfServerInstances: 1, - transmissionMode: PipeTransmissionMode.Byte, - options: (PipeOptions)CurrentUserOnly); - - return new NamedPipeServerStream( - pipeName: inOutPipeName, - direction: PipeDirection.InOut, - maxNumberOfServerInstances: 1, - transmissionMode: PipeTransmissionMode.Byte, - options: PipeOptions.Asynchronous | (PipeOptions)CurrentUserOnly); - } - - // Now deal with Windows PowerShell - // We need to use reflection to get a nice constructor - - var pipeSecurity = new PipeSecurity(); - - WindowsIdentity identity = WindowsIdentity.GetCurrent(); - WindowsPrincipal principal = new WindowsPrincipal(identity); - - if (principal.IsInRole(WindowsBuiltInRole.Administrator)) - { - // Allow the Administrators group full access to the pipe. - pipeSecurity.AddAccessRule(new PipeAccessRule( - new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null).Translate(typeof(NTAccount)), - PipeAccessRights.FullControl, AccessControlType.Allow)); - } - else - { - // Allow the current user read/write access to the pipe. - pipeSecurity.AddAccessRule(new PipeAccessRule( - WindowsIdentity.GetCurrent().User, - PipeAccessRights.ReadWrite, AccessControlType.Allow)); - } - - outPipe = outPipeName == null - ? null - : (NamedPipeServerStream)s_netFrameworkPipeServerConstructor.Invoke( - new object[] { - outPipeName, - PipeDirection.InOut, - 1, // maxNumberOfServerInstances - PipeTransmissionMode.Byte, - PipeOptions.Asynchronous, - 1024, // inBufferSize - 1024, // outBufferSize - pipeSecurity - }); - - return (NamedPipeServerStream)s_netFrameworkPipeServerConstructor.Invoke( - new object[] { - inOutPipeName, - PipeDirection.InOut, - 1, // maxNumberOfServerInstances - PipeTransmissionMode.Byte, - PipeOptions.Asynchronous, - 1024, // inBufferSize - 1024, // outBufferSize - pipeSecurity - }); - } - } -} diff --git a/src/PowerShellEditorServices/Server/PsesDebugServer.cs b/src/PowerShellEditorServices/Server/PsesDebugServer.cs index 98474b0be..dd2b2f644 100644 --- a/src/PowerShellEditorServices/Server/PsesDebugServer.cs +++ b/src/PowerShellEditorServices/Server/PsesDebugServer.cs @@ -16,50 +16,72 @@ namespace Microsoft.PowerShell.EditorServices.Server { - public class PsesDebugServer : IDisposable + /// + /// Server for hosting debug sessions. + /// + internal class PsesDebugServer : IDisposable { - protected readonly ILoggerFactory _loggerFactory; + private static bool s_hasRunPsrlStaticCtor = false; + private readonly Stream _inputStream; private readonly Stream _outputStream; + private readonly bool _useTempSession; + private readonly bool _usePSReadLine; + private readonly TaskCompletionSource _serverStopped; private IJsonRpcServer _jsonRpcServer; - private PowerShellContextService _powerShellContextService; - private readonly TaskCompletionSource _serverStopped; + protected readonly ILoggerFactory _loggerFactory; public PsesDebugServer( ILoggerFactory factory, Stream inputStream, - Stream outputStream) + Stream outputStream, + IServiceProvider serviceProvider, + bool useTempSession, + bool usePSReadLine) { _loggerFactory = factory; _inputStream = inputStream; _outputStream = outputStream; + ServiceProvider = serviceProvider; + _useTempSession = useTempSession; _serverStopped = new TaskCompletionSource(); + _usePSReadLine = usePSReadLine; } - public async Task StartAsync(IServiceProvider languageServerServiceProvider, bool useTempSession) + internal IServiceProvider ServiceProvider { get; } + + /// + /// Start the debug server listening. + /// + /// A task that completes when the server is ready. + public async Task StartAsync() { _jsonRpcServer = await JsonRpcServer.From(options => { options.Serializer = new DapProtocolSerializer(); options.Reciever = new DapReciever(); options.LoggerFactory = _loggerFactory; - ILogger logger = options.LoggerFactory.CreateLogger("DebugOptionsStartup"); + Extensions.Logging.ILogger logger = options.LoggerFactory.CreateLogger("DebugOptionsStartup"); // We need to let the PowerShell Context Service know that we are in a debug session // so that it doesn't send the powerShell/startDebugger message. - _powerShellContextService = languageServerServiceProvider.GetService(); + _powerShellContextService = ServiceProvider.GetService(); _powerShellContextService.IsDebugServerActive = true; // Needed to make sure PSReadLine's static properties are initialized in the pipeline thread. - _powerShellContextService - .ExecuteScriptStringAsync("[System.Runtime.CompilerServices.RuntimeHelpers]::RunClassConstructor([Microsoft.PowerShell.PSConsoleReadLine].TypeHandle)") - .Wait(); + if (!s_hasRunPsrlStaticCtor && _usePSReadLine) + { + s_hasRunPsrlStaticCtor = true; + _powerShellContextService + .ExecuteScriptStringAsync("[System.Runtime.CompilerServices.RuntimeHelpers]::RunClassConstructor([Microsoft.PowerShell.PSConsoleReadLine].TypeHandle)") + .Wait(); + } options.Services = new ServiceCollection() - .AddPsesDebugServices(languageServerServiceProvider, this, useTempSession); + .AddPsesDebugServices(ServiceProvider, this, _useTempSession); options .WithInput(_inputStream) diff --git a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs index 3daf3cd52..fbb975b6d 100644 --- a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs +++ b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs @@ -3,12 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using System.Collections.Generic; using System.IO; -using System.Management.Automation; -using System.Management.Automation.Host; -using System.Management.Automation.Runspaces; -using System.Reflection; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -20,66 +15,59 @@ namespace Microsoft.PowerShell.EditorServices.Server { - internal abstract class PsesLanguageServer + /// + /// Server runner class for handling LSP messages for Editor Services. + /// + internal class PsesLanguageServer { internal ILoggerFactory LoggerFactory { get; private set; } + internal ILanguageServer LanguageServer { get; private set; } private readonly LogLevel _minimumLogLevel; - private readonly bool _enableConsoleRepl; - private readonly bool _useLegacyReadLine; - private readonly HashSet _featureFlags; - private readonly HostDetails _hostDetails; - private readonly string[] _additionalModules; - private readonly PSHost _internalHost; - private readonly ProfilePaths _profilePaths; + private readonly Stream _inputStream; + private readonly Stream _outputStream; + private readonly HostStartupInfo _hostDetails; private readonly TaskCompletionSource _serverStart; - internal PsesLanguageServer( + /// + /// Create a new language server instance. + /// + /// Factory to create loggers with. + /// Protocol transport input stream. + /// Protocol transport output stream. + /// Host configuration to instantiate the server and services with. + public PsesLanguageServer( ILoggerFactory factory, - LogLevel minimumLogLevel, - bool enableConsoleRepl, - bool useLegacyReadLine, - HashSet featureFlags, - HostDetails hostDetails, - string[] additionalModules, - PSHost internalHost, - ProfilePaths profilePaths) + Stream inputStream, + Stream outputStream, + HostStartupInfo hostStartupInfo) { LoggerFactory = factory; - _minimumLogLevel = minimumLogLevel; - _enableConsoleRepl = enableConsoleRepl; - _useLegacyReadLine = useLegacyReadLine; - _featureFlags = featureFlags; - _hostDetails = hostDetails; - _additionalModules = additionalModules; - _internalHost = internalHost; - _profilePaths = profilePaths; + _minimumLogLevel = (LogLevel)hostStartupInfo.LogLevel; + _inputStream = inputStream; + _outputStream = outputStream; + _hostDetails = hostStartupInfo; _serverStart = new TaskCompletionSource(); } + /// + /// Start the server listening for input. + /// + /// A task that completes when the server is ready and listening. public async Task StartAsync() { LanguageServer = await OmniSharp.Extensions.LanguageServer.Server.LanguageServer.From(options => { - (Stream input, Stream output) = GetInputOutputStreams(); - options - .WithInput(input) - .WithOutput(output) + .WithInput(_inputStream) + .WithOutput(_outputStream) .WithServices(serviceCollection => serviceCollection - .AddPsesLanguageServices( - _profilePaths, - _featureFlags, - _enableConsoleRepl, - _useLegacyReadLine, - _internalHost, - _hostDetails, - _additionalModules)) + .AddPsesLanguageServices(_hostDetails)) .ConfigureLogging(builder => builder .AddSerilog(Log.Logger) - .AddLanguageServer(LogLevel.Trace) - .SetMinimumLevel(LogLevel.Trace)) + .AddLanguageServer(_minimumLogLevel) + .SetMinimumLevel(_minimumLogLevel)) .WithHandler() .WithHandler() .WithHandler() @@ -126,14 +114,18 @@ await serviceProvider.GetService().SetWorkingDirectory } }); }); + + _serverStart.SetResult(true); } + /// + /// Get a task that completes when the server is shut down. + /// + /// A task that completes when the server is shut down. public async Task WaitForShutdown() { await _serverStart.Task; await LanguageServer.WaitForExit; } - - protected abstract (Stream input, Stream output) GetInputOutputStreams(); } } diff --git a/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs b/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs index 074178567..7ad24f36b 100644 --- a/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs +++ b/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs @@ -15,15 +15,9 @@ namespace Microsoft.PowerShell.EditorServices.Server { internal static class PsesServiceCollectionExtensions { - public static IServiceCollection AddPsesLanguageServices ( + public static IServiceCollection AddPsesLanguageServices( this IServiceCollection collection, - ProfilePaths profilePaths, - HashSet featureFlags, - bool enableConsoleRepl, - bool useLegacyReadLine, - PSHost internalHost, - HostDetails hostDetails, - string[] additionalModules) + HostStartupInfo hostStartupInfo) { return collection.AddSingleton() .AddSingleton() @@ -33,13 +27,7 @@ public static IServiceCollection AddPsesLanguageServices ( PowerShellContextService.Create( provider.GetService(), provider.GetService(), - profilePaths, - featureFlags, - enableConsoleRepl, - useLegacyReadLine, - internalHost, - hostDetails, - additionalModules)) + hostStartupInfo)) .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/src/PowerShellEditorServices/Server/StdioPsesLanguageServer.cs b/src/PowerShellEditorServices/Server/StdioPsesLanguageServer.cs deleted file mode 100644 index 741404cac..000000000 --- a/src/PowerShellEditorServices/Server/StdioPsesLanguageServer.cs +++ /dev/null @@ -1,44 +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.Collections.Generic; -using System.IO; -using System.Management.Automation.Host; -using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Hosting; - -namespace Microsoft.PowerShell.EditorServices.Server -{ - internal class StdioPsesLanguageServer : PsesLanguageServer - { - internal StdioPsesLanguageServer( - ILoggerFactory factory, - LogLevel minimumLogLevel, - HashSet featureFlags, - HostDetails hostDetails, - string[] additionalModules, - PSHost internalHost, - ProfilePaths profilePaths) : base( - factory, - minimumLogLevel, - // Stdio server can't support an integrated console so we pass in false for - // enableConsoleRepl and useLegacyReadLine. - enableConsoleRepl: false, - useLegacyReadLine: false, - featureFlags, - hostDetails, - additionalModules, - internalHost, - profilePaths) - { - - } - - protected override (Stream input, Stream output) GetInputOutputStreams() - { - return (System.Console.OpenStandardInput(), System.Console.OpenStandardOutput()); - } - } -} diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs index 615c81357..037b8427f 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs @@ -34,6 +34,11 @@ namespace Microsoft.PowerShell.EditorServices.Services /// public class PowerShellContextService : IDisposable, IHostSupportsInteractiveSession { + private static readonly string s_commandsModulePath = Path.GetFullPath( + Path.Combine( + Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), + "../../Commands/PowerShellEditorServices.Commands.psd1")); + private static readonly Action s_runspaceApartmentStateSetter; static PowerShellContextService() @@ -59,7 +64,7 @@ static PowerShellContextService() private RunspaceDetails initialRunspace; private SessionDetails mostRecentSessionDetails; - private ProfilePaths profilePaths; + private ProfilePathInfo profilePaths; private IVersionSpecificOperations versionSpecificOperations; @@ -168,14 +173,7 @@ public PowerShellContextService( public static PowerShellContextService Create( ILoggerFactory factory, OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServer languageServer, - ProfilePaths profilePaths, - HashSet featureFlags, - bool enableConsoleRepl, - bool useLegacyReadLine, - PSHost internalHost, - HostDetails hostDetails, - string[] additionalModules - ) + HostStartupInfo hostStartupInfo) { var logger = factory.CreateLogger(); @@ -184,7 +182,8 @@ string[] additionalModules // We also want it if we are either: // * On Windows on any version OR // * On Linux or macOS on any version greater than or equal to 7 - bool shouldUsePSReadLine = enableConsoleRepl && !useLegacyReadLine + bool shouldUsePSReadLine = hostStartupInfo.ConsoleReplEnabled + && !hostStartupInfo.UsesLegacyReadLine && (VersionUtils.IsWindows || !VersionUtils.IsPS6); var powerShellContext = new PowerShellContextService( @@ -193,28 +192,25 @@ string[] additionalModules shouldUsePSReadLine); EditorServicesPSHostUserInterface hostUserInterface = - enableConsoleRepl - ? (EditorServicesPSHostUserInterface)new TerminalPSHostUserInterface(powerShellContext, logger, internalHost) + hostStartupInfo.ConsoleReplEnabled + ? (EditorServicesPSHostUserInterface)new TerminalPSHostUserInterface(powerShellContext, logger, hostStartupInfo.PSHost) : new ProtocolPSHostUserInterface(languageServer, powerShellContext, logger); EditorServicesPSHost psHost = new EditorServicesPSHost( powerShellContext, - hostDetails, + hostStartupInfo, hostUserInterface, logger); Runspace initialRunspace = PowerShellContextService.CreateRunspace(psHost); - powerShellContext.Initialize(profilePaths, initialRunspace, true, hostUserInterface); + powerShellContext.Initialize(hostStartupInfo.ProfilePaths, initialRunspace, true, hostUserInterface); - powerShellContext.ImportCommandsModuleAsync( - Path.Combine( - Path.GetDirectoryName(typeof(PowerShellContextService).GetTypeInfo().Assembly.Location), - @"..\Commands")); + powerShellContext.ImportCommandsModuleAsync(); // TODO: This can be moved to the point after the $psEditor object // gets initialized when that is done earlier than LanguageServer.Initialize - foreach (string module in additionalModules) + foreach (string module in hostStartupInfo.AdditionalModules) { var command = new PSCommand() @@ -241,7 +237,7 @@ string[] additionalModules /// An ILogger implementation to use for this instance. /// public static Runspace CreateRunspace( - HostDetails hostDetails, + HostStartupInfo hostDetails, PowerShellContextService powerShellContext, EditorServicesPSHostUserInterface hostUserInterface, ILogger logger) @@ -289,11 +285,11 @@ public static Runspace CreateRunspace(PSHost psHost) /// The initial runspace to use for this instance. /// If true, the PowerShellContext owns this runspace. public void Initialize( - ProfilePaths profilePaths, + ProfilePathInfo profilePaths, Runspace initialRunspace, bool ownsInitialRunspace) { - this.Initialize(profilePaths, initialRunspace, ownsInitialRunspace, null); + this.Initialize(profilePaths, initialRunspace, ownsInitialRunspace, consoleHost: null); } /// @@ -305,7 +301,7 @@ public void Initialize( /// If true, the PowerShellContext owns this runspace. /// An IHostOutput implementation. Optional. public void Initialize( - ProfilePaths profilePaths, + ProfilePathInfo profilePaths, Runspace initialRunspace, bool ownsInitialRunspace, IHostOutput consoleHost) @@ -333,7 +329,7 @@ public void Initialize( this.LocalPowerShellVersion, RunspaceLocation.Local, RunspaceContext.Original, - null); + connectionString: null); this.CurrentRunspace = this.initialRunspace; // Write out the PowerShell version for tracking purposes @@ -369,7 +365,7 @@ public void Initialize( // Set the $profile variable in the runspace this.profilePaths = profilePaths; - if (this.profilePaths != null) + if (profilePaths != null) { this.SetProfileVariableInCurrentRunspace(profilePaths); } @@ -423,19 +419,14 @@ public void Initialize( /// Imports the PowerShellEditorServices.Commands module into /// the runspace. This method will be moved somewhere else soon. /// - /// /// - public Task ImportCommandsModuleAsync(string moduleBasePath) + public Task ImportCommandsModuleAsync() { - PSCommand importCommand = new PSCommand(); - importCommand + PSCommand importCommand = new PSCommand() .AddCommand("Import-Module") - .AddArgument( - Path.Combine( - moduleBasePath, - "PowerShellEditorServices.Commands.psd1")); + .AddArgument(s_commandsModulePath); - return this.ExecuteCommandAsync(importCommand, false, false); + return this.ExecuteCommandAsync(importCommand, sendOutputToHost: false, sendErrorToHost: false); } private static bool CheckIfRunspaceNeedsEventHandlers(RunspaceDetails runspaceDetails) @@ -1148,7 +1139,7 @@ public async Task LoadHostProfilesAsync() if (this.profilePaths != null) { // Load any of the profile paths that exist - foreach (var profilePath in this.profilePaths.GetLoadableProfilePaths()) + foreach (var profilePath in GetLoadableProfilePaths(this.profilePaths)) { PSCommand command = new PSCommand(); command.AddCommand(profilePath, false); @@ -2216,7 +2207,7 @@ private SessionDetails GetSessionDetailsInNestedPipeline() }); } - private void SetProfileVariableInCurrentRunspace(ProfilePaths profilePaths) + private void SetProfileVariableInCurrentRunspace(ProfilePathInfo profilePaths) { // Create the $profile variable PSObject profile = new PSObject(profilePaths.CurrentUserCurrentHost); @@ -2275,6 +2266,22 @@ private void HandleRunspaceStateChanged(object sender, RunspaceStateEventArgs ar } } + private static IEnumerable GetLoadableProfilePaths(ProfilePathInfo profilePaths) + { + if (profilePaths == null) + { + yield break; + } + + foreach (string path in new [] { profilePaths.AllUsersAllHosts, profilePaths.AllUsersCurrentHost, profilePaths.CurrentUserAllHosts, profilePaths.CurrentUserCurrentHost }) + { + if (path != null && File.Exists(path)) + { + yield return path; + } + } + } + #endregion #region Events diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/EditorServicesPSHost.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/EditorServicesPSHost.cs index 7eab3458d..5eda7bd6c 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/EditorServicesPSHost.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/EditorServicesPSHost.cs @@ -22,7 +22,7 @@ public class EditorServicesPSHost : PSHost, IHostSupportsInteractiveSession #region Private Fields private ILogger Logger; - private HostDetails hostDetails; + private HostStartupInfo hostDetails; private Guid instanceId = Guid.NewGuid(); private EditorServicesPSHostUserInterface hostUserInterface; private IHostSupportsInteractiveSession hostSupportsInteractiveSession; @@ -48,7 +48,7 @@ public class EditorServicesPSHost : PSHost, IHostSupportsInteractiveSession /// An ILogger implementation to use for this host. public EditorServicesPSHost( PowerShellContextService powerShellContext, - HostDetails hostDetails, + HostStartupInfo hostDetails, EditorServicesPSHostUserInterface hostUserInterface, ILogger logger) { diff --git a/test/PowerShellEditorServices.Test.E2E/TestsFixture.cs b/test/PowerShellEditorServices.Test.E2E/TestsFixture.cs index 7b51226f3..e56d4668f 100644 --- a/test/PowerShellEditorServices.Test.E2E/TestsFixture.cs +++ b/test/PowerShellEditorServices.Test.E2E/TestsFixture.cs @@ -58,16 +58,17 @@ public async Task InitializeAsync() List args = new List { - Path.Combine(s_bundledModulePath, "PowerShellEditorServices", "Start-EditorServices.ps1"), - "-LogPath", s_logPath, + "&", + SingleQuoteEscape(Path.Combine(s_bundledModulePath, "PowerShellEditorServices", "Start-EditorServices.ps1")), + "-LogPath", SingleQuoteEscape(s_logPath), "-LogLevel", s_logLevel, - "-SessionDetailsPath", s_sessionDetailsPath, + "-SessionDetailsPath", SingleQuoteEscape(s_sessionDetailsPath), "-FeatureFlags", string.Join(',', s_featureFlags), "-HostName", s_hostName, "-HostProfileId", s_hostProfileId, "-HostVersion", s_hostVersion, "-AdditionalModules", string.Join(',', s_additionalModules), - "-BundledModulesPath", s_bundledModulePath, + "-BundledModulesPath", SingleQuoteEscape(s_bundledModulePath), "-Stdio" }; @@ -95,5 +96,10 @@ public virtual async Task DisposeAsync() public abstract Task CustomInitializeAsync( ILoggerFactory factory, StdioServerProcess process); + + private static string SingleQuoteEscape(string str) + { + return $"'{str.Replace("'", "''")}'"; + } } }