Monthly Archives: September 2016

Getting Automatic Services That Are Stopped With PowerShell

One of the first things I check when I am troubleshooting a system is whether all the services that should be running, are. I could just open up services.msc, click on the "Startup Type" column to sort by the startup type, and scroll down through the Automatic services to see which ones aren't running. But that's so…. GUI  :p. And slower, and so very one machine at a time. Instead, let's use PowerShell to make it all easier.

 

First, I checked Get-Service, thinking it would give me what I need. but it doesn't. There's no way with Get-Service to find out what the startup type is -- it's not a property returned by Get-Service. (Yes, I think this is a deficiency. And yes, I expect someday we might get an improvement to Get-Service. But for the moment, we have to work around it. )

 

Instead, I decided to use the Get-WmiObject cmdlet to find what we need. (If the machine you're running this from is running PowerShell v3 or later, you can substitute Get-CimInstance for Get-WmiObject. But if you do, you won't be able to use -Credential.)

 

Get-WmiObject Win32_Service returns a list of all the services on the local machine. We can extend it with -ComputerName to query the services on a remote computer. And we can filter those services, though the filtering uses WQL as the query language, which is a nuisance since it doesn't match up to the Filter syntax for the ActiveDirectory module, for example.

 

To get a list of all the services that should have started automatically, but that are not currently running, on the local machine:

Get-WmiObject -ClassName Win32_Service -Filter "StartMode='Auto' AND State<>'Running'"

But that output is a bit ugly, so we'll throw some Format-Table at it, and come up with:

Get-WmiObject -ClassName Win32_Service `
              -Filter "StartMode='Auto' AND State<>'Running'" `
             | Format-Table -Auto DisplayName,Name,StartMode,State

Not bad. That gives us an easy to read output with all the information we need. We can wrap that up in a simple cmdlet that assumes the local computer, but that allows us to run it against multiple computers. And we want it to be able to get that list of computer names through the pipeline, of course. Plus, we'll add a Credential parameter to allow us to run against machines on a different domain, or a workgroup, so long as we provide an appropriate credential.

 

If we're going to get output from multiple computers, however, we need to know which one has which services that aren't running. To do that, we take advantage of Format-Tables GroupBy parameter:

Get-WmiObject -ClassName Win32_Service `
              -Filter "StartMode='Auto' AND State<>'Running'" `
             | Format-Table -AutoSize `
                            -Property DisplayName,Name,StartMode,State `
                            -GroupBy  PSComputer

Now we have everything we need to pull our script together.

Get-myStoppedService.ps1

<#
.Synopsis
Gets a list of stopped services
.Description
Get-myStoppedService takes a list of computer names and returns 
a table of the stopped services on that computer that are set to 
automatically start. The default is to return a list on the local computer.
.Example
Get-myStoppedService
Returns a table of stopped services on the local computer
.Example
Get-myStoppedService -ComputerName 'server1','client2'
Returns a table of stopped services on server1 and client2, 
grouped by computer name
.Parameter ComputerName
A list of remote computer names to query. If the current account 
doesn't have permission to query WMI on the remote computer, use 
the Credential parameter to provide alternate credentials. 
The default is the local host.
.Parameter Credential
Standard PSCredential object. Use Get-Credential.
.Inputs
[string[]]
[PSCredential]
.Notes
    Author: Charlie Russel
 Copyright: 2016 by Charlie Russel
          : Permission to use is granted but attribution is appreciated
   Initial: 29 September, 2016 (cpr)
   ModHist:
          :
#>
[CmdletBinding()]
Param(
     [Parameter(Mandatory=$False,Position=0,ValueFromPipeline=$True)]
     [Alias("Name","VMName")]
     [string[]]
     $ComputerName = ".",
     [Parameter(Mandatory=$False,ValueFromPipeline=$True)]
     [PSCredential]
     $Credential = $NULL
     )

