Tuesday, March 12, 2013

Change Device COM Port via PowerShell

On a recent project, we had a situation where we had to make sure that a specific USB hardware device was always assigned to a COM1.  Because of the way USB devices are assigned COM ports, it was never going to get COM1 the first time. I whipped up a quick script to check for the device and then change its COM port. It worked great, but as is the case with many first iterations of scripts, there was a but...

Even though a new COM port was getting assigned, we found is that the previous COM port was remaining marked as used in the ComDB. The ComDB is a binary registry value that keeps track of which COM ports are in use. A great description of how the value works can be found here: http://www.rosseeld.be/DRO/PIC/NextSerialPortNo.htm.

To solve the problem we had two options:
1) Find a way to get access to the msports.dll functions within PowerShell which likely meant digging into the Windows Driver SDK.
2) Parse the binary registry value and update the ComDB manually.

I chose door number two because it seemed much more straight forward. The task was actually a little bit trickier than I expected because the ComDB reverses the COM port bits in each byte (e.g. COM1 is represented by Bit 8 and COM8 is represented by Bit 1). Also, to properly convert the Byte value, each Byte had to have 8 values (leading zeros can't be truncated). To remedy that I used a loop to make sure the binary character array had the proper number of entries.

The function below is the end result...

$DeviceName = "My Hardware Name"
$ComPort = "COM4"


function Change-ComPort {

Param ($Name,$NewPort)

#Queries WMI for Device
$WMIQuery = 'Select * from Win32_PnPEntity where Description = "' + $Name + '"'
$Device = Get-WmiObject -Query $WMIQuery

#Execute only if device is present
if ($Device) {

#Get current device info
$DeviceKey = "HKLM:\SYSTEM\CurrentControlSet\Enum\" + $Device.DeviceID
$PortKey = "HKLM:\SYSTEM\CurrentControlSet\Enum\" + $Device.DeviceID + "\Device Parameters"
$Port = get-itemproperty -path $PortKey -Name PortName
$OldPort = [convert]::ToInt32(($Port.PortName).Replace("COM",""))

#Set new port and update Friendly Name
$FriendlyName = $Name + " (" + $NewPort + ")"
New-ItemProperty -Path $PortKey -Name "PortName" -PropertyType String -Value $NewPort -Force
New-ItemProperty -Path $DeviceKey -Name "FriendlyName" -PropertyType String -Value $FriendlyName -Force

#Release Previous Com Port from ComDB
$Byte = ($OldPort - ($OldPort % 8))/8
$Bit = 8 - ($OldPort % 8)
if ($Bit -eq 8) { 
$Bit = 0 
$Byte = $Byte - 1
}
$ComDB = get-itemproperty -path "HKLM:\SYSTEM\CurrentControlSet\Control\COM Name Arbiter" -Name ComDB
$ComBinaryArray = ([convert]::ToString($ComDB.ComDB[$Byte],2)).ToCharArray()
while ($ComBinaryArray.Length -ne 8) {
$ComBinaryArray = ,"0" + $ComBinaryArray
}
$ComBinaryArray[$Bit] = "0"
$ComBinary = [string]::Join("",$ComBinaryArray)
$ComDB.ComDB[$Byte] = [convert]::ToInt32($ComBinary,2)
Set-ItemProperty -path "HKLM:\SYSTEM\CurrentControlSet\Control\COM Name Arbiter" -Name ComDB -Value ([byte[]]$ComDB.ComDB)

}
}

Change-ComPort $DeviceName $ComPort

Tuesday, February 19, 2013

PowerShell Setting ACL Inheritance and Propegation

I recently worked on a project that required a lot of  AdHoc moving of user home directories. To make the process easier for the support teams, I put together a PowerShell script (and a web front end for it, but more on that later) that would move a users home directory to a new server and update their account's homeDirectory attribute.

As is often the case these days, these home directories are stored on NAS devices that typically auto-create home directories on the fly with the appropriate permissions. As a result, my script needed to explicitly grant the user Full Control to their new folder. I didn't find the native help on the set-acl cmdlet very helpful in regard to adding a user to an existing folder's ACL. Luckily I did something similar in VB.NET and had good idea of what needed to be done. That along with a little get-acl and get-member action (and a little trial and error) got me where I needed to be.

The process is actually pretty straight froward.

  1. Capture the current ACL with Get-ACL
    $DirACL = Get-ACL "\\FileServer\users$\JoeUser"
  2. Create a new FileSystemAccessRule for the user and add it to the ACL you just captured
    $AccessRule = New-Object System.Security.AccessControl.FileSystemAccessRule "Domain\JoeUser","FullControl","ContainerInherit,ObjectInherit","None","Allow"
    $DirACL.AddAccessRule($AccessRule)
  3. Write the new ACL back with set-acl
    Set-ACL "\\FileServer\users$\JoeUser" $DirACL
Since it take a couple of lines and is something that often needs to be repeated, it could easily be made a function like this:

Function Set-FullControl {
     param ([string]$User, [string]$FolderPath)

     $DirACL = Get-ACL $FolderPath
     $AccessRule = New-Object System.Security.AccessControl.FileSystemAccessRule $User,"FullControl","ContainerInherit,ObjectInherit","None","Allow"
     $DirACL.AddAccessRule($AccessRule)
     Set-ACL $FolderPath $DirACL
}

Set-FullControl "DOMAIN\JoeUser" "\\FileServer\users$\JoeUser"