Last month, the Iron Scripter Chairman put out a rather large and complex challenge. The basic premise of the challenge was to export a PowerShell session to a file, and then import it in later PowerShell session. In essence, the save the working state of your PowerShell session. This would include items such as defined aliases, variables, functions, PSDrives, PSSessions, and more. Short of running in a virtual machine or maybe a container, where you could save the entire state, this is a tall order. But not insurmountable
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
In some instances, PowerShell has specific commands, like Export-Alias and Import-Alias. And as a last resort, there is always Export-CliXml. This is a preferred format because it will capture rich object data. I approached the problem by working on an export and import process for different elements. Here's a look at a few of them.
Master HashTable
My export process begins with a master hashtable. The hashtable starts with some metadata.
$master = [ordered]@{
Computername = [System.Environment]::MachineName
Username = [System.Environment]::UserName
Date = Get-Date
PSVersion = "{0}.{1}" -f $PSVersionTable.PSVersion.Major, $PSVersionTable.PSVersion.Minor
Host = $host.Name
Version = $host.version
}
I'll be adding to this hashtable and eventually exporting using Export-CliXML.
PrivateData
One item you might change and want to persist across sessions is $host.PrivateData. For example, I change the error color to Green to make it easier to read. To save this information, I created hashtable using the PSObject so that I could enumerate names and values.
$pdHash = @{}
$host.PrivateData.psobject.properties | ForEach-Object -Process { $pdhash.Add($_.name, $_.value) }
$master.Add("PrivateData", $pdHash)
To import, once I imported the cliXML file, $data is the saved PrivateData. Because it is a hashtable, it is easy to unroll and then set $host.privatedata settings.
$data.GetEnumerator() | ForEach-Object {
Write-Verbose "Setting private data value for $($_.name)"
if ($pscmdlet.ShouldProcess($_.Name, "Set value to $($_.value.value)")) {
$host.privatedata.$($_.name) = $_.value
}
}
Variables
Variables aren't difficult to export. But there was no reason to export default variables.
$exclude = "PWD", "PSCulture", "Profile", "`$", "?", "^", "false", "true", "host",
"nestedpromptlevel", "home", "MyInvocation", "Pid", "PSEdition", "PSHome", "PSVersionTable", "pwd",
"ShellID", "StackTrace", "NestedpromptLevel", "PSUiCulture", "ConsoleFileName", "Error", "ExecutionContext",
"OutputEncoding", "PSBoundParameters", "PSCmdlet", "Passthru", "PSScriptRoot"
$myVariables = Get-Variable -Scope global | Where-Object { $exclude -notcontains $_.name }
To restore the variables, I use Set-Variable and splat parameters based on the imported objects.
foreach ($var in $data) {
Write-Verbose "Adding $($var.name)"
$vHash = @{
Name = $var.name
Description = $var.Description
Visibility = $var.Visibility -as [System.Management.Automation.SessionStateEntryVisibility]
Option = $var.options -as [System.Management.Automation.ScopedItemOptions]
Value = $var.value
Force = $True
Scope = "Global"
ErrorAction = "Stop"
}
Try {
Set-Variable @vHash
}
Catch {
Write-Warning "Skipping variable $($vhash.name). $($_.Exception.message)"
}
} #foreach
PSSessions
Exporting PSSessions was slightly complicated. I knew that I'd be using New-PSSession to recreate the sessions. So I needed to export all of the information I would need for that step. I'm only exporting open sessions. I also needed to take PSSessionOptions into account.
$sessions = Get-PSSession | Where-Object { $_.state -eq "Opened" }
$all = foreach ($session in $sessions) {
$ci = $session.runspace.connectioninfo
$obj = [ordered]@{
PSTypeName = "ExportedPSSession"
Computername = $session.Computername
ComputerType = $session.Computertype
ConfigurationName = $session.ConfigurationName
Credential = $ci.credential
Username = $ci.Username
CertificateThumbprint = $ci.CertificateThumbprint
Port = $ci.Port
Transport = $session.transport
}
#add connection info
$ciProperties = @("MaximumReceivedDataSizePerCommand", "MaximumReceivedObjectSize", "NoMachineProfile", "ProxyAccessType", "ProxyAuthentication", "ProxyCredential", "SkipCACheck", "SkipCNCheck", "SkipRevocationCheck", "NoEncryption", "UseUTF16", "OutputBufferingMode", "IncludePortInSPN", "MaxConnectionRetryCount", "Culture", "UICulture", "OpenTimeout", "CancelTimeout", "OperationTimeout", "IdleTimeout")
$ciHash = @{}
foreach ($property in $ciProperties) {
#" $property = $($ci.$property)"
if ($null -ne $ci.$property) {
$ciHash.Add($property, $ci.$property)
}
else {
Write-Host "Skipping $property"
}
}
$obj.Add("ConnectionInfo", $ciHash)
New-Object -TypeName PSObject -Property $obj
}
$master.Add("PSSessions", $all)
This gives me a rich object for each PSSession. Here's how I rebuild it in a new sesssion.
Foreach ($session in $data) {
$params = @{ErrorAction = "Stop" }
Write-Verbose "Processing session for $($session.computername)"
if ($session.CertificateThumbprint) {
$params.Add("CertificateThumbprint".$session.CertificateThumbprint)
}
if ($session.configurationName -AND ($session.transport -ne 'SSH')) {
$params.Add("ConfigurationName", $session.ConfigurationName)
}
if ($session.credential) {
$params.add("Credential", $session.credential)
}
if ($session.port -AND ($session.port -notmatch "5985|80")) {
Write-Verbose "Using custom port $($session.port)"
$params.Add("Port", $session.port)
}
if ($session.transport -eq 'SSH') {
$params.Add("Hostname", $session.computername)
$params.Add("SSHTransport", $True)
if ($session.Username) {
$params.Add("UserName", $session.userName)
}
}
elseif ($session.Computertype.value -eq 'VirtualMachine') {
Write-Verbose "Connecting to virtual machine $($session.ComputerName)"
$params.Add("VMName", $session.computername)
}
elseif ($session.computertype.value -eq "RemoteMachine") {
Write-Verbose "Connecting to remote computer $($session.ComputerName)"
$params.Add("Computername", $session.computername)
$ci = $session.ConnectionInfo
$opt = New-PSSessionOption @ci
$params.Add("SessionOption", $opt)
}
else {
Write-Warning "Computertype $($session.Computertype.value) not handled at this time."
$skip = $True
}
if (-Not $skip) {
Try {
[void](New-PSSession @params)
}
Catch {
Write-Warning "Failed to recreate PSSession for $($session.Computername). $($_.Exception.Message)"
$params | Out-String | Write-Verbose
}
}
}
This should handle almost any type of PSSession.
CIMSessions
These type of remoting sessions, were a bit trickier because any credential information used isn't part of the object. So if exported, I need to prompt the user for credentials.
$cim = Get-CimSession | Where-Object { $_.TestConnection() } |
Select-Object Computername, Protocol,
@{Name = "Credential"; Expression = { Get-Credential -Message "Enter a credential for the $($_.computername.toUpper()) CIMSession if needed or click cancel" ; } }
$master.Add("CimSessions", $cim)
The only CIMSessionOption I'm accounting for is Protocol. Otherwise, recreating the CIMSession isn't that difficult.
$data | ForEach-Object {
$params = @{Computername = $_.computername }
if ($_.protocol -eq "DCOM") {
$params.add("SessionOption", (New-CimSessionOption -Protocol DCOM))
}
if ($_.credential.username) {
$params.add("Credential", $_.credential)
}
[void](New-CimSession @params)
} #foreach
PSSessionExport
I put all of the related commands into a module called PSSessionExport. I wrote a single command, Export-PSWorkingSession, that creates the master XML file using Export-CliXML.
I made exporting PSSessions and CimSessions optional. The command will work in both Windows PowerShell and PowerShell 7.x.
Because I included metadata, I wrote a simple command to get information about the export.
You can only import into a PowerShell session running the same major version. But the process is pretty quick. Start a new session, import the module, and import the saved PowerShell working session.
I included support for -WhatIf and plenty of Verbose output so you can see what is happening.
Want to try? Since this is a proof-of-concept, I don't plan in publishing this to the PowerShell Gallery. But you can get the code from the Github repository. I'm not really planning on maintaining this, at least not on a regular basis. However, I have enabled Discussions on the repository.
Enjoy!