if ($Credential) {
   Get-WMIObject -ClassName Win32_Service `
                 -Credential $Credential `
                 -ComputerName $ComputerName `
                 -Filter "StartMode='Auto' AND State<>'Running'" `
                | Format-Table -Auto DisplayName,Name,StartMode,State -GroupBy PSComputerName
} else {
   Get-WmiObject -ClassName Win32_Service `
                 -ComputerName $ComputerName `
                 -Filter "StartMode='Auto' AND State<>'Running'" `
                | Format-Table -Auto DisplayName,Name,StartMode,State -GroupBy PSComputerName
}

Starting a PowerShell window as a Domain Admin

If you run as a limited user on your own desktop, as you should, it's useful to keep a separate PowerShell window open as the Domain Administrator. I give that window a nice dark red background so I know instantly that I'm in a powerful window and to be appropriately careful. But how can I actually start a window as a different account, and using Run As Administrator? Ah, so glad you asked.

$AdminCred = Get-Credential -UserName "TreyResearch\domainadmin" `
                            -Message "Enter your password for the DomainAdmin account:" 
Start-Process PowerShell.exe -Credential $AdminCred `
                             -ArgumentList "Start-Process PowerShell.exe -Verb RunAs" `
                             -NoNewWindow

Save this as "Start-myAdmin.ps1" or equivalent and it's always available. The nice thing about using Start-Process with the NoNewWindow parameter is that it doesn't leave a spare window open. Try it. It will make it just that much easier to run as a limited user with no administrative rights, even on your own workstation. And really, when you have the power to be a domain admin, you really, really, really shouldn't run any other way.

Param() Tricks

One of the new features of PowerShell v5 is support for creating hard links, symbolic links and junctions. This is long overdue, and much appreciated. Before, I'd been forced to the workaround of using "cmd /c mklink" to create links, and I'm always glad to find a way to get rid of any vestige of cmd. Plus, having it as a part of PowerShell gives me way more flexibility in creating scripts.

 

As I was looking at some of my existing scripts, it occurred to me that I should be taking advantage of hard links in more scripts. I already use hard links for my various RDP connects, using a long switch statement. (I'll write that up one of these days, it's actually pretty cool.) But what caught my eye today was the script I wrote to create virtual machines for my labs -- New-myVM.ps1. I have a -Client parameter to the script that is a Boolean, defaulted to $False:

[CmdletBinding()]
Param([Parameter(Mandatory = $True,Position = 0)]
      [alias("Name")]
      [string]
      $VmName,
      [Parameter(Mandatory=$False)]
      [Boolean]
      $Client=$False
      )

Which is OK, but it occurred to me that I could do better. So, first, I created a new, hard-linked file with:

New-Item -Type HardLink -Name New-myClientVM.ps1 -Path .\New-myVM.ps1

Now I have one file with two names. Cool. So, let's take that a step further. I can tell which version of it I called from the command line by taking advantage of the automatic PowerShell variable $myInvocation:

$myInvocation.mycommand.name

This returns the filename (".name") of the command (".mycommand") that was executed ($myInvocation). So now, I can use:

$client =  ($myInvocation.mycommand.name -match "client")

I put that near the top of the script, and now I could branch depending on whether I was creating a server VM or a client VM. Which was definitely better, but still left me thinking it could be improved.

 

So, how about making the whole thing a lot cleaner by getting rid of that extra line? After all, I'm creating a variable and defaulting its value to $false, but why not default its value more intelligently, controlled by which file I executed to create the VM? I can still override it with the parameter (so no scripts that call this script will break), but now, I can set it automatically without using a parameter at all.

[CmdletBinding()]
Param([Parameter(Mandatory = $True,Position = 0)]
      [alias("Name")]
      [string]
      $VmName,
      [Parameter(Mandatory=$False)]
      [Boolean]
      $Client=($myInvocation.myCommand.Name -match "Client")
      )

Now that pleases me. It feels "cleaner", it's clear what I'm doing, and it doesn't take any longer to evaluate than it would as a standalone line.

Starting PowerShell Automatically – Revisited

A few months ago, I posted a quick blog on how I set PowerShell to start automatically when I log in. Well, it occurred to me that I should extend that to show you how I set the console window parameters so that my PowerShell windows are the right size and font, since on all my systems I use the same basic settings (except for my Surface Book with the 4K addon monitor -- my old eyes need a bit bigger on that, so I adjusted the values accordingly.)

The function I use to set the console size, font and font weight is: (FontSize here translates to 18 point on my FullHD systems)

Function Set-myDefaultFont () {
   $64bit = "HKCU:\Console\%SystemRoot%_System32_WindowsPowerShell_v1.0_powershell.exe"
   $Default = "HKCU:\Console"
   Set-ItemProperty -Path $Default -Name FaceName   -Value "Consolas"
   Set-ItemProperty -Path $Default -Name FontSize   -Type DWord -Value 0x00120000
   Set-ItemProperty -Path $Default -Name FontWeight -Type DWord -Value 0x000002bc
   Set-ItemProperty -Path $Default -Name FontFamily -Type DWord -Value 0x00000036 
   Set-ItemProperty -Path $Default -Name QuickEdit  -Type DWord -Value 0x00000001
   Set-ItemProperty -Path $64bit -Name FaceName     -Value "Consolas"
   Set-ItemProperty -Path $64bit -Name FontSize     -Type DWord -Value 0x00120000
   Set-ItemProperty -Path $64bit -Name FontWeight   -Type DWord -Value 0x000002bc
   Set-ItemProperty -Path $64bit -Name FontFamily   -Type DWord -Value 0x00000036 
   Set-ItemProperty -Path $64bit -Name QuickEdit    -Type DWord -Value 0x00000001
}

That's all there is to it. I've added this function to the Set-myPowerShellStart.ps1 script, and I call that whenever I create or log onto a new system. If you prefer different sizes or a different font, simply change the values to match what you need. For example, if you want a 16 point font, try a value of 0x00100000.

ETA: Fixed value of FontSize.

Active Directory — Unlocking a User Account with PowerShell

As any SysAdmin knows, users periodically lock themselves out of their accounts, usually because they forgot a password or somehow mistyped it too many times. And after all, if it failed once, why not keep trying it? Unlocking that account is NOT something you do with Set-ADUser, unfortunately, because the PowerShell ActiveDirectory module has a special, single-purpose cmdlet - Unlock-ADAccount. Now, it doesn't take a whole script to run a single cmdlet, but sometimes there are good reasons to wrap a command in a more complicated script, and this is one I've had to. Primarily because I work on at least three different domains that are unrelated - my home domain, my "work" domain, and my testlab domain. With this script, I can also take advantage of an encrypted password for one of those domains, stored securely on my main desktop's hard disk. Plus, I've added in a bit of code to go grab the current holder of the FSMO PDCEmulator role, since I'd prefer to unlock the account directly against that PDC.

Also, to make life easier, and allow us to stick this in the middle of a pipeline when we need to, we'll add the ability to handle parameters from the pipeline. We do that in the Param section:

[CmdletBinding()]
Param(
     [Parameter(Mandatory=$True,ValueFromPipeline=$true,Position=0)]
     [alias("user","account")]
     [string[]]
     $Identity,
     [Parameter(Mandatory=$False,ValueFromPipeline=$True,Position=1)]
     [string]
     $Domain = "DOMAIN",
     [Parameter(Mandatory=$False,ValueFromPipeline=$True)]
     [PSCredential]
     $Credential = $NULL
     )

You'll notice here that we've allowed for each of our parameters to accept pipeline input. And they'll accept that "ByValue", meaning that the piped objects must have the same .NET type, or must be able to be converted to that type. So only an actual PSCredential object can be accepted for the -Credential parameter.

 

Next, we want to be able to handle credentials in any of several ways. So we use:

if ( $Credential ) {
   $myCred = $Credential
} else {
   # Test if there is a stored, encrypted, password we can use
   $pwPath = "$home\Documents\WindowsPowerShell\$domainPW.txt"
   if (Test-Path $pwPath ) {
      $mypw = Get-Content $pwPath | ConvertTo-SecureString
      $myCred = New-Object -TypeName System.Management.Automation.PSCredential `
                           -ArgumentList "$DOMAIN\Charlie",$mypw
   } else {
      # Prompt for a credential, since we don't seem to have one here. 
      $myCred = Get-Credential 
   }
}

Let's look at this for a moment. First, we know from the Param section that the default value of $Credential is $NULL, so our first test is whether that's been overridden at the command line with either a pipelined parameter, or a specifically entered credential object. If so, we're good, and we use that. But failing that, we'll check if there's one stored securely on disk. (More on storing credentials on disks in another post soon.) If we've stored one on disk that matches the domain we're going against, we'll use that. If not, and we still don't have a PSCredential object we can use, we'll simply prompt for one.

 

Next up, we want to run this against the domain controller that hosts the PDCEmulator role. Not a big deal in simplified domain environment, but really a good idea in a complicated, multi-site environment where site-to-site propagation is a bit slow.

$PDC = (Get-ADDomain -Identity $DOMAIN -Credential $myCred).PDCEmulator

We'll plug the value of $PDC into the -Server parameter of our AD commands.

Finally, the meat of the whole thing. I'm not assuming you're only unlocking a single account, so I've designed this to take a list of strings ([string[]]). (A list can, of course, be a list of length one.)

ForEach ($usr in $Identity) {
   Unlock-ADAccount -Identity $usr `
                    -Credential $myCred `
                    -Server $PDC `
                    -PassThru `
       | Get-ADUser -Properties LockedOut

That last line ensures that we report back in a way that shows the account no longer locked out. But for Get-ADUser to actually do anything, you need to include the -PassThru parameter in the Unlock-ADAccount command. Otherwise, nothing at all gets passed to the Get-ADUser command.

So, here's the whole thing:

<#
.Synopsis
Unlocks a  domain account
.Description
Unlock-myUser accepts an array of DOMAIN account names and unlocks the accounts 

This script accepts pipeline input for the credential. If no credential is supplied, 
it will attempt to use one you have stored on disk. Failing that, it will prompt 
you for credentials. 
.Example
Unlock-myUser -Identity Charlie
Unlocks the account of Charlie 
.Example
Unlock-myUser -Identity Charlie, Sharon
Unlocks the accounts of Charlie and Sharon 
.Example
Unlock-myUser -Identity Charlie -Domain TREYRESEARCH `
              -Credential (Get-Credential `
                               -username "TREYRESEARCH\Alfie"  `
                               -Message "Enter your Domain PW")

Unlocks the account of TreyResearch\Charlie, prompting Alfie for credentials. 
.Parameter Identity
The AD identity of the user or users whose acounts are to be reset.
.Parameter Domain
The Active Directory domain of the user or users whose accounts are to be reset
.Parameter Credential
The user credentials to run the script under. 
.Inputs
[string[]]
[string]
[PSCredential]
.Notes
    Author: Charlie Russel
 Copyright: 2016 by Charlie Russel
          : Permission to use is granted but attribution is appreciated
   Initial: 07 Sept, 2016 (cpr)
   ModHist: 09 Sept, 2016 (cpr) - added PDC test and Domain parameter 
          :
#>
[CmdletBinding()]
Param(
     [Parameter(Mandatory=$True,ValueFromPipeline=$true,Position=0)]
     [alias("user","account")]
     [string[]]
     $Identity,
     [Parameter(Mandatory=$False,ValueFromPipeline=$True,Position=1)]
     [string]
     $Domain = "DOMAIN",
     [Parameter(Mandatory=$False,ValueFromPipeline=$True)]
     [PSCredential]
     $Credential = $NULL
     )

if ( $Credential ) {
   $myCred = $Credential
} else {
   # Test if there is a stored, encrypted, password we can use
   $pwPath = "$home\Documents\WindowsPowerShell\$domainPW.txt"
   if (Test-Path $pwPath ) {
      $mypw = Get-Content $pwPath | ConvertTo-SecureString
      $myCred = New-Object -TypeName System.Management.Automation.PSCredential `
                           -ArgumentList "DOMAIN\Charlie",$mypw
   } else {
      # Prompt for a credential, since we don't seem to have one here. 
      $myCred = Get-Credential 
   }
}

# Find out which server holds the PDC role. 
# Useful in complicated, multi-site environments where 
# Domain changes might not propagate quickly. 

$PDC = (Get-ADDomain -Identity $DOMAIN -Credential $myCred).PDCEmulator

ForEach ($usr in $Identity) {
   Unlock-ADAccount -Identity $usr `
                    -Credential $myCred `
                    -Server $PDC `
                    -PassThru `
       | Get-ADUser -Properties LockedOut
}

As always, feel free to use this script or any portion of it that you find useful. However, if you do, I'd appreciate attribution and a pointer back to my blog.

Testing for Location on a Laptop

On my laptops, I have a different set of drive maps when I'm at home, or on the road. At home, I map to various local domain resources, but when I'm on the road, those resources aren't available and I need to create "local" maps to versions that I can sync up to those domain resources when I get back home. But how to know where I am? Use PowerShell's Test-Connection, of course, the PowerShell version of "ping".

 

The problem is further complicated by the dual nature of my home network - I have two different subnets, and the resources could be reachable on either, depending on the wireless network to which I'm connected.

 

So, how to handle? I've chosen to use the logical OR operator for this test. I could have used an if {} elseif {} construction, but that seemed clunky, so I went with a simple test:

$AtHome= ((test-connection 192.168.16.2 `
                           -Count 1 `
                           -TimeToLive 1 `
                           -quiet) `
         -OR (Test-Connection 192.168.17.2 `
                           -count 1 `
                           -TimeToLive 1 `
                           -quiet)
        )

The test tells me if my domain controller is reachable on either of the subnets used. Because PowerShell uses shortcut processing for logical operators, if the test succeeds at 192.168.16.2, it immediately returns $True to the $AtHome variable and doesn't bother processing the second test. But if it fails the first test, it then tries the second subnet. If it can reach 192.168.17.2, it sets $AtHome to $True. If not, it sets $AtHome to $False.

 

Now, when I call my drive mapping script, and I pass it the result of $AtHome. That script knows to modify the behaviour based on the Boolean value of $AtHome...

Map-myDrive -SMB -AtHome $AtHome -Force

Mapping Drives Revisited

In my old drive mapping post, I was forced to do some fairly ugly stuff because I had to call the old net use command. Yech. Eventually, we got New-PSDrive, and that helped, but in PowerShell v5 (Windows 10's version), we get New-SmbMapping and it actually works. (New-SmbMapping was added earlier, but there were issues. Those appear to be resolved in the final version of v5.)

 

When New-PSDrive finally had persistent drive mappings, I replaced my my old MapDrives.cmd file with a new MapDrives.ps1 that used the New-PSDrive syntax:

New-PSDrive -Name I -root \\srv2\Install -Scope Global -PSProv FileSystem -Persist

A bit awkward, but it works. However, it sometimes ran into problems with drives that were mapped with net use, so I was glad when we finally got a useful version of New-SmbMapping. Now the syntax for mapping my I: drive to \\srv1\install is:

New-SmbMapping -LocalPath I: -RemotePath \\srv2\Install -Persistent $True

Great. But it doesn't have a -Force parameter, so I can't tell it to override any maps that already exist. That requires cleaning up the old maps before I make new ones. For that, we have Remove-SmbMapping and Remove-PsDrive.

Function Remove-myMaps () {
   $DriveList = (Get-SmbMapping).LocalPath
   write-verbose "Removing drives in DriveList: $drivelist"
   Foreach ($drive in $DriveList) {
      $psDrive = $drive -replace ":" #remove unwanted colon from PSDrive name
      Write-Verbose "Running Remove-SmbMapping -LocalPath $Drive -Force -UpdateProfile"
      Remove-SmbMapping -LocalPath $Drive -Force -UpdateProfile 
      If ( (Get-PSDrive -Name $psDrive) 2>$Null ) {
      Write-Verbose "Running Remove-PsDrive -Name $psDrive -Force "
      Remove-PSDrive -Name $psDrive -Force
      }
   }
      write-host " "
      $DriveList = (Get-SMBMapping).LocalPath
      Write-Verbose "The drive list is now: $DriveList"
}

As you might have noticed, PsDrives don't have a colon, and SmbMapping drives do. But PowerShell gives us the useful -replace operator, allowing us to simply remove the stray colon from the drive letter that SmbMapping has.

So, here's the entire script. Feel free to use it as the basis for your own mappings, but please, respect the copyright and don't republish it but link to here instead. Thanks. :)

Charlie.

<#
.Synopsis
Maps network drives to drive letters
.Description
Map-myDrive is used to map network resources to local drive letters. It can use SmbMapping or PsDrive to 
do the mapping, but defaults to simple PsDrives for historical reasons. (SmbMapping was buggy when it 
was first introduced!)
.Example
Map-myDrive

Performs a standard drive mapping on the local machine using New-PsDrive syntax.
.Example
Map-MyDrive -SMB

Performs a standard drive mapping on the local machine using New-SmbMapping syntax.
.Example
Map-MyDrive -SMB -Force

Performs a standard drive mapping on the local machine using New-SmbMapping syntax, and
forces an unmapping of any existing drive maps before remapping.
.Example
Map-MyDrives -SMB -Force -AtHome $False

Performs a standard drive mapping on the local machine using New-SmbMapping syntax, and
forces an unmapping of any existing drive maps before remapping. The script assumes that 
it is NOT being run in my home domain, and therefore does only local mappings.
.Parameter SMB
When set, Map-myDrive uses SMB syntax to do the drive mappings
.Parameter Force
When set, Map-myDrive completely unmaps any existing drive mappings, and then remaps them. 
.Parameter AtHome
When True, Map-myDrive assumes that it has connectivity to the home domain resources. When
False, it assumes no home domain resources are available and maps to local shares. 
.Inputs
[switch]
[switch]
[switch]
[Boolean]
.Notes
    Author: Charlie Russel
 Copyright: 2016 by Charlie Russel
          : Permission to use is granted but attribution is appreciated
   Initial: 10 June, 2012
   ModHist: 26 June, 2012 A single, cleaner, call to GWMI
          : 26 April,2015 Cleaned up smb unmapping failures, and added Verbose
          : 27 June, 2015 New unmapping function
          : 04 Sept, 2016 Added AtHome Boolean to override environment, Force switch to force unmapping/remapping
          :
#>
[CmdletBinding(SupportsShouldProcess=$True)] 
Param ([Parameter(Mandatory=$false)][Switch]$SMB,
       [Parameter(Mandatory=$false)][Switch]$Force,
       [Parameter(Mandatory=$false)][Boolean]$AtHome=$True)

#
Write-Verbose "Running mapdrives.ps1 with SMB set to $SMB"
Write-Verbose "Athome is set to $AtHome"
Write-Verbose "Force is $Force"

$Psh = Get-Process PowerShell

# Start by checking for mapped drives
$DriveList = $Null
$PSDriveList = $Null
#$DriveList = Get-WMIObject Win32_LogicalDisk | Where-Object { $_.DriveType -eq 4 }

$DriveList = (Get-SMBMapping).LocalPath
write-verbose "The DriveList is: $DriveList"

if ($DriveList) {
   $PSDriveList = $DriveList -replace ":"
   write-verbose "The PSDrivelist is: $PSDriveList"
}

# Unmap any lingering ones
Function Remove-myMaps () {
   write-verbose "Removing drives in DriveList: $drivelist"
   Foreach ($drive in $DriveList) {
      $psDrive = $drive -replace ":" #remove unwanted colon from PSDrive name
      Write-Verbose "Running Remove-SmbMapping -LocalPath $Drive -Force -UpdateProfile"
      Remove-SmbMapping -LocalPath $Drive -Force -UpdateProfile 
      If ( (Get-PSDrive -Name $psDrive) 2>$Null ) {
      Write-Verbose "Running Remove-PsDrive -Name $psDrive -Force "
      Remove-PSDrive -Name $psDrive -Force
      }
   }
      write-host " "
      $DriveList = (Get-SMBMapping).LocalPath
      Write-Verbose "The drive list is now: $DriveList"
} 

Function Map-myPSDrive () {
   New-PSDrive -Name I -root \\server\Install    -Scope Global -PSProv FileSystem -Persist
   New-PSDrive -Name J -root \\server\Download   -Scope Global -PSProv FileSystem -Persist
   New-PSDrive -Name S -root \\server\SharedDocs -Scope Global -PSProv FileSystem -Persist
   New-PSDrive -Name W -root \\server\Working    -Scope Global -PSProv FileSystem -Persist
   New-PSDrive -Name H -root \\server2\Download  -Scope Global -PSProv Filesystem -Persist
   New-PSDrive -Name K -root \\server2\Kindle    -Scope Global -PSProv FileSystem -Persist
   New-PSDrive -Name M -root \\server2\Music     -Scope Global -PSProv FileSystem -Persist
   New-PSDrive -Name N -root \\server2\Audible   -Scope Global -PSProv FileSystem -Persist
   New-PSDrive -Name P -root \\server2\Pictures  -Scope Global -PSProv FileSystem -Persist
   New-PSDrive -Name T -root \\labserver\Captures\70-742 -Scope Global -PSProv FileSystem -Persist
}

# Map using SMBMapping. It's more robust
Function New-mySMBMaps () {
   New-SmbMapping -LocalPath I: -RemotePath \\server\Install    -Persistent $True
   New-SmbMapping -LocalPath J: -RemotePath \\server\Download   -Persistent $True
   New-SmbMapping -LocalPath S: -RemotePath \\server\SharedDocs -Persistent $True
   New-SmbMapping -LocalPath W: -RemotePath \\server\Working    -Persistent $True
   New-SmbMapping -LocalPath H: -RemotePath \\server2\Downloads -Persistent $True
   New-SmbMapping -LocalPath K: -RemotePath \\server2\Kindle    -Persistent $True
   New-SmbMapping -LocalPath M: -RemotePath \\server2\Music     -Persistent $True
   New-SmbMapping -LocalPath N: -RemotePath \\server2\Audible   -Persistent $True
   New-SmbMapping -LocalPath P: -RemotePath \\server2\Pictures  -Persistent $True
   New-SmbMapping -LocalPath T: -RemotePath \\labserver\Captures\70-742 -Persistent $True
}

Function Map-myLocal () {
   New-SmbMapping -LocalPath K: -RemotePath \\localhost\Kindle          -Persistent $False  
   New-SmbMapping -LocalPath T: -RemotePath \\localhost\Captures\70-742 -Persistent $False  
}

if(! $PSDriveList ) {
   if ($Force) { Remove-myMaps }
   Write-Verbose "SMB is set to $SMB"
   if ($SMB) { 
      If ($AtHome) { 
         New-mySMBMaps 
      } else {
         Map-myLocal
      }
   } else { 
      If ($AtHome) { 
         Map-myPSDrive 
      } else {
         Map-myLocal
      }
    }
} else { 
   # $Psh.count is the number of open PowerShell windows. I know that if it's less than or equal
   # to 2, then I haven't yet mapped the drives on both limited user and administrative windows. 
   # Therefore, we need to mapdrives here. Or, if I have run this with a -Force command, obviously.
   if (($Psh.count -le 2) -OR ($Force)) {
   Remove-myMaps
   Write-Verbose "SMB is set to $SMB"
   if ($SMB) { 
      If ($AtHome) { 
         New-mySMBMaps 
      } else {
         Map-myLocal
      }
   } else { 
      If ($AtHome) { 
         Map-myPSDrive 
      } else {
         Map-myLocal
      }
   }
  }
}