Native PSSession PowerShell Scripting
The exploration of CIM and using the CimSession
.NET class in PowerShell scripting got me thinking about another remoting option, PSSession
. This could be another option for creating PowerShell scripts with remoting baked in. I'd like to have a function that doesn't rely on parameter sets to distinguish between a computer name and an existing PSSession.
However, unlike CimSession
, I can't easily create a PSSession
object from a computer name. There is a little more involved, but it isn't too difficult. I thought I'd demonstrate how to crate and use a PSSession
object from scratch. If nothing else, hopefully, you'll learn a little more about how PowerShell remoting works under the hood and maybe pickup a scripting idea or two.
What I want to demonstrate requires PowerShell 7. I'm going to rely on a static method that doesn't exist in Windows PowerShell. But remember, you can still remotely connect to and manage systems running Windows PowerShell from a PowerShell 7 desktop. I am also leveraging traditional PowerShell remoting which means a Windows platform. Although I will show you how to setup an SSH PowerShell remoting session from scratch.
There are several pieces to this puzzle. Creating a PSSession
object is not as easy as what I showed you with CimSession
. We know we ultimately need to create a System.Management.Automation.Runspaces.PSSession
object. It was easy enough to create a session using New-PSSession
and getting the type name with Get-Member
. I'll dig deeper using Get-TypeMember
.

In PowerShell 7 I can use the Create
method.
PS C:\> Get-TypeMember System.Management.Automation.Runspaces.PSSession -Name Create | Select -ExpandProperty Syntax
$obj.Create([Runspace]runspace,[String]transportName,[PSCmdlet]psCmdlet)
So I need a runspace object. Let's see what that entails.
To create a runspace I can use the [RunspaceFactory]
class.

