Skip to content
Menu
The Lonely Administrator
  • PowerShell Tips & Tricks
  • Books & Training
  • Essential PowerShell Learning Resources
  • Privacy Policy
  • About Me
The Lonely Administrator

Answering the PowerShell Export Challenge

Posted on March 26, 2021March 26, 2021

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

Manage and Report Active Directory, Exchange and Microsoft 365 with
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!


Behind the PowerShell Pipeline

Share this:

  • Click to share on X (Opens in new window) X
  • Click to share on Facebook (Opens in new window) Facebook
  • Click to share on Mastodon (Opens in new window) Mastodon
  • Click to share on LinkedIn (Opens in new window) LinkedIn
  • Click to share on Pocket (Opens in new window) Pocket
  • Click to share on Reddit (Opens in new window) Reddit
  • Click to print (Opens in new window) Print
  • Click to email a link to a friend (Opens in new window) Email

Like this:

Like Loading...

Related

reports

Powered by Buttondown.

Join me on Mastodon

The PowerShell Practice Primer
Learn PowerShell in a Month of Lunches Fourth edition


Get More PowerShell Books

Other Online Content

github



PluralSightAuthor

Active Directory ADSI Automation Backup Books CIM CLI conferences console Friday Fun FridayFun Function functions Get-WMIObject GitHub hashtable HTML Hyper-V Iron Scripter ISE Measure-Object module modules MrRoboto new-object objects Out-Gridview Pipeline PowerShell PowerShell ISE Profile prompt Registry Regular Expressions remoting SAPIEN ScriptBlock Scripting Techmentor Training VBScript WMI WPF Write-Host xml

©2025 The Lonely Administrator | Powered by SuperbThemes!
%d