Let's start with the design discussion first.

Problem Statement: I need to RDP onto multiple machines in multiple labs for multiple projects.  Each project has its own customizations to my $PROFILE. I need a way to keep all those files up-to-date and yet accessible.

Firstly, my desktop's $PROFILE isn't a good start - it has customizations for my dev environment, not my lab environments.  And, while I've built huge Rube Goldberg machines of Gold+Diff scripts to differentiate environments, I've found those to be quite brittle.  I'd rather take the hit of maintaining the four or five $PROFILEs for my lab environments separately.

While I can access local resources via the \\tsclient\c\users\timdunn share, that doesn't scale.  In one lab, we access multiple AD domains by the same hop-box, so I'm C:\users\timdunn.DOMAIN1, C:\users\timdunn.DOMAIN2, etc. based on which AD domain I'm using for that session.   That's not a concern importing my $PROFILE from my desktop to the hop-box, but it is a concern going from the hop-box to the various lab machines.  I don't want to have kludge-logic to try to handle these one-offs.

Oh, and there's our sustaining engineering lab, which is still running Server 2003R2 (for the next fifteen months and counting).  On that jump-box, my $HOME is D:\Documents and Settings\timdunn.SELAB.LOCAL  Especially given that we see the sunset from here, I don't want kludge-logic to handle that environment.  I want the same code to be able to handle all of these.

Subst.exe to the rescue.  What I'll do is normalize on a virtual drive to map to $HOME, no matter what its name.  That way, no matter which AD domain I'm using, that session will access \\tsclient\p to get to my desktop's $HOME, and it will provide the virtual drive P:\ to map to my jump-box session's $HOME.

===

Okay, now how about keeping the separate projects' files separate?  Instead of my usual $PROFILE path, I'm going to create a folder directly under my $HOME to store this:

$HOME 
  | 
  \- WindowsPowerShell\ 
  | 
  +- CorpNet\ 
  | +- Microsoft.PowerShell_profile.ps1 
  | +- Microsoft.PowerShell_profile.ps1.sha256 
  | +- ProjectFunctionLibrary.psm1 
  | \- ProjectFunctionLibrary.psm1.sha256 
  | 
  +- SELab\ 
  | 
  +- PerfLab\ 
  | 
  +- E2ELab\, etc. ... 
  | 
  +- CommonFunctionLibrary.psm1 
  \- CommonFunctionLibrary.psm1.sha256

This means all my files are in the same place ($HOME\WindowsPowerShell), but separated by projects, and I have one single file that has all the stuff that I want in all my environments, such as Set-ConsoleSize, all the SSL / certificate stuff, etc.

===

How about the setup?

This is hardcoded to use $HOME\WindowsPowerShell\$ProjectName (I’m using ‘CorpNet’, but you’ll likely use something else, hence the variable name.) so you’ll need to create those folders.

Save the script below as $HOME\WindowsPowerShell\$ProjectName\Microsoft.PowerShell_profile.ps1 and edit it to replace ‘CorpNet’ with your appropriate project name.  Configure and load the functions into the local PSH session with the following command:

. $PROFILE

This is going to bomb out because the .sha256 file is missing.  No big dea.  Let’s create it. Run the following:

Get-Sha256Hash “$HOME\WindowsPowerShell\$ProjectName\Microsoft.PowerShell_profile.ps1” > “$HOME\WindowsPowerShell\$ProjectName\Microsoft.PowerShell_profile.ps1 .sha256”

You’ll also need to create the following files and generate SHA hashes for them:

$null > “$HOME\WindowsPowerShell\CommonFunctionLibrary.psm1”

$null > “$HOME\WindowsPowerShell\$ProjectName\ProjectFunctionLibrary.psm1”

Now, everything should be working.  Let’s test it by again running:

. $PROFILE

If that works, then run:

Copy-SetProfileToClipboard

This will load the clipboard with the necessary code snippet.  (Paste it into a notepad.exe window if you want to double-check it.)  In the RDP session, open a PSH window and right-click it into there.

Note that the last line is commented out.  Up-arrow, Home, and uncomment it if you like how the rest of the snippet looks.  Press [Enter] and it will back up your existing $PROFILE to "$PROFILE (datestamp)", copy Microsoft.PowerShell_profile.ps1 from "\\tsclient\p\WindowsPowerShell\$ProjectName” (which is really just “$HOME\WindowsPowerShell\$ProjectName\Microsoft.PowerShell_profile.ps1” on your desktop RDPing to the session) to $PROFILE, then dot in $PROFILE.

===

Now, let's talk replication.  It's easy enough to compare datestamps and copy whichever is newer over the older, but I want it to go from one direction: from my desktop, out to my hop-box, and from there to the individual lab machines.

Now, we have a slightly prank-friendly culture in one of our labs, but I don't want anything there somehow making it back to the other labs.  A bit of EFS and a rudimentary checksum will dissuade most attempts.  If you need stronger security, there's always script-signing.

Note that EFS and roaming %USERPROFILE% folders do not play nicely.

===

Finally, there's the payload.  The code below contains nothing that you would usually find in a $PROFILE - no hacks to change your $Host.UI.RawUI.WindowTitle or redefine function:\Prompt, etc.  That's where the CommonFunctionLibrary.psm1 and ProjectFunctionLibrary.psm1 come in.

$PROFILE on all these boxes should not change once deployed.  Any changes to the two FunctionLibrary.psm1 files, such as snippets from this blog, will be replicated out automatically.  After each edit, make sure to re-generate the SHA hash, otherwise the system will flag it as a mismatch.

Just remember to . $PROFILE every so often to push the changes through if you keep your PSH windows open overnight.

===

Here's the code:

#region header
################################################################################

<#
.SYNOPSIS
TS-friendly startup file

.DESCRIPTION
A $PROFILE that is intended to work across RDP sessions using shared \\tsclient\ 

Gets around the fact that username may vary from session to session by mapping $HOME to virtual drive (default 'P') via subst.exe, then accessing \\tclient\P.

After loading functions,

1. Tests its own checksum ($myinvocation.scriptname).sha256

2. Calls subst.exe to alias drive P: to $home

3. Tries to access \\tsclient\p\windowspowershell\

4. Ensures Microsoft.PowerShell_profile.ps1 under \\tsclient\p\windowspowershell\ has correct hashfile

5. Copies it and its hash to $home\WindowsPowershell\ 

6. Enables EFS on the copied file (but not hashfile) if possible.

7. If $PROFILE was updated, call itself again.

8. Load any modules in $ProfileData.ModuleList array

.PARAMETER UpdateFileTimeout
If a call to the Update-File function exceeds this value, future calls to the same -Path drive will instantly return with a Warning about performance.

.PARAMETER Reloaded
$PROFILE has called itself - do not recurse further.

