Close

Argument Trace: HostVersion

A project log for Source Code Analysis: PowerShellEditorServices

Source Code Breakdown of PowerShellEditorServices (at the time of v3.5.4)

nicholas-jacksonNicholas Jackson 10/31/2022 at 18:140 Comments

Tracing HostVersion parameter to Start-EditorServices

This is my trace of the HostVersion parameter. The purpose of this log is to identify all uses for the HostVersion parameter. These uses will be documented in my unofficial documentation on the Start-EditorServices command

  1. Unofficial Docs - Syntax
    1. [-HostVersion <
          string,
          mandatory,
          ="" not="" null="" or="" empty=""
      >] 
  2. PSES/module/PSES/Start-EditorServices.ps1
    1. [Parameter(Mandatory=$true)]
      [ValidateNotNullOrEmpty()]
      [string]
      $HostVersion
  3.  PSES/module/PSES/Start-EditorServices.ps1
    1. Import-Module -Name "$PSScriptRoot/PowerShellEditorServices.psd1"
      Start-EditorServices @PSBoundParameters
  4. PSES/module/PSES/PowerShellEditorServices.psd1
    1. # Script module or binary module file associated with this manifest.
      RootModule = if ($PSEdition -eq 'Core')
          {
              'bin/Core/Microsoft.PowerShell.EditorServices.Hosting.dll'
          }
          else
          {
              'bin/Desktop/Microsoft.PowerShell.EditorServices.Hosting.dll'
          }
    2. I believe this is a call to the Microsoft.PowerShell.EditorServices.Hosting namespace - still uncertain about this.
  5. PSES/src/PSES.Hosting/Commands/StartEditorServicesCommand.cs
    1. /// <summary>
      /// The Start-EditorServices command, the conventional entrypoint for PowerShell Editor Services.
      /// </summary>
      [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance",
        "CA1819:Properties should not return arrays",
        Justification = "Cmdlet parameters can be arrays")]
      [Cmdlet(VerbsLifecycle.Start, "EditorServices", 
        DefaultParameterSetName = "NamedPipe")]
      public sealed class StartEditorServicesCommand : PSCmdlet
      { ... }
    2. The parameter from the NamedPipe parameter set:
      1. /// <summary>
        /// The version to report for the host.
        /// </summary>
        [Parameter(Mandatory = true)]
        [ValidateNotNullOrEmpty]
        public Version HostVersion { get; set; }
    3. The only use for HostVersion in the entire CMDLethopefully this will be an easy argument trace :D
      1. HostInfo hostInfo = new(HostName, HostProfileId, HostVersion);
      2. HostInfo is just a data structure that stores strings and semvers - no need to trace into this. will need to trace the hostinfo object tho :(

Tracing HostInfo/EditorServicesConfig

From my previous experiences with these objects, this is gonna be a long trace :(

  1. PSES/src/PSES.Hosting/Commands/StartEditorServicesCommand.cs (CreateConfigObject Private Method)
    1. The only use of HostVersion from above is in the private method CreateConfigObject from the StartEditorServicesCommand (The C# object representing the Start-EditorServices Cmdlet)
    2. This is a longer function, so I won't list the source code here, but I'll summarize it:
      1. Logs diagnostic message "Creating host configuration"
      2. Expands BundledModulesPath (cmdlet parameter) if it is a relative path
      3. Gets the $profile object from PowerShell
      4. Creates the HostInfo object
      5. Pulls in some kind of session state stuff from the runspace object - I have no idea what this is or if it is even useful :/
      6. Instantiates an EditorServicesConfig object with
        1. the HostInfo object,
        2. the PSHost object from the PWSH runtime the CMDlet will run in
        3. the SessionDetailsPath cmdlet parameter
        4. the Expanded BundledModulesPath path-string
        5. the LogPath cmdlet parameter
      7. Sets these properties on the new EditorServicesConfig object:
        1. FeatureFlags with the FeatureFlags cmdlet parameter
        2. LogLevel with the LogLevel cmdlet parameter
        3. ConsoleRepl with GetReplKind private method
          1. REPL is short for Read-Eval-Print-Loop
          2. This is the function that determines which readline Powershell is using by pulling from an Enum<out int>
          3. The 3 options are:
            1. None - no interactive console
            2. LeagacyReadLine - the readline implementation before MicroSoft ported in the PSReadLine repository
            3. PSReadLine - the latest readline implementation
        4. AdditionalModules with the AdditionalModules cmdlet parameter
        5. Service Transport Configs with:
          1. these private methods:
            1. GetLanguageServiceTransport
            2. GetDebugServiceTransport
          2. these return a config representing the type of pipe used for each service (named-pipe/split-named-pipes/stdio-pipe)
        6. InitialSessionState with the session state stuff from earlier
        7. ProfilePaths struct (ProfilePathConfig enum) with the $profile object from earlier - note for devs: the enums and the internal methods do nothing, but bloat the code.
        8. StartupBanner with the StartupBanner cmdlet parameter
          1. default can be found hereI can use this later when I trace the StartupBanner object. Actually, any mention of "cmdlet parameter" in this log can be used for tracing.
      8. Returns the EditorServicesConfig object
        1. This object is effectively just a struct that stores the above values. It does not have any method members
  2. PSES/src/PSES.Hosting/Commands/StartEditorServicesCommand.cs (EndProcessing Protected Method)
    1. This is an override to Cmdlet.EndProcessing Method
    2. The call to CreateConfigObject()
      1. EditorServicesConfig editorServicesConfig = CreateConfigObject();
    3. Pass to EditorServicesLoader.Create() only use for editorServicesConfig
      1. using EditorServicesLoader psesLoader =
          EditorServicesLoader.Create(
            _logger,
            editorServicesConfig,
            sessionFileWriter,
            _loggerUnsubscribers );
    4. Only call to the psesLoader object
      1. // Synchronously start editor services and wait here until it shuts down.
        #pragma warning disable VSTHRD002
        
        psesLoader.LoadAndRunEditorServicesAsync().GetAwaiter().GetResult();
        
        #pragma warning restore VSTHRD002 
  3. PSES/src/PSES.Hosting/EditorServicesLoader.cs (Create() - public static method)
    1. editorServicesConfig from above gets passed in as hostConfig
    2. this function is just a named constructor. The actual constructor does nothing except assign it's parameters in the private variable stack
    3. this function just loads in some additional c# assemblies then returns the new EditorServicesConfig object
    4. note: the stored EditorServicesConfig (_hostConfig) is stored in the "private" stack
  4. PSES/src/PSES.Hosting/EditorServicesLoader.cs (List of all uses of _hostConfig in this class)
    1. used within:
      1. LoadAndRunEditorServicesAsync - public task
        1. Calls the following 3 methods
      2. UpdatePSModulePath - public method
      3. LogHostInformation - private method
      4. ValidateConfiguration - private method
  5. PSES/src/PSES.Hosting/EditorServicesLoader.cs (UpdatePSModulePath - public method)
    1. This private method is relatively short:
      1. 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);
          if ($"{psModulePath}{Path.PathSeparator}".Contains($"{_hostConfig.BundledModulePath}{Path.PathSeparator}")) {
            _logger.Log(PsesLogLevel.Diagnostic, "BundledModulePath already set, skipping");
            return;
          }
          psModulePath = $ "{psModulePath}{Path.PathSeparator}{_hostConfig.BundledModulePath}";
          Environment.SetEnvironmentVariable("PSModulePath", psModulePath);
          _logger.Log(PsesLogLevel.Verbose, $ "Updated PSModulePath to: '{psModulePath}'");
        }
    2. This doesn't manipulate HostInfo.Version at all, but it does show that BundledModulePath is a sub path of $env:PSModulePath, specifically "${$env:PSModulePath}\${$HostVersion}"
  6. PSES/src/PSES.Hosting/EditorServicesLoader.cs (LogHostInformation - private method)
    1. This method is lengthy, but doesn't seem to manipulate HostVersion
    2. Simply dumps HostVersion to the logger.
    3. It is important to note that Host Version and PowerShell Version are 2 separate log entries here
  7. PSES/src/PSES.Hosting/EditorServicesLoader.cs (ValidateConfiguration - private method)
    1. This method is also short
      1. 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));
          }
        }
    2. No manipulations to HostVersion
  8. LoadAndRunEditorServicesAsync - public task
    1. This is task that gets called with the "psesLoader.LoadAndRunEditorServicesAsync" function
      1. public Task LoadAndRunEditorServicesAsync() {
        
          LogHostInformation();
        
          #if!CoreCLR
          // Make sure the .NET Framework version supports .NET Standard 2.0
          CheckNetFxVersion();
          #endif
        
          UpdatePSModulePath();
          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");
        
          _editorServicesRunner = new EditorServicesRunner(_logger, _hostConfig, _sessionFileWriter, _loggersToUnsubscribe);
        
          // The trigger method for Editor Services
          return Task.Run(_editorServicesRunner.RunUntilShutdown);
        }
    2. The only significant calls here are the construction of the EditorServicesRunner object and the Task.Run(_editorServicesRunner.RunUntilShutdown)
  9. PSES/src/PSES.Hosting/EditorServicesLoader.cs (EditorServicesRunner - internal class constructor)
    1. Regular setter-constructor (private variables are prefixed with "_"
    2. Signature
      1. public EditorServicesRunner(
          HostLogger logger,
          EditorServicesConfig config,
          ISessionFileWriter sessionFileWriter,
          IReadOnlyCollection<IDisposable> loggersToUnsubscribe) 
  10. PSES/src/PSES.Hosting/EditorServicesLoader.cs (RunUntilShutdown - public task)
    1. wrapper for CreateEditorServicesAndRunUntilShutdown (private async task)
    2. Makes a few log entries to logger, and writes the "session started" state to the session.json file
  11. PSES/src/PSES.Hosting/EditorServicesLoader.cs (CreateEditorServicesAndRunUntilShutdown - private async task)
    1. This is the C# Task factory wrapped by RunUntilShutdown
    2. It is lengthy, so I will break it up:
      1. determines which servers it will be creating (debug, server, or both):
        1. bool creatingLanguageServer = _config.LanguageServiceTransport != null;
          bool creatingDebugServer = _config.DebugServiceTransport != null;
          bool isTempDebugSession = creatingDebugServer && !creatingLanguageServer;
      2. !!! - creates a HostStartupInfo object - this object is a wrapper for HostInfo, which wraps the HostVersion variable
      3. If it is only running a debug server, run it then return out of the task
      4. Unsubs from the host logger before the session's server is run
      5. Writes the startup bannerbut only if there is a Read-Eval-Print Loop running
      6. Creates a PsesLanguageServer using the CreateLanguageServerAsync method (declaration)
        1. !!! - the HostStartupInfo object from above is passed to this method
        2. this task is halted until the language server pipe is connected to. This halts the entire CreateEditorServicesAndRunUntilShutdown Task
        3. Serves as a wrapper for EditorServicesServerFactory.CreateLanguageServer()
        4. !!! - Only significant calls to this new object:
          1. languageServer.StartAsync()
          2. languageServer.WaitForShutDown()
      7. Creates the debug server and passes the PsesLanguageServer to it's constructor
      8. Awaits language server shutdown
      9. Resubs the host logger
  12. PSES/src/PSES.Hosting/EditorServicesLoader.cs (CreateHostStartupInfo - private method)
    1. This is the factory used to create the HostStartupInfo from 11.b.ii of this log (link is to the source code)
      1. This is just a wrapper for the HostStartupInfo constructor in order to for some reason pointlessly clone _config.ProfilePaths??
      2. HostStartupInfo's constructor for the most part is just a setter-constructor (Parameter -> UpperCamelCase Properties)
        1. Only deviation is that it again adds parsing to the bundledmodulepath variable. - Why do we keep doing this?? We can't just do this in one spot guys?
  13. PSES/src/PSES.Hosting/EditorServicesLoader.cs (CreateLanguageServerAsync - private async method)
    1. see 11.b.vi

  14. PSES/src/PSES/Hosting/EditorServicesServerFactory (CreateLanguageServer - public method)
    1. see 11.b.vi.iii
    2. wraps PSESLanguageServer constructor
  15. PSES/src/PSES/Server/PsesLanguageServer.cs (PsesLanguageServer - public constructor)
    1. see above (14.b)
    2. just a setter-constructor
  16. PSES/src/PSES/Server/PsesLanguageServer.cs (StartAsync - public async task)
    1. see 11.b.vi.iv.i
    2. wraps OmniSharp LanguageServer.From constructor
    3. only usage of the HostStartupInfo object is in the AddPsesLanguageServices call
  17. PSES/src/PSES/Server/PsesLanguageServer.cs (WaitForShutdown - public task)
    1. see 11.b.vi.iv.ii
    2. wraps Omnisharp LanguageServer.WaitForExit task and shutsdown _psesHost (this is created as a service in the From constructor)
  18. PSES/src/PSES/Server/PsesServerCollectionExtensions.cs (AddPsesLanguageServices - extension method for IServiceCollection)
    1. Just adds the object as a service. According to the search below, there is no further reference to this object's version parameters.
  19. Services check (this should be through PsesLanguageServer.LanguageServer.Services)
    1. according to this search, the only instance of PsesLanguageServer is the ones I've already documented.
    2. EditorServicesServerFactory.cs has 1 ref to LanguageServer.Services, and this is to create the debug server
      1. this is also the only instance of it in EditorServicesRunner.cs

Discussions