The CreateRunspace
method looks promising.
PS C:\> Get-TypeMember RunspaceFactory -Name CreateRunspace | Select-Object -ExpandProperty Syntax
$obj.CreateRunspace()
$obj.CreateRunspace([PSHost]host)
$obj.CreateRunspace([InitialSessionState]initialSessionState)
$obj.CreateRunspace([PSHost]host,[InitialSessionState]initialSessionState)
$obj.CreateRunspace([RunspaceConnectionInfo]connectionInfo,[PSHost]host,[TypeTable]typeTable)
$obj.CreateRunspace([RunspaceConnectionInfo]connectionInfo,[PSHost]host,[TypeTable]typeTable,[PSPrimitiveDictionary]applicationArguments)
$obj.CreateRunspace([RunspaceConnectionInfo]connectionInfo,[PSHost]host,[TypeTable]typeTable,[PSPrimitiveDictionary]applicationArguments,[String]name)
$obj.CreateRunspace([PSHost]host,[RunspaceConnectionInfo]connectionInfo)
$obj.CreateRunspace([RunspaceConnectionInfo]connectionInfo)
Because I know I will be writing a function that I want to use with remoting, I bet I need to look into how to create a RunspaceConnectionInfo
object. I'm guessing this is part of the Runspaces
namespace. I'll start typing the class name in PowerShell.
PS C:\>[System.Management.Automation.Runspaces.
After the period, I can press Ctrl+Tab
and get a list of all possible completions.

Let's see what this class looks like.

I don't see any likely methods, so I can see if there is a a standard New()
constructor.

Creating a ConnectionInfo Object
Even though there are many options, let's see if the generic New()
method will suffice.
PS C:\> $ci = [System.Management.Automation.Runspaces.WSManConnectionInfo]::new()
PS C:\> $ci | Select *
ConnectionUri : http://localhost/wsman
ComputerName : localhost
Scheme : http
Port : 80
AppName : /wsman
Credential :
ShellUri : http://schemas.microsoft.com/powershell/Microsoft.PowerShell
AuthenticationMechanism : Default
CertificateThumbprint :
MaximumConnectionRedirectionCount : 0
MaximumReceivedDataSizePerCommand :
MaximumReceivedObjectSize :
UseCompression : True
NoMachineProfile : False
ProxyAccessType : None
ProxyAuthentication : Default
ProxyCredential :
SkipCACheck : False
SkipCNCheck : False
SkipRevocationCheck : False
NoEncryption : False
UseUTF16 : False
OutputBufferingMode : None
IncludePortInSPN : False
EnableNetworkAccess : False
MaxConnectionRetryCount : 5
Culture : en-US
UICulture : en-US
OpenTimeout : 180000
CancelTimeout : 60000
OperationTimeout : 180000
IdleTimeout : -1
MaxIdleTimeout : 2147483647
I'll try changing the computer name to a remote server.
$ci.ComputerName = "DOM1"
Nice. The ConnectionURI
property automatically updated.
PS C:\> $ci.ConnectionUri
AbsolutePath : /wsman
AbsoluteUri : http://dom1/wsman
LocalPath : /wsman
Authority : dom1
HostNameType : Dns
IsDefaultPort : True
IsFile : False
IsLoopback : False
PathAndQuery : /wsman
Segments : {/, wsman}
IsUnc : False
Host : dom1
Port : 80
Query :
Fragment :
Scheme : http
OriginalString : http://DOM1/wsman
DnsSafeHost : dom1
IdnHost : dom1
IsAbsoluteUri : True
UserEscaped : False
UserInfo :
And I verified this is the object type I need.
PS C:\> $ci.GetType().Name
WSManConnectionInfo
PS C:\> $ci.PSObject.TypeNames
System.Management.Automation.Runspaces.WSManConnectionInfo
System.Management.Automation.Runspaces.RunspaceConnectionInfo
System.Object
Creating the Runspace
I know from my earlier research that I can create runspace using a ConnectionInfo
object.
PS C:\> $rs = [runspacefactory]::CreateRunspace($ci)
PS C:\> $rs
Id Name ComputerName Type State Availability
-- ---- ------------ ---- ----- ------------
4 Runspace4 DOM1 Remote BeforeOpen None
This runspace doesn't belong to anything yet. I need to create a PSSession
object.
Creating the PSSession
Based on my earlier research I can use the Create()
method.
$ps = [System.Management.Automation.Runspaces.PSSession]::Create($rs,"WSMan",$PSCmdlet)
Because I created this from scratch, Get-PSSession
doesn't show it.

I see I need to open the session.
$ps.Runspace.Open()
This may take a moment as authentication has to happen. My session is built using my credential for the connection to DOM
. If you look back at the WSManConnectionInfo
object you'll see there is a Credential
property you could set.
Adding Commands
Finally, I need to run something. For that I will need a pipeline.
$pl = $ps.Runspace.CreatePipeline()
The Pipeline
object has a Commands
property which is this type of object.
PS C:\> Get-TypeMember System.Management.Automation.Runspaces.CommandCollection
Type: System.Management.Automation.Runspaces.CommandCollection
Name MemberType ResultType IsStatic IsEnum
---- ---------- ---------- -------- ------
Add Method Void
AddScript Method Void
GetType Method Type
Count Property Int32
Item Property Command
I can add a single command like a cmdlet name or function or a script. If you want to do anything other than run a command like Get-Process
, I think you'll find it easier to define a script.
PS C:\> $sb = { Get-ChildItem c:\windows\system32\ntds.dit | Select fullName,length,LastWriteTime }
PS C:\> $pl.Commands.AddScript($sb)```
All that remains is to run the commands in the script block.
```text
PS C:\> $pl.Invoke() | tee -Variable r
FullName Length LastWriteTime
-------- ------ -------------
C:\windows\system32\ntds.dit 12582912 6/3/2024 6:05:40 PM
At this point the session is still open, but the pipeline object is finished. If I want to run another command, I need to create a new pipeline and command.
PS C:\> $pl = $ps.Runspace.CreatePipeline()
PS C:\> $sb = { Get-Service ADWS,DNS,KDC,CertSvc,NTDS}
PS C:\> $pl.Commands.AddScript($sb)
PS C:\> $pl.invoke()
Status Name DisplayName
------ ---- -----------
Running ADWS Active Directory Web Services
Running CertSvc Active Directory Certificate Services
Running DNS DNS Server
Running KDC Kerberos Key Distribution Center
Running NTDS Active Directory Domain Services
Helper Code
I walked through each step of the process , but it can run very quickly. I'll use a helper script.
#requires -version 7.4
#mkpssession.ps1
# You could modify to support credentials and/or SSL
Param([string]$ComputerName = $env:COMPUTERNAME)
$ci = [System.Management.Automation.Runspaces.WSManConnectionInfo]::new()
$ci.ComputerName = $ComputerName
$rs = [RunSpaceFactory]::CreateRunspace($ci)
$rs.ApartmentState = 'STA'
$rs.ThreadOptions = 'ReuseThread'
#script output
[System.Management.Automation.Runspaces.PSSession]::Create($rs, 'WSMan', $PSCmdlet)
These commands are prototyping a future function.
$ps = .\mkpssession.ps1 -ComputerName SRV1
$ps.Runspace.open()
$pl = $ps.Runspace.CreatePipeline()
$sb = {Get-WindowsFeature | Where Installed | Select Name,DisplayName,FeatureType,@{Name="ComputerName";Expression={$ENV:COMPUTERNAME}}}
$pl.commands.AddScript($sb)
$pl.Invoke()

One benefit that I find attractive with this approach is that if I were to use Invoke-Command
, I'd have to deal with the RunspaceID
property. I never need it so I always have to take extra steps to get rid of it in my output. With my native PSSession, there is no such property.
Clean Up
Remember, there is no session object created that you can see with Get-PSSession
. Because the session state is managed on the remote server, you need to clean things up on the client side.
$ps.Runspace.close()
If your forget to do this, the session process will eventually time out on the remote server. Or it will be removed when the remote server reboots.
And to be a good .NET scripter, it wouldn't hurt to properly dispose of the session.
$ps.Dispose()
Summary
If you feel a little overwhelmed, that is to be expected. I'm throwing a lot of native .NET code at you and I don't expect you to be a .NET developer. With time and practice though, this type of code won't feel so alien. A big challenge with this type of scripting is discover. Hopefully, I've given you some ideas on a methodology and tools you can use.
Try out the code samples in a non-production environment. Next time, I'll show you a proof-of-concept script and since we're in the days of PowerShell 7, I'll briefly demonstrate how to create an SSH-based PSSession from scratch.