.PARAMETER NoSourceFolder
Sessions does not have access to \\tsclient\ either because it is a Console session, or the RDP session did not map drives.

.NOTES
Who     What        When        Why
timdunn v3.0        2014-02-01  Yet one more rewrite!
timdunn v3.0.01     2014-02-06  Add flag to enable loading $profile as module so functions show up with Module attribute.

#>

param (
    [int]$UpdateFileTimeout = 3,
    [switch]$Reloaded,
    [switch]$NoSourceFolder
);

#endregion
#region functions
################################################################################

function Test-FileIsEncrypted
{
    param (
        [string]$Path = $null
    );

    if ($Path -and (Test-Path -Path $Path))
    {
    
        [bool](([int]((Get-Item -Path $Path).Attributes.Value__ / 16384)) % 2)

    } # if ($Path -and (Test-Path -Path $Path))

} # function Test-FileIsEncrypted

function Read-HostWithTimeout
{
    <#
    .SYNOPSIS
    A half-baked Read-Host that takes a -Timeout parameter

    .DESCRIPTION
    An unholy union of $ost.UI.RawUI.ReadKey() and Read-Host.  It is usable, but that's good enough for my current needs.

    Here are the limitations I've hit so far:

    - Keyboard buffer doesn't.  If you type the first two characters within a second, the second one (or more) will be lost.

    - Keyboard buffer is not contiguious.  The first character is captured by ReadKey(), and the rest by Read-Host.  This means up-arrow will only show the characters captured by Read-Host.

    - This also means you can't back up over the first character.

    Still, all I need is a Y/N with a timeout, and that works.

    .PARAMETER Prompt
    Prompt for input.  Will have a ': ' appended, but no linefeed.

    .PARAMETER Timeout
    Seconds to wait for input.  If no input received, will return $null;

    .PARAMETER NoNewLine
    Return first key pressed, do not wait for user to press [Enter].

    .NOTES
    Who         What        When        Why
    timdunn     V0.1        2014-01-27  I'm not ready to even call this beta-quality.

    Why is this in $PROFILE?  It's needed for two scenarios

    - Delay in reloading $PROFILE to prevent a runaway loop (though the -Reloaded flag also address that.)

    - "Proceed or not" in case \\tsclient\c not found.

    #>

    param (
        [string]$Prompt = $null,
        [int]$Timeout = 10,
        [switch]$NoNewLine
    );

    if ($Timeout -le 0)
    {

        Write-Warning "$($MyInvocation.MyCommand.Name) -Timeout '$Timeout' less than or equal to 0.  Stopping.";
        return;

    } # if ($Timeout -le 0)

    $local:key = $null;

    if ($Prompt)
    {

        Write-Host -ForegroundColor Green -BackgroundColor Black "${Prompt}: " -NoNewline;

    } # if ($Prompt)

    $end = (Get-Date) + (New-TimeSpan -Seconds $Timeout);

    while (((Get-Date) -lt $end) -and !$local:key)
    {

        if ($host.ui.RawUI.KeyAvailable)
        {

            $local:key = $host.UI.RawUI.ReadKey("NoEcho, IncludeKeyUp").Character;
            break;

        } # if ($host.ui.RawUI.KeyAvailable)

        Start-Sleep -Milliseconds 20;

    } # while ((Get-Date) -lt $end)

    if ($local:key -eq $null)
    {

        return;

    } # if (!$local:key)

    if (!$NoNewLine)
    {

        Write-Host -NoNewline $local:key;
        $local:key += Read-Host;

    } # if (!$NoNewLine)

    $local:key;

} # function Read-HostWithTimeout


function Test-DateTimeWithinInterval
{
    <#
    .SYNOPSIS
    Test if two timestamps are within the specified interval

    .DESCRIPTION
    Because file LastWriteTime DateTime values are stored to the millisecond, a copy of a file may not have the same value as the original.  This allows for a "plus-or-minus" range, default of 5 seconds.

    .PARAMETER ReferenceObject
    First DateTime

    .PARAMETER DifferenceObject
    Second DateTime

    .PARAMETER Interval
    Timespan (measured in seconds) the two DateTime parameters must be within, absolute value, to return true.

    .NOTES
    Why is this in $PROFILE?  Because the file sync function needs it.
    #>

    param (
        [DateTime]$ReferenceObject,
        [DateTime]$DifferenceObject,
        [Int]$Interval = 5
    );

    [Math]::Abs(($ReferenceObject - $DifferenceObject).TotalSeconds) -le $Interval;

} # function Test-DateTimeWithinInterval


function Get-Sha256Hash
{
    <#
    .SYNOPSIS
    Get SHA256 checksum

    .DESCRIPTION
    Get SHA256 checksum.  Yes, PowerShell3 has Get-FileHash, but I have some PSH V2 environments that don't benefit from that yummy goodness.

    .PARAMETER Path
    File for which to generate checksum

    .NOTES
    Who         What        When        Why
    timdunn     V1.0        2014-01-27  Initial creation.

    Why is it in this  $PROFILE?  Because Ithe file sync function needs it.
    #>

    param
    (
        [string]$Path = $null
    );

    if (!$Path)
    {

        Write-Warning "$($MyInvocation.MyCommand.Name) -Path not specified.  Stopping.";
        return;

    } # if (!$Path)
    elseif (Test-Path $Path)
    {
    
        if ((Get-Item $Path).PsIsContainer)
        {

            Write-Warning "$($MyInvocation.MyCommand.Name) -Path '$Path' is a folder.  Stopping.";
            return;

        } # if ((Get-Item $Path).PsIsContainer)

    } # if (Test-Path $Path)
    else
    {
    
        Write-Warning "$($MyInvocation.MyCommand.Name) -Path '$Path' not found.  Stopping.";
        return;

    } # if (Test-Path $Path)

    $Path = (Resolve-Path -Path $Path).ProviderPath;

    if (!($hasher = [System.Security.Cryptography.sha256]::Create()))
    {
    
        Write-Warning "$($myinvocation.mycommand.name) Unable to create SHA hasher.";
        return;

    } # if (!($hasher = [System.Security.Cryptography.sha256]::Create()))

    if (!($inputstream = New-Object System.IO.StreamReader $Path))
    {
        Write-Warning "$($myinvocation.mycommand.name) -Path '$Path' Unable to open stream.";
        return;

    } # if (!($inputstream = New-Object System.IO.StreamReader $Path))

    if (!($hashbytes = $hasher.ComputeHash($inputstream.BaseStream)))
    {
    
        Write-Warning "$($myinvocation.mycommand.name) Failed to compute hash.";
        return;

    } # if (!($hashbytes = $hasher.ComputeHash($inputstream.BaseStream)))

    $inputstream.Close();

	if (!($builder = New-Object system.text.stringbuilder))
    {
    
        Write-Warning "$($myinvocation.mycommand.name) Unable to create StringBuilder.";
        return;

    } # if (!($builder = New-Object system.text.stringbuilder))

	$hashbytes | % { [void] $builder.Append($_.ToString("X2")); }
    [string]$builder.ToString();

} # function Get-Sha256Hash


