Following on from the post where I wrote about managing the entries in the PATH environment variable to avoid OS and application misbehaviour, I thought I'd share another story on PATH which can also show you why we (IT guys) should never forget the basics and that the simplest idea is the best idea!
I've come across some hosts where the PATH was more than 2048 characters long which made applications stop working. OK, no problem (I thought), I just run my script which removes duplicates from the PATH and it should go well under 2048 characters. I'm not an overconfident guy, but I was surprised when I saw the length still well above 2000. Now what? Then it hit me, why do I take it for granted that all these entries in the PATH are needed? Maybe there are PATH entries which don't even exist on the file system anymore! Simple, isn't it, just get rid of garbage from the environment variable.
Let's run through the PATH entries one by one and check if those folders exist on the box at all - important: if you do this on a cluster node where one of the PATH entries points to one of the clustered drives, you can incorrectly identify that folder as non existent, but in reality the folder exists, it's just on a disk which is active on another node of the cluster at that time, so be careful.
Of course we want to do this remotely:
$srv = "r2d2"
$pathString = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $srv).OpenSubKey("SYSTEM\CurrentControlSet\Control\Session Manager\Environment").getvalue("path")
$pathstring.split(";") | %{
$obj = "" | Select Item,Accessible
$obj.Item = $_.trim()
$obj.Accessible = test-path ("\\$srv\" + $obj.item.replace(":","$"))
$obj}
Make it shorter by creating the output object on the fly:
$srv = "r2d2"
$pathString = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $srv).OpenSubKey("SYSTEM\CurrentControlSet\Control\Session Manager\Environment").getvalue("path")
$pathstring.split(";") | %{
New-Object psobject -Property @{test=(test-path $_); item=$_}
} | ft item,test -auto
Or you can do it locally on a host (which makes it simpler and it can even be a oneliner:
(Get-ItemProperty "HKLM:SYSTEM\CurrentControlSet\Control\Session Manager\Environment").path.split(";") | %{new-object psobject -Property @{test=(test-path $_); item=$_}} | ft item,test -auto
This will show you something like:
You can make this a script by adding parameters, get the list of hosts from $input...etc.
t
Powershell one-liners and short scripts for real-life problems on large and complex Windows networks.
Showing posts with label remote registry. Show all posts
Showing posts with label remote registry. Show all posts
14 May, 2014
28 December, 2013
PATH environment variable ordering - OS
The following problem can be annoying even if you have 5 servers, but even more nerve wrecking if you experience it on 100+ servers - at my favorite time - on a Monday morning.
Usually there are applications installed on a server - why would we need them otherwise. Some applications need to access their files without full path of those therefore they update the PATH environment variable. This is all well because most of the developers know how to do it nicely, but I've come across some applications in my 10+ years of IT experience which I still cannot get my head around.
The PATH environment variable is basically a list of folders separated by ; (semicolons). And even though no one says it, the order of these entries does matter. E.g.: there are applications which - for whatever unspoken reason - add entries to the beginning (!) of this list. Now, if you read the previous 2 sentences carefully, you can have an idea why this is a very bad idea:
The entries in the PATH are evaluated in order, that's why the OS puts stuff in there first (like C:\WINDOWS, C:\WINDOWS\System32...etc.) because there are lot of files there and it's quicker to run a server if you know that most of the queries for files via PATH will be served by the first couple of items. If you put entries to the beginning which are only important for some applications and for just minor number of queries, you basically make your system slower.
Even worse, if - for whatever unspoken reason - there are applications which put PATH entries to the beginning of the variable which refer to either mapped network drives or UNC path, then you can be in trouble. E.g. when there was one such on a couple of servers and surprise, the box got hung on boot-up because the custom disk manager wanted to look-up some files and hit a PATH entry pointing to a UNC path before the custom SMB engine was initialised. Nice.
Side notes:
It reads how many backup values exist already, increments the number and creates a new value with the current content of PATH
MoveToArrayBeginning
It runs through an array and adds items to a temp array. If an item matches a given pattern it adds the item to the beginning of the array otherwise it adds it to the end. The key piece is this, which adds the element to the array as either the first or last item:
$farray | %{
if($_ -imatch $pattern){
[Void]$newArray.Insert(0,$_)
}
else{
[Void]$newArray.add($_)
}
}
MoveToArrayEnd
It collects all items matching a pattern to a temp array and then removes and adds those to the end of the original array. It needs the temp array to preserve the original order of the items being moved to the end.
t
Usually there are applications installed on a server - why would we need them otherwise. Some applications need to access their files without full path of those therefore they update the PATH environment variable. This is all well because most of the developers know how to do it nicely, but I've come across some applications in my 10+ years of IT experience which I still cannot get my head around.
The PATH environment variable is basically a list of folders separated by ; (semicolons). And even though no one says it, the order of these entries does matter. E.g.: there are applications which - for whatever unspoken reason - add entries to the beginning (!) of this list. Now, if you read the previous 2 sentences carefully, you can have an idea why this is a very bad idea:
The entries in the PATH are evaluated in order, that's why the OS puts stuff in there first (like C:\WINDOWS, C:\WINDOWS\System32...etc.) because there are lot of files there and it's quicker to run a server if you know that most of the queries for files via PATH will be served by the first couple of items. If you put entries to the beginning which are only important for some applications and for just minor number of queries, you basically make your system slower.
Even worse, if - for whatever unspoken reason - there are applications which put PATH entries to the beginning of the variable which refer to either mapped network drives or UNC path, then you can be in trouble. E.g. when there was one such on a couple of servers and surprise, the box got hung on boot-up because the custom disk manager wanted to look-up some files and hit a PATH entry pointing to a UNC path before the custom SMB engine was initialised. Nice.
Side notes:
- There can also be an issue with older applications which can only handle up to 1024 characters of the PATH - even though since Windows 2003 SP1 and WinXP SP2 the supported length of PATH is 2048 characters
- in Powershell (or rather .NET framework), the Add-Type command fails if there are invalid entries in the PATH
Solutions:
To resolve the 2 issues above ((1) make sure the PATH has a desired order and (2) it is as short as possible) you can do a couple of things:- remove the last backslash from each entry - this saves some characters and makes it easier to spot duplicates later:
foreach($item in $tmpdataArray){
if($item[-1] -eq "\"){$item = $item -replace "\\$",""}
$dataArray += $item
} - remove duplicate entries from the PATH:
$newTmpdataArray = $dataArray | Sort-Object -Unique -Descending - if the length is still a problem, consider converting long folder names to the old 8.3 type names, e.g.:
$newdata = $newdata -ireplace "\\program files\\", "\progra~1\" - put entries to the beginning which are needed for the OS and put any "external path" (UNC, mapped drives) to the end or remove them if possible, e.g. put all c:\Windows entries to the beginning and put all UNC ones to the end:
$newTmpdataArray = MoveToArrayBeginning $newTmpdataArray "c:\\windows"$newTmpdataArray = MoveToArrayEnd $newTmpdataArray "^\\\\" - always backup the original value!
| Script's output showing the original and the new PATH values and their length |
Functions
BackupPATHIt reads how many backup values exist already, increments the number and creates a new value with the current content of PATH
MoveToArrayBeginning
It runs through an array and adds items to a temp array. If an item matches a given pattern it adds the item to the beginning of the array otherwise it adds it to the end. The key piece is this, which adds the element to the array as either the first or last item:
$farray | %{
if($_ -imatch $pattern){
[Void]$newArray.Insert(0,$_)
}
else{
[Void]$newArray.add($_)
}
}
MoveToArrayEnd
It collects all items matching a pattern to a temp array and then removes and adds those to the end of the original array. It needs the temp array to preserve the original order of the items being moved to the end.
param ( [string] $hosts = "",
[string] $log = "",
[switch] $set = $false,
[switch] $rollback = $false,
[switch] $convertToSortName = $false,
[switch] $IknowWhatIamDoing = $false)
#### Function for creating/writing registry value remotely
function CreateRegValue ([string]$srv, [string]$value, [string]$newdata, [string]$key) {
$regKey = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine',$srv).OpenSubKey($key,$true).SetValue($value, $newdata,'string')
if(-not $?){
return $false
}
return $true
}
Function BackupPATH($srv, $data){
#backing up the original value
$sequenceNumber = 0
# create managePATHVariable subkey if doesn't exist
if(!([Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine',$srv).OpenSubKey('SOFTWARE\PATHBackup'))){
[Void][Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine',$srv).OpenSubKey('SOFTWARE',$true).CreateSubKey('SOFTWARE\PATHBackup')
}
else{
$backupValues = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine',$srv).OpenSubKey('SOFTWARE\PATHBackup').getvaluenames()
#get latest number sequence for backup value name
$latestBackupValueName = $backupValues | ?{$_ -imatch "^PATH_backup"} | sort -Descending | Select -first 1
[int]$sequenceNumber = [regex]::match($latestBackupValueName, "\d+$").value
$sequenceNumber++
}
$newBackupValueName = "PATH_backup_" + $sequenceNumber
# Backing up original value to HKLM\SOFTWARE\PATHBackup [$newBackupValueName]..." "nonew"
if(CreateRegValue $srv $newBackupValueName $data "SOFTWARE\PATHBackup"){
write-host "Backup OK"
}
else{
write-host "Backup error"
exit
}
}
#### Function for reading registry value remotely
function GetRegValue ([string]$srv, [string]$value, [string]$key) {
$regvalue = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine',$srv).OpenSubKey($key).GetValue($value)
return $regvalue
}
#### Function for moving array items to the beginning of the array
function MoveToArrayBeginning ($farray, $pattern ) {
$newArray = New-Object System.Collections.ArrayList
$farray | %{
if($_ -imatch $pattern){
[Void]$newArray.Insert(0,$_)
}
else{
[Void]$newArray.add($_)
}
}
return $newArray
}
#### Function for moving array items to the end of the array
function MoveToArrayEnd ($farray, $pattern) {
# we need a new .net array because the function will get a PS array (which doesn;t have .remove method)
$newArray = New-Object System.Collections.ArrayList
$itemsToRemove = New-Object System.Collections.ArrayList
[Void]$newArray.addRange($farray)
# collect all items that need to be moved to the end
$farray | %{
if($_ -imatch $pattern){
[Void]$itemsToRemove.add($_)
}
}
# go through the items need moving and remove/add them to the end of the original array
$itemsToRemove | %{
[Void]$newArray.remove($_)
[Void]$newArray.add($_)
}
return $newArray
}
$objColl = @()
$k = 1
#### Collate the host list.
$hostlist = @($Input)
if ($hosts) {
if($hosts -imatch " "){
$hostsArr = @($hosts.split(" "))
$hostlist += $hostsArr
}
else{
$hostlist += $hosts
}
}
$hostlistlength = ($hostlist | measure).count
if($hostlistlength -gt 0){
foreach ($hosts in $hostlist) {
$srv = $hosts
if($srv -ne ""){ # if the hostname is not empty
Write-Progress -activity "Performing PATH checks/changes" -Status "Processing host $k of $hostlistlength : $srv " -PercentComplete ($k/$hostlistlength * 100) -currentoperation "Checking if remote host is accessible..."
$oldlength = $data = $null
$tmpdataArray = $dataArray = $newTmpdataArray = @()
$sObject = "" | select ComputerName,OldValue,OldValueLength,NewValue,NewValueLength,Duplicates,Result
$sObject.ComputerName = $srv
$data = GetRegValue $srv "PATH" "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"
if($data){ # if reg data is not empty
$oldlength = $data.length
$sObject.OldValue = $data
$sObject.OldValueLength = $oldlength
$tmpdataArray = $data.split(";") # splitting string to array by ;
#remove last \ from each entry in $dataArray to make sure we pick up the duplicates which only differ in a \ at the end
foreach($item in $tmpdataArray){
if($item[-1] -eq "\"){$item = $item -replace "\\$",""}
$dataArray += $item
}
# 0. sort descending order
$newTmpdataArray = $dataArray | Sort-Object -Unique -Descending # building a new array without duplicate items
# record duplicate entries for listing them in the output
$duplicateEntries = @()
$testHashTable = @{}
$dataArray | foreach {$testHashTable["$_"] += 1}
$testHashTable.keys | where {$testHashTable["$_"] -gt 1} | foreach {
$duplicateEntries += $_
}
$sObject.Duplicates = [string]::Join(";",$duplicateEntries)
# re-add the array elemnets to a new array with array type: System.Collections.ArrayList, this supports elements removal and addition
$newdataArray = New-Object System.Collections.ArrayList
[void]$newdataArray.AddRange($newTmpdataArray)
# 1. put all c:\program files to the beginning
$newTmpdataArray = MoveToArrayBeginning $newTmpdataArray "C:\\progra~"
$newTmpdataArray = MoveToArrayBeginning $newTmpdataArray "C:\\program files"
# 2. put all c:\windows to the beginning
$newTmpdataArray = MoveToArrayBeginning $newTmpdataArray "c:\\windows"
# 3. put all %variabe% to the end
$newTmpdataArray = MoveToArrayEnd $newTmpdataArray "^%"
# 4. put all non-c: drives to the end
$newTmpdataArray = MoveToArrayEnd $newTmpdataArray "^[a-bd-z]:"
# 5. put all unc path to the end
$newTmpdataArray = MoveToArrayEnd $newTmpdataArray "^\\\\"
# converting array to string with separator ;
$newdata = [string]::join(";", $newTmpdataArray)
# check if the PATH contains Program files anr replace it with the 8.3 name
if($convertToSortName){
if ($data -imatch "program files") {
$newdata = $newdata -ireplace "\\program files\\", "\progra~1\" #trim the path by replacing "program files" with "progra~1"
$newdata = $newdata -ireplace "\\program files (x86)\\", "\progra~2\" #trim the path by replacing "program files (x86)" with "progra~2"
}
}
if(($newdata.Length -lt $data.Length) -or ($data -ine $newdata)){ # if the new string is shorter
if($set) {
BackupPATH $srv $data
#Writing new PATH value to HKLM\"SYSTEM\CurrentControlSet\Control\Session Manager\Environment" [$value]
CreateRegValue $srv "PATH" $newdata "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"
# Checking if new PATH value is set
$checkdata = GetRegValue $srv "PATH" "SYSTEM\CurrentControlSet\Control\Session Manager\Environment" # read it again to check an log the new registry data
$newlength = $checkdata.length
if($checkdata -ieq $newdata){
$sObject.Result = "OK"
}
else{
$sObject.Result = "Could not set new value"
}
}
else{
$checkdata = $newdata
$newlength = $checkdata.length
$sObject.Result = "PATH would be changed (use -set)"
}
$sObject.NewValue = $newdata
$sObject.NewValueLength = $newlength
}
else{
$sObject.Result = "No need to change PATH"
}
}
else{
$sObject.Result = "Could not get PATH from registry"
}
}
$objColl += $sObject
}
}
else{
write-host "No hostname or hostlist is specified."
}
$objColl
t
26 January, 2013
Edit MultiString registry value remotely - OS
The other day I needed to edit a RED_MULTI_SZ value on ~400 servers. You could say: it's an "everyday" task, why a blog post about it?
Indeed, it's no big deal, but I spent about an hr on one particular issue with this task that I wanted to share.
As I mentioned I had to edit a MultiString value (which is basically an array - in scripting world) and add a new element to it.
Let's start at the beginning. To write a String value to a remote registry you can use Microsoft.Win32.RegistryKey:
[Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine','REMOTEHOST').OpenSubKey("SOFTWARE\Microsoft\FTH",$true).SetValue('ExclusionList', $newdata,'string')
However, if you want to edit a REG_MULTI_SZ value, you need to read the content first, do whatever you want with it (search for item, remove, add...etc) and then write it back to the registry. In my case I had to add a new item to the beginning of the registry value:
$Array = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine','r2d2').OpenSubKey("SOFTWARE\Microsoft\FTH",$true).GetValue("ExclusionList")
$newArray = @("newitem")
$newarray += $array
[Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine','r2d2').OpenSubKey("SOFTWARE\Microsoft\FTH",$true).SetValue('ExclusionList', $newarray,'MultiString')
And it will throw an error:
Exception calling "SetValue" with "3" argument(s): "The type of the value object did not match the specified
RegistryValueKind or the object could not be properly converted."
At line:1 char:1
+ [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine','r2d2').OpenSubK ...
Why? The error message has the answer: the type of value object did not match...etc. Hah! Object?? What object? I've got strings in an array... let's see:
PS C:\> $newarray.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
Khm, ok, I forgot about PS being object oriented :) Let's force the array to have String type then:
[string[]]$newArray = @("newitem")
Now, let's double check its type:
PS C:\> $newArray.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True String System.Object
Awesome. As expected, the following line now works:
[Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine','r2d2').OpenSubKey("SOFTWARE\Microsoft\FTH",$true).SetValue('ExclusionList', $newarray,'MultiString')
May the Force...
t
Indeed, it's no big deal, but I spent about an hr on one particular issue with this task that I wanted to share.
As I mentioned I had to edit a MultiString value (which is basically an array - in scripting world) and add a new element to it.
Let's start at the beginning. To write a String value to a remote registry you can use Microsoft.Win32.RegistryKey:
[Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine','REMOTEHOST').OpenSubKey("SOFTWARE\Microsoft\FTH",$true).SetValue('ExclusionList', $newdata,'string')
However, if you want to edit a REG_MULTI_SZ value, you need to read the content first, do whatever you want with it (search for item, remove, add...etc) and then write it back to the registry. In my case I had to add a new item to the beginning of the registry value:
$Array = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine','r2d2').OpenSubKey("SOFTWARE\Microsoft\FTH",$true).GetValue("ExclusionList")
$newArray = @("newitem")
$newarray += $array
[Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine','r2d2').OpenSubKey("SOFTWARE\Microsoft\FTH",$true).SetValue('ExclusionList', $newarray,'MultiString')
And it will throw an error:
Exception calling "SetValue" with "3" argument(s): "The type of the value object did not match the specified
RegistryValueKind or the object could not be properly converted."
At line:1 char:1
+ [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine','r2d2').OpenSubK ...
Why? The error message has the answer: the type of value object did not match...etc. Hah! Object?? What object? I've got strings in an array... let's see:
PS C:\> $newarray.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
Khm, ok, I forgot about PS being object oriented :) Let's force the array to have String type then:
[string[]]$newArray = @("newitem")
Now, let's double check its type:
PS C:\> $newArray.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True String System.Object
Awesome. As expected, the following line now works:
[Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine','r2d2').OpenSubKey("SOFTWARE\Microsoft\FTH",$true).SetValue('ExclusionList', $newarray,'MultiString')
May the Force...
t
Subscribe to:
Comments (Atom)