function Test-FileSha256Checksum
{
    <#
    .SYNOPSIS
    Test if hash of specified file matches contents of specified hash file.
    
    .DESCRIPTION
    Take SHA256 hash of -Path argument value, compare it to the contents of -Sha256Path parameter argument value.
    
    .PARAMETER Path
    Path of file from which to obtain checksum.  Function returns $false if not specified.
    
    .PARAMETER Sha256Path
    Path of file containing expected checksum.  Defaults to "<-Path parameter argument value>.sha256".

    .PARAMETER Verbose
    Output file data
    
    .OUTPUT
    [bool].  
    
    $True if all files exists, and hash of -Path parameter matches contents of -Sha256Path parameter argument value.
    
    $False otherwise.
    
    .NOTES
    Who     What        When        Why
    timdunn v1.0        2014-02-01  Refactor this check into its own function.

    #>
    
    param (
        [string]$Path = $null,
        [string]$Sha256Path = $null,
        [string]$Verbose
    );
    
    if (!$Path)
    {

        Write-Warning "$($MyInvocation.MyCommand.Name) -Path not specified.";
        return $false;
    
    } # if (!$Path)
    
    if (!(Test-Path -Path $Path))
    {
    
        Write-Warning "$($MyInvocation.MyCommand.Name) -Path '$Path' not found.";
        return $false;
    
    } # if (!$Path)
    
    if (!$Sha256Path)
    {
    
        $Sha256Path = "$Path.sha256";
    
    } # if (!$Sha256Path)
    
    if (!(Test-Path -Path $Sha256Path))
    {
    
        Write-Warning "$($MyInvocation.MyCommand.Name) -Sha256Path '$Sha256Path' not found.";
        return $false;
    
    } # if (!$Path)
    
    $local:PathChecksum = Get-Sha256Hash -Path $Path;
    $local:GcSha256Path = Get-Content -Path $Sha256Path;
    
    if ($local:PathChecksum -eq $local:GcSha256Path)
    {
    
        if ($Verbose)
        {
    
            Write-Host -ForegroundColor Cyan -BackgroundColor Black "INFO: $($MyInvocation.MyCommand.Name) Passed:";
            Write-Host -ForegroundColor Green -BackgroundColor Black ("    Get-Sha256Hash -Path '$Path' (LastWriteTime {0}):" -f (Get-Item $Path).LastWriteTime.ToString('yyyy-MM-dd HHmmss'));
            Write-Host -ForegroundColor Green -BackgroundColor Black  "        $local:PathChecksum";
            Write-Host -ForegroundColor Green -BackgroundColor Black ("    Get-Content -Path '$Sha256Path' (LastWriteTime {0}):" -f (Get-Item $Path).LastWriteTime.ToString('yyyy-MM-dd HHmmss'));
            Write-Host -ForegroundColor Green -BackgroundColor Black  "        $local:GcSha256Path";
            Write-Host $null;
    
        } # if ($Verbose)

        return $true;
    
    } # if ($local:PathChecksum -eq $local:GcSha256Path)
    else
    {
    
        Write-Warning "$($MyInvocation.MyCommand.Name) failed:";
        Write-Warning ("    Get-Sha256Hash -Path '$Path' (LastWriteTime {0}):" -f (Get-Item $Path).LastWriteTime.ToString('yyyy-MM-dd HHmmss'));
        Write-Warning "        $local:PathChecksum";
        Write-Warning ("    Get-Content -Path '$Sha256Path' (LastWriteTime {0}):" -f (Get-Item $Sha256Path).LastWriteTime.ToString('yyyy-MM-dd HHmmss'));
        Write-Warning "        $local:GcSha256Path";
        Write-Host $null;
    
        return $false;
    
    } # if ($local:PathChecksum -eq $local:GcSha256Path) ... else
    
    # we should not be here.  But, just in case...
    
    return $false;

} # function Test-FileSha256Checksum


function Update-File
{
    <#
    .SYNOPSIS
    Copies a file from one location to the other if the source file is newer.
    
    .DESCRIPTION
    Copies a file from one location to the other if the source file is newer and checksum matches.  Checksumming can be bypassed with the -NoChecksum switch.
    
    If destination folder is not a //unc/shared/path, destination file is encrypted with cipher.exe to an Encrypted File System (EFS) file.  Encryption can be bypassed with the -NoEFS switch.  
    
    .OUTPUT
    [int]
    
    > 0  - File copy (and encryption if applicable) successful.
    
    = 0 - File dates unchanged.
    
    < 0 - File copy (or encryption if applicable) failed.
    
    .PARAMETER Path
    Source file to copy if newer.  
    
    .PARAMETER Destination
    Destination to which to copy file.

    .PARAMETER Timeout
    Value at which it flags future calls to pass w/o copying due to performance issues.  Defaults to 10 seconds.  It records the most recent performance for that particular source drive in the $global:ProfileData.UpdatefileTime hash, keyd by the source drive.

    .PARAMETER NoChecksum
    Checksum file defaults to "<-Path parameter argument value>.sha256".  If -NoChecksum specified, do not calculate source file checksum prior to copying, and do not copy checksum file.
    
    .PARAMETER NoEFS
    Do not encrypt destination file with cipher.exe.
    
    .NOTES
    Who     What        When        Why
    timdunn v1.0        2014-02-01  Refactor this function so it better handles EFS and SHA.

    #>
    
    param (
        [string]$Path = $null,
        [string]$Destination = $null,
        [int]$Timeout = 10,
        [switch]$NoChecksum,
        [switch]$NoEFS
    );

    # this can really lag.  Get ready to bail out if we run over time.
    $startTime = Get-Date;
    $notAfter = $startTime + (New-TimeSpan -Seconds $Timeout);

    #region Validate arguments
    
    if (!$Path) 
    { # make sure -Path is specified

        Write-Warning "$($MyInvocation.MyCommand.Name) -Path not specified.";
        return -1;

    } # if (!$Path) 

    #region make sure we didn't flag this source drive for performance issues

    if (!$global:ProfileData.UpdateFileTime -or ($global:ProfileData.UpdateFileTime.GetType().ToString() -ne 'System.Collections.Hashtable'))
    {

        $global:ProfileData.UpdateFileTime = @{};
    
    } # if (!$global:ProfileData.UpdateFileTime -or ($global:ProfileData.UpdateFileTime.GetType().ToString() -ne 'System.Collections.Hashtable'))

    if (!$global:ProfileData.SourceDriveFlagged -or ($global:ProfileData.SourceDriveFlagged.GetType().ToString() -ne 'System.Collections.Hashtable'))
    {

        $global:ProfileData.SourceDriveFlagged= @{};
    
    } # if (!$global:ProfileData.UpdateFileTime -or ($global:ProfileData.UpdateFileTime.GetType().ToString() -ne 'System.Collections.Hashtable'))

    $sourceDrive = $Path -replace ":\\.*", ":\" -replace "^(\\\\[^\\]*\\[^\\]*\\).*", '$1';

    Write-Verbose "`$Timeout = '$Timeout'";
    Write-Verbose "`$sourceDrive = '$sourceDrive'";
    Write-Verbose "`$global:ProfileData.UpdateFileTime.'$sourceDrive' = '$([int]$global:ProfileData.UpdateFileTime.$sourceDrive)'";
    Write-Verbose "`$Path = '$Path'";
    Write-Verbose "`$Path encrypted = `$$(Test-FileIsEncrypted -Path $Path)";
    Write-Verbose "`$Destination = '$Destination'";
    if (Test-Path $Destination)
    {
        Write-Verbose "`$Destination encrypted = `$$(Test-FileIsEncrypted -Path $Destination)";
    } # if (Test-Path $Destination)
    else
    {
        Write-Verbose "`$Destination does not exist.";
    } # if (Test-Path $Destination)

    $local:sourceLastWriteTime = (Get-Item -Path $Path).LastWriteTime;
    
    if ($global:ProfileData.UpdateFileTime.$sourceDrive -and ($global:ProfileData.UpdateFileTime.$sourceDrive -gt $Timeout))
    {

        Write-Warning "$($MyInvocation.MyCommand.Name) -Path '$Path' skipped."

        if (!$global:ProfileData.SourceDriveFlagged.$sourceDrive)
        {
        
            $global:ProfileData.SourceDriveFlagged.$sourceDrive = $true;

            Write-Warning "    Previous call against drive $sourceDrive exceeded timeout of $Timeout seconds."
            Write-Warning "    Not processing any more calls against drive $sourceDrive.";
            Write-Warning "    Increase the timeout when calling";
            Write-Warning "    . `$PROFILE -UpdateFileTimeout ";
        
        } # if (!$global:ProfileData.SourceDriveFlagged.$sourceDrive)
        
        Write-Host $null;
        return 0;
        
    } # if ($global:ProfileData.UpdateFileTime.SourceDrive  -and ($global:ProfileData.UpdateFileTime.SourceDrive -gt $Timeout))

    #endregion

    if (!(Test-Path -Path $Path))
    {

        Write-Warning "$($MyInvocation.MyCommand.Name) -Path '$Path' not found.";
        return -2;

    } # if (!(Test-Path -Path $Path))
    
    if ((Get-Date) -gt $notAfter)
    {

        $global:ProfileData.UpdateFileTime.SourceDrive = $Timeout + 1;
        $global:ProfileData.SourceDriveFlagged."Test-Path $Path" = $true;
        Write-Warning "$($MyInvocation.MyCommand.Name) -Path '$Path' timed out after $Timeout seconds.  Stopping.";
        return -10;

    } # if ((Get-Date) -gt $notAfter)

    if ((Get-Item -Path $Path).PsIsContainer)
    { # -Path must not be a folder

        Write-Warning "$($MyInvocation.MyCommand.Name) -Path '$Path' must be a file.";
        return 0;

    } # if ((Get-Item -Path $Path).PsIsContainer)
    
        if ((Get-Date) -gt $notAfter)
    {

        $global:ProfileData.UpdateFileTime.SourceDrive = $Timeout + 1;
        $global:ProfileData.SourceDriveFlagged."(Get-Item -Path $Path0.PsIsContainer" = $true;
        Write-Warning "$($MyInvocation.MyCommand.Name) -Path '$Path' timed out after $Timeout seconds.  Stopping.";
        return 0;

    } # if ((Get-Date) -gt $notAfter)

    if (!$NoChecksum -and !(Test-FileSha256CheckSum -Path $Path))
    { # unless -NoChecksum specified, ensure source file checksum exists and is correct.

        return -4;
    
    } # if (!$NoChecksum -and !(Test-FileSha256CheckSum -Path $Path))
    
    if ((Get-Date) -gt $notAfter)
    {

        $global:ProfileData.UpdateFileTime.SourceDrive = $Timeout + 1;
        $global:ProfileData.SourceDriveFlagged."!(Test-FileSha256CheckSum -Path $Path)" = $true;
        Write-Warning "$($MyInvocation.MyCommand.Name) -Path '$Path' timed out after $Timeout seconds.  Stopping.";
        return 0;

    } # if ((Get-Date) -gt $notAfter)

    if (!$Destination)
    { # make sure -Destination is specified
    
        Write-Warning "$($MyInvocation.MyCommand.Name) -Destination not specified.";
        return -5;
    
    } # if (!$Destination)
    
    if (!(Test-path $Destination))
    { # -Destination does not exist
    
        md $Destination | Out-Null;
        rm $Destination;
    
    } # if (!(Test-path $Destination))

    if ((Split-Path -Leaf -Path $Path) -ne (Split-Path -Leaf -Path $Destination))
    { # make sure -Path and -Destination files have the same name.

        $Destination = Join-Path -Path $Destination -ChildPath (Split-Path -Path $Path -Leaf);

    } # if ((Split-Path -Leaf -Path $Path) -ne (Split-Path -Leaf -Path $Destination))
    
    if ($Destination -match '^\\\\')
    { # encryption doesn't like \\unc\share\paths

        $NoEFS = $true;
        
        if (Test-FileIsEncrypted -Path $Path)
        { # 15th bit is 'Encrypted' of the FileAttributes enum.  2**15 = 16384

            try
            {

                (Get-Item -Path $Path).Decrypt();

            } # try
            catch
            {

                Write-Warning "$($MyInvocation.MyCommand.Name) attempt to decrypt -Destination '$Path' returns:";
                Write-Warning "    $($_.Exception.Message -replace '\n' -replace '\r')";

            } # catch
            finally
            {

                Write-Verbose "Decryption of '$Path' successful.";

            } # finally
            
        } # if (Test-FileIsEncrypted -Path $Path)
        else
        {

            Write-Verbose "Decryption of '$Path' not needed.  File is already unencrypted.";

        } # if (Test-FileIsEncrypted -Path $Path)
    } # if ($Destination -match '^\\\\')
    else
    {

        Write-Verbose "Decryption of '$Path' not needed.";

    } # if ($Destination -match '^\\\\') ... else
    
    if ((Get-Date) -gt $notAfter)
    {

        $global:ProfileData.UpdateFileTime.SourceDrive = $Timeout + 1;
        $global:ProfileData.SourceDriveFlagged."Decrypt `$Path test" = $true;
        Write-Warning "$($MyInvocation.MyCommand.Name) -Path '$Path' timed out after $Timeout seconds.  Stopping.";
        return 0;

    } # if ((Get-Date) -gt $notAfter)

    #endregion
    #region test if we DON'T need to copy files
    
    if (Test-Path -Path $Destination)
    {
        $local:destinationLastWriteTime = (Get-Item -Path $Destination).LastWriteTime;
        
        if (Test-DateTimeWithinInterval -ReferenceObject $local:sourceLastWriteTime -DifferenceObject $local:destinationLastWriteTime -Interval 5)
        {

            Write-Host -ForegroundColor Cyan -BackgroundColor Black "INFO: $($MyInvocation.MyCommand.Name) no copy needed."
            Write-Host -ForegroundColor Green -BackgroundColor Black "    -Path '$Path'";
            Write-Host -ForegroundColor Green -BackgroundColor Black "        LastWriteTime: $($local:sourceLastWriteTime.ToString(''))";
            Write-Host -ForegroundColor Green -BackgroundColor Black "    -Destination '$Destination'";
            Write-Host -ForegroundColor Green -BackgroundColor Black "        LastWriteTime: $($local:destinationLastWriteTime.ToString(''))";
            Write-Host $null;

            $global:ProfileData.UpdateFileTime.$sourceDrive = [int]((Get-Date) - $startTime).TotalSeconds;
            return 0;

        } # if (Test-DateTimeWithinInterval -ReferenceObject $local:sourceLastWriteTime -DifferenceObject $local:destinationLastWriteTime -Interval 5)
        elseif ($local:sourceLastWriteTime -lt $local:destinationLastWriteTime)
        { # destination should never be newer than source.

            Write-Warning "$($MyInvocation.MyCommand.Name) destination file newer than source file:";
            Write-Warning "    -Path '$Path'";
            Write-Warning "        LastWriteTime: $($local:sourceLastWriteTime.ToString(''))";
            Write-Warning "    -Destination '$Destination'";
            Write-Warning "        LastWriteTime: $($local:destinationLastWriteTime.ToString(''))";
        
            $global:ProfileData.UpdateFileTime.$sourceDrive = [int]((Get-Date) - $startTime).TotalSeconds;
            if ((Read-HostWithTimeout -Prompt "Press 'y' within 5 seconds to continue" -NoNewLine -Timeout 5) -eq 'y')
            {
        
                Write-Host $null;
                return 0;
        
            } # if ((Read-HostWithTimeout -Prompt "Press 'y' within 5 seconds to continue" -NoNewLine -Timeout 5) -eq 'y')
            else
            {
        
                Write-Host $null;
                return -7;
        
            } # if ((Read-HostWithTimeout -Prompt "Press 'y' within 5 seconds to continue" -NoNewLine -Timeout 5) -eq 'y') ... else
        
        } # elseif ($local:sourceLastWriteTime -lt $local:destinationLastWriteTime)
    } # if (Test-Path -Path $Destination)
    
    if ((Get-Date) -gt $notAfter)
    {

        $global:ProfileData.UpdateFileTime.SourceDrive = $Timeout + 1;
        $global:ProfileData.SourceDriveFlagged."Timestamp test" = $true;
        Write-Warning "$($MyInvocation.MyCommand.Name) -Path '$Path' timed out after $Timeout seconds.  Stopping.";
        return 0;

    } # if ((Get-Date) -gt $notAfter)

    #endregion
    #region Copy file(s)
    
    if (!(Test-Path -Path (Split-Path -Path $Destination -Parent)))
    { # create parent folder if needed
        
        New-Item -ItemType Directory -Path (Split-Path -Path $Destination -Parent) | Out-Null;

    } # if (!(Test-Path -Path (Split-Path -Path $Destination -Parent)))    

    Write-Host -ForegroundColor Cyan -BackgroundColor Black "INFO: $($MyInvocation.MyCommand.Name) copying."
    Write-Host -ForegroundColor Green -BackgroundColor Black "    -Path '$Path'";
    Write-Host -ForegroundColor Green -BackgroundColor Black "        LastWriteTime: $($local:sourceLastWriteTime.ToString(''))";
    Write-Host -ForegroundColor Green -BackgroundColor Black "    -Destination '$Destination'";

    if ($local:destinationLastWriteTime)
    {

        Write-Host -ForegroundColor Green -BackgroundColor Black "        LastWriteTime: $($local:destinationLastWriteTime.ToString(''))";

    } # if ($local:destinationLastWriteTime)
    else
    {

        Write-Host -ForegroundColor Green -BackgroundColor Black "        LastWriteTime: N/A";

    } # if ($local:destinationLastWriteTime)... else

    Write-Host $null;

    if (Test-Path -Path $Destination)
    { # create backup file

        Copy-Item -Path $Destination -Destination "$Destination ($(Get-Date -f 'yyyy-MM-dd HHmmss'))";

    } # if (Test-Path -Path $Destination)

    Copy-Item -Path $Path -Destination $Destination -Force;
    (Get-Item $Destination).IsReadOnly = $false;
    
    if (Test-Path -Path $Path)
    { # nuke the remote bit
    
        try
        {

            cmd.exe /c "type NUL > `"$Path`":Zone:Identifier" 2>&1 | Out-Null;
        
        } # try
        
        catch
        {

            Write-Warning $_.Exception.Message;

        } # catch
    
    } # if (Test-Path -Path $Path)
    else
    {

        Write-Warning "$($MyInvocation.MyCommand.Name) copy failed.  -Destination '$Destination' not found.";
        $global:ProfileData.UpdateFileTime.$sourceDrive = [int]((Get-Date) - $startTime).TotalSeconds;
        return -10;

    } # if (Test-Path -Path $Path) ... else
    
    if (!$NoEFS)
    { # if -NoEFS switch was not specified, encrypt $Destination

        if (!(Test-FileIsEncrypted -Path $Destination))
        { # 15th bit is 'Encrypted' of the FileAttributes enum.  2**15 = 16384

            try
            {

                (Get-Item -Path $Destination).Encrypt();

            } # try
            catch
            {

                Write-Warning "$($MyInvocation.MyCommand.Name) attempt to encrypt -Destination '$Destination' returns:";
                Write-Warning "    $($_.Exception.Message -replace '\n' -replace '\r')";

            } # catch
            finally
            {

                Write-Verbose "Encryption of '$Destination' successful.";

            } # finally

        } # if (!(Test-FileIsEncrypted -Path $Destination))
        else
        {

            Write-Verbose "Encryption of '$Destination' not needed.  File is already encrypted.";

        } # if (!(Test-FileIsEncrypted -Path $Destination))

    } # if (!$NoEFS)
    else
    {

        Write-Verbose "Encryption of '$Destination' skipped because -NoEFS flag set.";
    
    } # if (!$NoEFS)
    
    if (!$NoChecksum)
    {

        Copy-Item -Path "$Path.sha256" -Destination "$Destination.sha256";

        if (!(Test-FileSha256Checksum -Path $Destination))
        {

            if ((Read-HostWithTimeout -Prompt "Press 'y' within 5 seconds to continue" -NoNewLine -Timeout 5) -ne 'y')
            {

                Write-Host $null;
                $global:ProfileData.UpdateFileTime.$sourceDrive = [int]((Get-Date) - $startTime).TotalSeconds;
                return -7;

            } # if ((Read-HostWithTimeout -Prompt "Press 'y' within 5 seconds to continue" -NoNewLine -Timeout 5) -ne 'y')

        } # if (!(Test-FileSha256Checksum -Path $Destination))

    } # if (!$NoChecksum)

    $global:ProfileData.UpdateFileTime.$sourceDrive = [int]((Get-Date) - $startTime).TotalSeconds;
    return 1;
    
    #endregion
} # function Update-File


function Copy-SetProfileToClipboard
{
    <#
    .SYNOPSIS
    Loads clipboard with here-text to set up profile system on an RDP session.

    .DESCRIPTION
    Loads clipboard with here-text to set up profile system on an RDP session.

    .PARAMETER ProjectName
    Name of project on the SOURCE side, such as \\tsclient\p\WindowsPowerShell\Project-2.  Defaults to current project.

    .PARAMETER Force
    Do not comment out the line that calls the Set-Profile function.  This means a right-click paste into the PowerShell.exe window will run the command.

    .NOTES
    #>

    param
    (
        [string]$ProjectName = $global:ProfileData.ProjectName,
        [switch]$Force
    );

    if (!($local:sourceFolder = $global:ProfileData.SourceFolder))
    {

        $local:sourceFolder = $global:ProfileData.oldSourceFolder;
    
    } # if (!($local:sourceFolder = $global:ProfileData.SourceFolder))

    # substitute project name
    $local:SourceFolder = $local:sourceFolder -replace "$($global:ProfileData.ProjectName)$", $ProjectName;

    # huge block of here-text to go into clip.exe
    $local:data = @"
        function Set-Profile
        {

            if (!(Test-Path -Path `$PROFILE))
            { # make sure path\to\profile\folder exists
        
                md `$PROFILE | Out-Null;
                rm `$PROFILE;
        
            } # if (!(Test-Path -Path `$PROFILE))

            `$profileSrc = "$($local:sourceFolder)\Microsoft.PowerShell_profile.ps1";

            if (!(Test-Path -Path `$profileSrc))
            {
        
                Write-Warning "`$(`$MyInvocation.MyCommand.Name) Unable to find `$profileSrc.  Stopping."
                return `$false;
        
            } # if (!(Test-Path -Path `$profileSrc))

            if (Test-Path `$PROFILE)
            {

                `$backupPath = "`$PROFILE (`$((Get-Item -Path `$PROFILE).LastWriteTime.ToString('yyyy-MM-dd hhmms')))";

                if (!(Test-Path -Path `$backupPath))
                { # back up `$PROFILE
        
                    Copy-Item `$PROFILE `$backupPath;
        
                } # if (!(Test-Path -Path `$backupPath))

            } # if (Test-Path `$PROFILE)

            if (!(Test-Path -Path `$PROFILE) -or !`$backupPath -or (Test-Path -Path `$backupPath))
            { # if backup successful
        
                if (`$backupPath -and (Test-Path -Path `$backupPath))
                {
                    
                    Write-Host -ForegroundColor Green -BackgroundColor Black "`$backupPath created";
                
                } # if (`$backupPat -and (Test-Path -Path `$backupPath))

                Copy-Item `$profileSrc `$PROFILE;
                Copy-Item "$($local:sourceFolder)\Microsoft.PowerShell_profile.ps1.sha256" "`$PROFILE.sha256";
                Write-Host -ForegroundColor Green -BackgroundColor Black "`$profileSrc copied to `$profile";

                return `$true
        
            } # if (Test-Path -Path `$backupPath)
            else
            {
        
                Write-Warning "`$(`$MyInvocation.MyCommand.Name) Unable to back up ```$PROFILE to `$backupPath.  Stopping.";
                return `$false;
        
            } # if (Test-Path -Path `$backupPath) ... else

        } # function Set-Profile
"@;

    if (!$Force)
    {
        $lastLine = '# ';
    
    } # if (!$Force)

    $local:data += "`r`n`r`n$lastLine if (Set-Profile) { . `$PROFILE -UpdateFileTimeout 60; }`r`n;";
    $local:data | clip.exe;
    Write-Host -ForegroundColor Green -BackgroundColor Black "Set-Profile function copied to clipboard";

} # function Copy-SetProfileToClipboard


function Update-ProfileSystem
{

    . $profile -UpdateFileTimeout 90;

} # function Update-ProfileSystem

#endregion
#region bail out if we're being imported as a module
################################################################################

if ($global:profilePsm1)
{

    return;

} # if ($global:profilePsm1)

#endregion

#region set up single global-scope hashtable to store everything
################################################################################

if (!$global:LoadProfileStartTime)
{

    $global:LoadProfileStartTime = Get-Date;

} # if (!$global:LoadProfileStartTime)

if (!(Get-history).count -and !$global:ProfileData)
{ # if this is being run as part of a new PowerShell window, print some blank lines so the Write-Progess doesn't cover stuff up.

    Write-Host $null;
    Write-Host $null;
    Write-Host $null;

} # if (!(Get-history).count)

$global:ProfileData = @{};
$global:ProfileData.ProjectName = 'CorpNet';
$global:ProfileData.ModuleList = @();

#endregion
#region validate checksum
################################################################################

$local:ScriptName = (&{$MyInvocation}).Scriptname;

Write-Progress $local:ScriptName "Validate checksum";

if (!(Test-FileSha256Checksum -Path $local:ScriptName))
{ # Test-FileSha256Checksum will output a warning if it fails.

    return;    

} # if (!(Test-FileSha256Checksum -Path $local:ScriptName))

#endregion
#region set up virtual drive for RDP sessions started from this machine
################################################################################

Write-Progress $local:ScriptName "Set up Subst.exe'd drive"

# for mounting across RDP sessions and different usernames, define a virtual drive, e.g.: P:,
# to $home\WindowsPowerShell\ProjectName.  after that, use \\tsclient\p\ to refer to $home on the source machine
$global:ProfileData.SubstDrive = 'P';
$global:ProfileData.PhysicalSourceFolder = "WindowsPowerShell\$($global:ProfileData.ProjectName)"; # relative to $HOME

if ($local:substOutput = (& subst.exe | ? { $_ -match "^$($global:ProfileData.SubstDrive):" } ))
{ # test if virtual drive is already in use

    $local:substOutput = $local:substOutput.Split(" ", 3)[2];

    if ($local:substOutput -ne "$home")
    { # make sure it is pointing to the right place

        Write-Warning "`$ProfileData.SubstDrive '$($global:ProfileData.SubstDrive)' already assigned to '$local:substOutput'.";
        return;

    } # if ($local:substOutput -ne "$home\$()")

} # if ($local:substOutput = ((& subst.exe) -match "^$($global:ProfileData.SubstDrive):") -replace ".*\s")
else
{

    & subst.exe "$($global:ProfileData.SubstDrive):" "$home";

} # if ($local:substOutput = ((& subst.exe) -match "^$($global:ProfileData.SubstDrive):") -replace ".*\s")


#endregion
#region try to access \\tsclient\ if this is an RDP session
################################################################################

$global:ProfileData.SourceFolder = "\\tsclient\$($global:ProfileData.SubstDrive)\$($global:ProfileData.PhysicalSourceFolder)";

Write-Progress $local:ScriptName "Test access to $($global:ProfileData.SourceFolder)";

$Error.Clear();

if (!$NoSourceFolder)
{ # if we are on Console, assume no \\tsclient\p drive.  yes, we can mstsc /v foo /console or /admin, but...

    $NoSourceFolder = $env:SESSIONNAME -eq 'Console';

} # if (!$NoSourceFolder)

if (!$global:ProfileData.UpdateFileTime -or ($global:ProfileData.UpdateFileTime.GetType().ToString() -ne 'System.Collections.Hashtable'))
{

    $global:ProfileData.UpdateFileTime = @{};
    
} # if (!$global:ProfileData.UpdateFileTime -or ($global:ProfileData.UpdateFileTime.GetType().ToString() -ne 'System.Collections.Hashtable'))

$startTime = Get-Date;

if ($NoSourceFolder -or !(Test-Path -Path $global:ProfileData.SourceFolder -ErrorAction SilentlyContinue))
{

    if ($error.Count)
    {

        Write-Warning "$($MyInvocation.MyCommand.Name) Test-Path -Path $($global:ProfileData.SourceFolder) results in '$($Error[0].Exception.message)'.";

    } # if ($error.Count)

    $global:ProfileData.oldSourceFolder = $global:ProfileData.SourceFolder;
    $global:ProfileData.SourceFolder = $null;

    if (!$NoSourceFolder)
    {

        Write-Warning "$($MyInvocation.MyCommand.Name) unable to open $($global:ProfileData.oldSourceFolder).";
        if (!(Read-HostWithTimeout -Prompt "Press Ctrl+C within 5 seconds to stop processing" -NoNewLine -Timeout 5))
        {
        
            Write-Host $null;
        
        } # if (!(Read-HostWithTimeout -Prompt "Press Ctrl+C within 5 seconds to stop processing" -NoNewLine -Timeout 5))

    } # if (!$NoSourceFolder)

} # if (!$NoSourceFolder -and !(Test-Path -Path $global:ProfileData.SourceFolder -ErrorAction SilentlyContinue))

$global:ProfileData.UpdateFileTime."\\tsclient\$($global:ProfileData.SubstDrive)\" = [int]((Get-Date) - $startTime).TotalSeconds;

#endregion
#region update key modules from \\tsclient\ to $home\windowspowershell\
################################################################################

# files are copied from $ProfileData.SourceFolder (e.g. \\tsclient\p\WindowsPowerShell\Project-A)
# to $ProfileData.LocalFolder (e.g. c:\user\timdunn\WindowsPowerShell\Project-A)
# $PROFILE is then copied from $ProfileData.LocalFolder\Microsoft.Powershell_Profile.ps1 to $PROFILE

Write-Progress $local:ScriptName "Update `$Profile, common and project function libraries.";

$global:ProfileData.LocalFolder = "$home\$($global:ProfileData.PhysicalSourceFolder)";

if ($global:ProfileData.SourceFolder)
{ # Update three key files from \\tsclient\p\WindowsPowerShell: 
    # $home\WindowsPowerShell\CommonFunctionLibrary.psm1
    # $home\WindowsPowerShell\ProjectName\Microsoft.PowerShell_profile.ps1
    # $home\WindowsPowerShell\ProjectName\ProjectFunctionLibrary.psm1

    $null = Update-File -Timeout $UpdateFileTimeout -Path "$($global:ProfileData.SourceFolder -replace '\\[^\\]*$')\CommonFunctionLibrary.psm1" -Destination "$($global:ProfileData.LocalFolder -replace '\\[^\\]*$')\CommonFunctionLibrary.psm1";
    $null = Update-File -Timeout $UpdateFileTimeout -Path "$($global:ProfileData.SourceFolder)\ProjectFunctionLibrary.psm1" -Destination "$($global:ProfileData.LocalFolder)\ProjectFunctionLibrary.psm1";
    $null = Update-File -Timeout $UpdateFileTimeout -Path "$($global:ProfileData.SourceFolder)\Microsoft.PowerShell_profile.ps1" -Destination "$($global:ProfileData.LocalFolder)\Microsoft.PowerShell_profile.ps1";

} # if ($global:ProfileData.SourceFolder){


#endregion
#region update $PROFILE from $home\windowspowershell\\Microsoft.PowerShell_profile.ps1
################################################################################

Write-Progress $local:ScriptName "Updating $PROFILE if needed";

$returnCode = Update-File -Timeout $UpdateFileTimeout -Path "$($global:ProfileData.LocalFolder)\Microsoft.PowerShell_profile.ps1" -Destination $Profile;
if ($returnCode -lt 0)
{

    Write-Warning "Update-File returned '$returnCode' for updating `$Profile";
    Write-Warning "$PROFILE will not be updated.";

    if ((Read-HostWithTimeout -Prompt "Press 'y' within 5 seconds to continue" -NoNewLine -Timeout 5) -ne 'y')
    {

        Write-Host $null;
        return;

    } # if ((Read-HostWithTimeout -Prompt "Press 'y' within 5 seconds to continue" -NoNewLine -Timeout 5) -ne 'y')

} # if ($returnCode -lt 0)
elseif ($returnCode -gt 0)
{

    if ($Reloaded)
    {

        Write-Warning "$($MyInvocation.MyCommand.Name) in a possible reload loop.";

        if ((Read-HostWithTimeout -Prompt "Press 'y' within 5 seconds to continue" -NoNewLine -Timeout 5) -ne 'y')
        {

            Write-Host $null;
            return;

        } # if ((Read-HostWithTimeout -Prompt "Press 'y' within 5 seconds to continue" -NoNewLine -Timeout 5) -ne 'y')

    } # if ($Reloaded)
    else
    {

        Write-Host -ForegroundColor Cyan -BackgroundColor Black "INFO: Reloading updated `$PROFILE.";
        if (!(Read-HostWithTimeout -Prompt "Press Ctrl+C within 5 seconds to stop processing" -NoNewLine -Timeout 5))
        {

            Write-Host $null;
        
        } # if (!(Read-HostWithTimeout -Prompt "Press Ctrl+C within 5 seconds to stop processing" -NoNewLine -Timeout 5))
        Write-Host $null;

        . $PROFILE -NoSourceFolder -Reloaded -UpdateFileTimeout $UpdateFileTimeout;

    } # if ($Reloaded) ... else

    return; 
} # elseif ($returnCode -gt 0) ... else

#endregion
#region checksum key modules and queue up to be loaded
################################################################################

Write-Progress $local:ScriptName "checksum common and project function libraries";

foreach ($path in @(
    "$($global:ProfileData.LocalFolder -replace '\\[^\\]*$')\CommonFunctionLibrary.psm1", 
    "$($global:ProfileData.LocalFolder)\ProjectFunctionLibrary.psm1"
))
{

    $local:sha256Path = "$path.sha256";
    if (Test-Path -Path $local:sha256Path)
    {
        $local:PathChecksum = Get-Sha256Hash $path;
        $local:GcSha256Path = Get-Content -Path $local:sha256Path;

        if ($local:PathChecksum -eq $local:GcSha256Path)
        {

            $global:ProfileData.ModuleList += $path;
        
        } # if ($local:PathChecksum -eq $local:GcSha256Path)
        else
        {
            Write-Warning "$path checksum does not match.";
            Write-Warning ("    Get-Sha256Hash -Path '$Path' (LastWriteTime {0}):" -f (Get-Item $Path).LastWriteTime.ToString('yyyy-MM-dd HHmmss'));
            Write-Warning "        $local:PathChecksum";
            Write-Warning ("    Get-Content -Path '$Sha256Path' (LastWriteTime {0}):" -f (Get-Item $local:Sha256Path).LastWriteTime.ToString('yyyy-MM-dd HHmmss'));
            Write-Warning "        $local:GcSha256Path";
            Write-Host $null;
            
            if (!(Read-HostWithTimeout -Prompt "Press Ctrl+C within 5 seconds to stop processing" -NoNewLine -Timeout 5))
            {

                Write-Host $null;
        
            } # if (!(Read-HostWithTimeout -Prompt "Press Ctrl+C within 5 seconds to stop processing" -NoNewLine -Timeout 5))

        } # if ($local:PathChecksum -eq $local:GcSha256Path) ... else

    } # if (Test-Path -Path $shaPath)
    else
    {
        Write-Warning "$local:sha256Path not found.  $path will not be loaded";
        if (!(Read-HostWithTimeout -Prompt "Press Ctrl+C within 5 seconds to stop processing" -NoNewLine -Timeout 5))
        {

            Write-Host $null;
        
        } # if (!(Read-HostWithTimeout -Prompt "Press Ctrl+C within 5 seconds to stop processing" -NoNewLine -Timeout 5))

    } # if (Test-Path -Path $shaPath)

} # foreach ($module in @("..\CommonFunctionLibrary.psm1", "ProjectFunctionLibrary.psm1"))

#endregion
#region load modules
################################################################################

Write-Progress $local:ScriptName "Load modules";

for ($i = 0; $i -lt $global:ProfileData.ModuleList.Count; $i++)
{ # use the more archane for (;;) construct so we can append to the array from within the array

    $local:modulePath = $global:ProfileData.ModuleList[$i];

    if (!$local:modulePath)
    { 

        continue;

    } # if (!$local:modulePath)
    elseif (Test-Path -Path $local:modulePath)
    {

        if ($local:modulePath -match '([^\\]*)\.psm1$')
        {

            $local:moduleName = $matches[1];

            if (Get-Module -Name $local:moduleName) 
            {

                Write-Host -ForegroundColor Cyan -BackgroundColor Black "INFO: Unloading $local:modulePath";
                Remove-Module -Name $local:moduleName;

            } # if (Get-Module -Name $local:moduleName) 

        } # if ($local:modulePath -match '([^\\]*)\.psm1$')
        
        if ($DebugPreference -eq 'Continue')
        {

            Set-PSDebug -Trace 2;

        } # if ($DebugPreference -eq 'Continue')

        Write-Host -ForegroundColor Cyan -BackgroundColor Black "INFO: Loading $local:modulePath";
        Write-Host $null;

        Import-Module $local:ModulePath -Force;

        if ($DebugPreference -eq 'Continue')
        {

            Set-PSDebug -Off;

        } # if ($DebugPreference -eq 'Continue')

        

    } # elseif (Test-Path -Path $local:modulePath)
    else
    {

        Write-Warning "$($MyInvocation.MyCommmand) Module '$local:ModulePath' (in `$ProfileData.ModuleList) not found.";
        if (!(Read-HostWithTimeout -Prompt "Press Ctrl+C within 5 seconds to stop processing" -NoNewLine -Timeout 5))
        {

            Write-Host $null;
        
        } # if (!(Read-HostWithTimeout -Prompt "Press Ctrl+C within 5 seconds to stop processing" -NoNewLine -Timeout 5))
    
    } # if (!$local:modulePath) ... elseif ... else
    
} # for ($i = 0; $i -lt $global:ProfileData.ModuleList.Count; $i++)

#endregion
#region finishing up
################################################################################

$global:ContinuePreference = 'Continue';
$global:SilentlyContinuePreference = 'SilentlyContinue';

# load local profile

if (Test-Path "$home/profile.ps1") 
{ 

    . "$home/profile.ps1"; 

} # if (Test-Path "$home/profile.ps1") 

# import profile as module to get commands by module

if (!$global:profilePsm1)
{

    $global:profilePsm1 = "$env:Temp\Microsoft.PowerShell_profile.ps1.psm1"
    Copy-Item -Force $profile $global:profilePsm1;
    Get-Sha256Hash $global:profilePsm1 > "$global:profilePsm1.sha256";
    Import-Module $global:profilePsm1;
    Remove-Variable -Name profilePsm1 -Scope Global -Force -ErrorAction SilentlyContinue;

} # if (!$global:profilePsm1)

Write-Host -ForegroundColor Cyan -BackgroundColor Black ("INFO: $PROFILE loaded in {0} seconds" -f
    [int](((Get-Date) - $global:LoadProfileStartTime).TotalSeconds));
Remove-Item -Path Variable:LoadProfileStartTime; 
Write-Host $null;

#endregion