PowerShell script to update RDMS server list

Remote Desktop Services in Windows Server 2012 is great in many ways, but it has some serious flaws. One of these is the pretty terrible so-called Remote Desktop Management Server concept. This is actually just some functionality built into Server Manager.

One of the most annoying things is that RDMS will fail to work if a server has been added to your Remote Desktop Deployment but you’ve not added it as a server to be managed via your own personal instance of Server Manager. This is quite a likely situation if you work in a team and/or have an automated server provisioning process. And it sucks. Instead of seeing your RD deployment, you just get a blank page with some text saying:

The following servers in this deployment are not part of the server pool:
SOMENEWRDSERVER.YOUR.FQDN
The servers must be added to the server pool.

No “Add now” button, no “continue anyway”, just a flat refusal to do anything at all.

So, Having recently done some work playing around with XML in PowerShell, and figuring that ServerManager probably stores its config in an XML file (though that wasn’t guaranteed – but luckily is true) I wrote a cmdlet that will update Server Manager to ensure that all the servers in your Remote Desktop deployment are added to Server Manager.

You’ll need to modify the code to specify your Connection Broker(s) (it copes with HA Connection Brokers). Save the code into a .psm1 file and then you can load this in your $profile script by using Import-Module <your-modules.psm1>.

function global:Update-RDMS {
    <#
        .Synopsis
        Ensure all RDS deployment servers are added to the Remote Desktop Management Server (Server Manager)

        .Description
        This cmdlet queries the Connection Broker and updates Server Manager to ensure all the RDS servers are added.
        Server Manager is killed if running, updated and restarted.

        .Example
        Update-RDMS
        Updates the RDMS

        .Notes
        RCM August 2015

        .Link
        http://www.rcmtech.co.uk/
    #>
    Begin{}
    Process{
        Write-Debug "Starting Update-RDMS"

        $ConnectionBrokers = "CBR01.rcmtech.co.uk","CBR02.rcmtech.co.uk"
        $ServerManagerXML = "$env:USERPROFILE\AppData\Roaming\Microsoft\Windows\ServerManager\Serverlist.xml"
        Write-Debug "Import RDS cmdlets"
        Import-Module RemoteDesktop
        Write-Debug "Find active Connection Broker"
        $ActiveManagementServer = $null
        foreach($Broker in $ConnectionBrokers){
            $ActiveManagementServer = (Get-ConnectionBrokerHighAvailability -ConnectionBroker $Broker).ActiveManagementServer
            if($ActiveManagementServer -eq $null){
                Write-Host "Unable to contact $Broker" -ForegroundColor Yellow
            } else {
                break
            }
        }
        if($ActiveManagementServer -eq $null){
            Write-Error "Unable to contact any Connection Broker"
        }else{
            if(Get-Process -Name ServerManager -ErrorAction SilentlyContinue){
                Write-Debug "Kill Server Manager"
                # Have to use tskill as stop-process gives an "Access Denied" with ServerManager
                Start-Process -FilePath "$env:systemroot\System32\tskill.exe" -ArgumentList "ServerManager"
            }
            Write-Debug "Get RD servers"
            $RDServers = Get-RDServer -ConnectionBroker $ActiveManagementServer
            Write-Debug "Get Server Manager XML"
            [XML]$SMXML = Get-Content -Path $ServerManagerXML
            foreach($RDServer in $RDServers){
                $Found = $false
                Write-Host ("Checking "+$RDServer.Server+" ") -NoNewline -ForegroundColor Gray
                foreach($Server in $SMXML.ServerList.ServerInfo){
                    if($RDServer.Server -eq $Server.name){
                        $Found = $true
                    }
                }
                if($Found -eq $true){
                    Write-Host "OK" -ForegroundColor Green
                }else{
                    Write-Host "Missing" -ForegroundColor Yellow
                    $NewServer = $SMXML.CreateElement("ServerInfo")
                    $SMXML.ServerList.AppendChild($NewServer) | Out-Null
                    $NewServer.SetAttribute("name",$RDServer.Server)
                    $NewServer.SetAttribute("status","1")
                    $NewServer.SetAttribute("lastUpdateTime",[string](Get-Date -Format s))
                    $NewServer.SetAttribute("locale","en-GB")
                }
            }
            # Remove xmlns attribute on any newly added servers, this is added automatically by PowerShell but causes Server Manager to reject the new server
            $SMXML = $SMXML.OuterXml.Replace(" xmlns=`"`"","")
            Write-Debug "Save XML file"
            $SMXML.Save($ServerManagerXML)
            Write-Debug "Start Server Manager"
            Start-Process -FilePath "$env:systemroot\System32\ServerManager.exe"
        }
    }
    End{}
}
Posted in PowerShell, Remote Desktop, Windows | Tagged , , , , , , , , , , , , , , , , | Leave a comment

Powershell RegEx Web Scraping

I wrote this as a learning process, and because I’d been wanting something that did this for a while anyway. Checking a website regularly is tedious, but getting an email when it shows new info I’m interested in is perfect.

The local police force publish the locations of safety cameras (speed cameras), including mobiles ones, once a week (though sometimes more towards the end of the week, which is less useful!). I wanted a way to be notified if any new camera sites were added, and also to know which of the possible mobile camera sites were going to be active in a particular week – but only be told of sites that I was interested in (i.e. those on my way to/from work). Not that I zoom around the place or anything, but it’s just nice to know!

The information is published on a web site, with the full list of camera locations split into separate pages for each area/county, plus a page that lists the mobile camera sites for the week.

The URL for the week’s active mobile sites changes when it is updated, so I’m retrieving that and then “following it” to get to the page for the current week. The current list URL ends in an index number which seems to increment when the page is updated, so I store this number in the registry so that I can compare it from one run of the script to the next, and only send notifications when it changes.

The full list of camera sites is exported to an XML file, and then compared with the web site each time the script subsequently runs to allow and changes to sites to be tracked and a notification sent. The XML is then updated and the process repeats. I’ve hard-coded the script to only bother checking certain areas/counties as I don’t need to be notified of new sites in places that I never go to. This would be easy to change though, see the array being filled around line 196, and note the number in the URL for the area you’re interested in.

The mobile sites that I’m interested in are specified in another XML file, along with my email address and SMTP (mail) server details.

The data on the web pages is formatted in tables and lists, so I am using regular expressions to find and extract it. This is a combination of removing matching text and retrieving matching text to get the page contents into a usable format in memory. This was the most interesting bit of the script to code, and trying to get the regex right in each case was “fun”. I found that regex101.com was helpful as the real-time highlighting allows you to quickly get things working. Note that PowerShell seems to use case-insensitive matching by default, so you’ll want to add an “i” into the “modifier” box to the right of the regular expression builder box.

I run the script every three hours via a scheduled task. I’ve yet to find a way to make PowerShell run hidden, but you can make it disappear quickly and keep running in the background with the -WindowStyle Hidden command line argument, which is good enough for me for the moment. I’ve wondered if the old utility runh.exe would still work, but not got around to testing this yet. Please comment if you have a way to do this. When creating the scheduled task, the program/script is powershell.exe and the argument is:

-WindowStyle Hidden <full path to the script.ps1>

The config XML file looks like this, the formatting is a bit weird because I used Export-Clixml to create it in the first place, deal with it (!):

<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
 <Obj RefId="0">
  <TN RefId="0">
   <T>System.Object</T>
  </TN>
  <ToString>System.Object</ToString>
  <MS>
   <S N="EmailAddress">me@mysite.co.uk</S>
   <S N="SMTPServer">smtp.mysite.co.uk</S>
   <S N="CamList">0109,0164,0160,0061,0057,0156,0195,0054,0021,0065,0066</S>
  </MS>
 </Obj>
</Objs>

You need to change it to your own settings. The script looks for the two XML files in whatever location you choose to run it from, I’ve got it sat in Documents\SafetyCam but you can put it anywhere you have read & write access to. Here’s the script:

# URI to the page containing the link to the page that has the list of this week's mobile camera locations
$NewsPageURI = "http://www.safecam.org.uk/News/index.aspx"
# URI to the full list of camera sites
$AllLocationsURI = "http://www.safecam.org.uk/CameraSites/camera_sites_map.aspx"
# URI stub of individual council sites list
$LocationsURIStub = "http://www.safecam.org.uk/CameraSites/CameraList.aspx?d="
# All known camera site list XML file
$SitesFile = Join-Path -Path $PSScriptRoot -ChildPath "SafetyCameraSites.XML"
# Config file
$ConfigFile = Join-Path -Path $PSScriptRoot -ChildPath "SafetyCameraConfig.xml"
# Registry key for tracking active sites and interested site changes
$RegKey = "HKCU:\Software\SafetyCam"
# Set up a CRLF string
$CRLF = "`r`n"

#############################################
################# Functions #################
#############################################
function Send-Error($ErrorText){
    Send-MailMessage -From "SafetyCameraScript@uwe.ac.uk" -To $MyEmailAddress -SmtpServer $SMTPServer -Subject "Error: $ErrorText" -Body $ErrorText
    Write-Host "Error: $ErrorText" -ForegroundColor Red
    exit
}
function Get-CameraTextFromHTML([string]$CamType){
    # $CamType should be "fixed", "mobile" or "red light"
    $CamText = ""
    if($PageHTML -match "<b>$CamType(.)*?<td class(.)*?absmiddle>(.)*?<\/tbody>"){
        # Found the matching fixed camera text pattern
        $CamText = $Matches[0]
        # Get rid of the table header stuff before the camera list
        $CamText = $CamText -replace '<B>(.)*?valign(.)*?"50%">',""
        # Get rid of the table footer stuff after the camera list
        $CamText = $CamText -replace '<\/td><\/tr><\/tbody>$',""
        # Get rid of the bit separating the two table colums
        $CamText = $CamText -replace '<\/td>(.)*?"50%">',""
        # Get rid of the HTML at the start of each row of text
        $CamText = $CamText -replace '<p>(.)*?absmiddle>',""
        # Tidy up any extraneous entries
        $CamText = $CamText -replace '<p>&nbsp;<\/p>',""
        $CamText = $CamText -replace '&gt;',">"
        # Get rid of the HTML at the end of each row of text and replace with a delimiter of our own
        $CamText = $CamText -replace '<\/a>(.)*?<\/p>',"!"
        # But not after the very last line
        $CamText = $CamText -replace '!$',""
    }elseif($PageHTML -match "<b>$CamType(.)*?<p>there are currently no operational $camtype cameras in this council area<\/p>"){
        # No cameras of this type found, tidy up text string
        $CamText = $Matches[0]
        $CamText = $CamText -replace '<b>(.)*?<p>',""
        $CamText = $CamText -replace '<\/p>',""
    }else{
        $CamText = "Unable to parse HTML"
    }
    $CamText
}
function Get-CamList([string]$CamURINumber){
    $Page = Invoke-WebRequest -Uri "$LocationsURIStub$CamURINumber"
    # Get the HTML content of the page, and strip out all CRLF to make it easier to use regular expressions
    $PageHTML = $Page.ParsedHtml.body.innerHTML.Replace("`r`n","")
    if($PageHTML -match ">[\w\s]+ camera information<"){
        $CouncilName = $Matches.Item(0)
        $CouncilName = $CouncilName -ireplace ">",""
        $CouncilName = $CouncilName -ireplace " camera information<",""
        $CouncilName = $CouncilName.replace("`r`n","")
    }
    # Get the fixed camera locations
    $Fixed = Get-CameraTextFromHTML -CamType "fixed"
    $Mobile = Get-CameraTextFromHTML -CamType "mobile"
    $RedLight = Get-CameraTextFromHTML -CamType "red light"
    # Split the locations into arrays
    $FixedArray = $Fixed.Split("!")
    $MobileArray = $Mobile.Split("!")
    $RedLightArray = $RedLight.Split("!")
    # Add each camera in each array into a Cam object and add these objects into a master CamList array
    $CamList = @()
    for ($i = 0; $i -lt $FixedArray.Count; $i++)
    { 
        $Cam = New-Object -TypeName System.Object
        Add-Member -InputObject $Cam -MemberType NoteProperty -Name "CamURINumber" -Value $CamURINumber
        Add-Member -InputObject $Cam -MemberType NoteProperty -Name "CouncilName" -Value $CouncilName
        Add-Member -InputObject $Cam -MemberType NoteProperty -Name "CamType" -Value "Fixed"
        Add-Member -InputObject $Cam -MemberType NoteProperty -Name "CamInfo" -Value $FixedArray[$i]
        $CamList += $Cam
    }
    for ($i = 0; $i -lt $MobileArray.Count; $i++)
    { 
        $Cam = New-Object -TypeName System.Object
        Add-Member -InputObject $Cam -MemberType NoteProperty -Name "CamURINumber" -Value $CamURINumber
        Add-Member -InputObject $Cam -MemberType NoteProperty -Name "CouncilName" -Value $CouncilName
        Add-Member -InputObject $Cam -MemberType NoteProperty -Name "CamType" -Value "Mobile"
        Add-Member -InputObject $Cam -MemberType NoteProperty -Name "CamInfo" -Value $MobileArray[$i]
        $CamList += $Cam
    }
    for ($i = 0; $i -lt $RedLightArray.Count; $i++)
    { 
        $Cam = New-Object -TypeName System.Object
        Add-Member -InputObject $Cam -MemberType NoteProperty -Name "CamURINumber" -Value $CamURINumber
        Add-Member -InputObject $Cam -MemberType NoteProperty -Name "CouncilName" -Value $CouncilName
        Add-Member -InputObject $Cam -MemberType NoteProperty -Name "CamType" -Value "RedLight"
        Add-Member -InputObject $Cam -MemberType NoteProperty -Name "CamInfo" -Value $RedLightArray[$i]
        $CamList += $Cam
    }
    # Reurn the CamList object
    $CamList
}

############################################
#################   Main   #################
############################################
# Read in config info from file
try{
    $Config = Import-Clixml -Path $ConfigFile
    $MyEmailAddress = $Config.EmailAddress
    $SMTPServer = $Config.SMTPServer
    $MySiteList = $Config.CamList
    $MySites = $MySiteList.Split(",")
}catch{
    Send-Error "Error: problem with config file"
}
Write-Host ("Email address   : "+$MyEmailAddress)
Write-Host ("SMTP server     : "+$SMTPServer)
Write-Host ("Monitored sites : "+$Config.CamList)
# Get the current locations page from the main news page
try{
    $NewsPage = Invoke-WebRequest -Uri $NewsPageURI
}catch{
    Send-Error "Error: Failed to retrieve main news page"
}
# Get the URL for this week's locations page
$NewsHref = ""
foreach($Link in $NewsPage.Links){
    if($Link.innerText -match "mobile speed camera enforcement schedule for week commencing"){
        $NewsHref = $Link.href
    }
}
if($NewsHref -eq ""){
    Send-Error "Error: Unable to get current locations URI from news page"
}
# Build the URI to the current locations page
$CurrentLocationsURI = $NewsPageURI.Replace("index.aspx",$NewsHref)
$NewsID = $NewsHref -ireplace "viewnews.aspx\?newsid=",""
# Set up a flag to determine whether to send email
$ActiveSiteChanges = $true
# Get previous config data from registry, this allows us to check for changes to NewsID (different active mobile sites list)
# or changes to the number of sites we're interested in reporting on.
if(Test-Path $RegKey){
    # Registry key exists, check NewsID value
    if(((Get-ItemProperty -Path $RegKey -Name "NewsID" -ErrorAction SilentlyContinue)."NewsID" -ne $NewsID) -or ((Get-ItemProperty -Path $RegKey -Name "MySiteList" -ErrorAction SilentlyContinue)."MySiteList" -ne $MySiteList)){
        # NewsID or MySiteList has changed, update them and run full script
        New-ItemProperty -Path $RegKey -Name "NewsID" -Value $NewsID -Force | Out-Null
        New-ItemProperty -Path $RegKey -Name "MySiteList" -Value $MySiteList -Force | Out-Null
        # Get the active locations
        try{
            $CurrentLocations = (Invoke-WebRequest -Uri $CurrentLocationsURI).Content
        }catch{
            Send-Error "Error: Failed to retrieve current locations page"
        }
        # Check that the current locations page contains the expected date header text
        if(($CurrentLocations -match ">[\s\w]*week commencing[\s\w]*.<") -eq $false){
            Send-Error "Error: Current Locations page did not contain expected date header text"
        }
        # Check for where mobile cameras are going to be located
        [string]$ActiveLocations = ""
        foreach($Site in $MySites){
            if($CurrentLocations -match $Site){
                $CurrentLocations -match ">[\w\s(),\/]*: "+$Site+"<" | Out-Null # matches location line, including "," and "/"
                $Location = $Matches.Item(0)
                $Location = $Location.Replace(">","")
                $Location = $Location.Replace("<","")
                $ActiveLocations += "Camera at "+$Location+$CRLF
            }
        }
        if($ActiveLocations -eq "" -and $CurrentLocations.Length -gt 0){
             $ActiveLocations = "No active locations found that you are interested in this week"
        }
    }else{
        # NewsID has not changed since last run of this script, camera list is probably unchanged
        $ActiveSiteChanges = $false
    }
}else{
    # Registry key does not exist, create it, then set the NewsID value and run full script
    New-Item -Path $RegKey | Out-Null
    New-ItemProperty -Path $RegKey -Name "NewsID" -Value $NewsID | Out-Null
    New-ItemProperty -Path $RegKey -Name "MySiteList" -Value $MySiteList | Out-Null
}
# Check for changes to numbers of camera sites
# Read in all known camera sites from HTML pages
$AllCams = @()
$AllCams += Get-CamList -CamURINumber 5 # North Somerset
$AllCams += Get-CamList -CamURINumber 7 # Bristol City Council
$AllCams += Get-CamList -CamURINumber 8 # South Gloucstershire
if(Test-Path $SitesFile){
    # Read in known camera sites from file
    try{
        $PreviousCams = Import-Clixml -Path $SitesFile
    }catch{
        Send-Error "Reading sites file"
    }
}else{
    # No config file present, can't compare this time. Write sites list to config file for use next time.
    try{
        $AllCams | Export-Clixml -Path $SitesFile
    }catch{
        Send-Error "Writing sites file"
    }
}
# Extract just the camera info (location and site code) so that we can search using -contains
# ... for current list
$CamInfo = @()
foreach($Cam in $AllCams){
    $CamInfo += $Cam.CamInfo
}
# ... for previous list loaded from file
$PreviousCamInfo = @()
foreach($PCam in $PreviousCams){
    $PreviousCamInfo += $PCam.CamInfo
}
# Check each current camera site to see if it exists in the previous list
$NewCams = @()
    foreach($Cam in $CamInfo){
    if($PreviousCamInfo -contains $Cam){
        # Cam is already known
    }else{
        $NewCams += $Cam
    }
}
# Check each previous camera site to see if still valid
$RemovedCams = @()
foreach($Cam in $PreviousCamInfo){
if($CamInfo -contains $Cam){
        # Cam is still there
    }else{
        $RemovedCams += $Cam
    }
}
$CamSiteChanges = $true
$CamSiteUpdates = ""
if($NewCams.Count -ne 0 -or $RemovedCams.Count -ne 0){
    foreach($NewCam in $NewCams){
        $CamSiteUpdates += "New site: $NewCam$CRLF"
    }
    foreach($RemovedCam in $RemovedCams){
        $CamSiteUpdates += "Removed site: $RemovedCam$CRLF"
    }
    # Overwrite the XML file with the latest camera list
    $AllCams | Export-Clixml -Path $SitesFile -Force
}else{
    $CamSiteUpdates = "No changes to camera sites"
    $CamSiteChanges = $false
}
$CamSiteUpdates = $CamSiteUpdates | Sort-Object
if($ActiveSiteChanges -or $CamSiteChanges){
    # Build up email body text
    $DateHeader = ("Current Safety Camera Locations as at "+(Get-Date -Format s))
    $Body = $DateHeader+$CRLF+$CRLF
    $Body += $ActiveLocations+$CRLF
    $Body += [string]$AllCams.Count+" locations searched for "+[string]$MySites.Count+" cameras"+$CRLF+$CRLF
    $Body += $CamSiteUpdates+$CRLF+$CRLF
    $Body += "Locations this week: "+$CurrentLocationsURI+$CRLF
    $Body += "All known locations: "+$AllLocationsURI+$CRLF+$CRLF
    Write-Host $Body -ForegroundColor Cyan
    $BodyHTML = $Body -replace "`r`n","
"
    Send-MailMessage -BodyAsHtml ('<font face="Calibri">'+$BodyHTML+'</font>') -Subject $DateHeader -To $MyEmailAddress -From "SafetyCameraScript@uwe.ac.uk" -SmtpServer $SMTPServer
}else{
    Write-Host "No changes to active cams or site list" -ForegroundColor Yellow
}

Obviously, if you use this as-is, and get zapped by a camera, that is your fault for breaking the law, not the fault of me or my script!! If your local police force publishes camera info in a different way you’ll have a nice coding exercise on your hands. I’m hoping that this post will be a good reference for web scraping and regular expressions in PowerShell.

Posted in PowerShell | Tagged , , , , , , , , , , , , , , , | Leave a comment

Experts Exchange Windows Server 2012 Master

Been working on achieving this for a little under a month after not doing any Experts Exchange stuff for ages. I am also now a Wizard overall. Hurrah.

Answering questions on EE is a good learning experience – you get to see the problems that people are having with certain technologies, which can greatly assist you if you come to do something similar in the future. It also increases your breadth of knowledge, which can only be a good thing.

Posted in Business, Free training, Windows | Tagged , , , | Leave a comment

Remove Forefront Client Security by force

I was recently trying to upgrade the Antivirus software on some servers from Forefront Client Security to System Center Endpoint Protection 2012 R2. On most servers it worked fine. However on a few I was unable to remove some of the FCS components due to missing .msi files. This was because somebody (not me!) had been deleting the contents of the C:\Windows\Installer folder, probably to save disk space.

This meant that the uninstall command was failing as Windows Installer couldn’t find the right .msi file. This in turn meant I was unable to install SCEP as its installer check to see that all previous have gone before it’ll install.

There is a very faffy way of fixing missing MSIs where you have to track down the correct version of the .msi via the Microsoft Update Catalogue and/or a WSUS server, but it was too fiddly and I didn’t have time. Thus, I wrote a script to manually uninstall the old FCS stuff (or at least, enough of it that SCEP will install and be happy).

The script will continue on errors, and you will get errors as some of the stuff only relates to 32-bit OS, and some only to 64-bit. Plus you might have already been able to remove some of the FCS components properly via Windows Installer.

Here’s the script, save it as a .cmd file and run as administrator.

@echo off
echo Stop services
net stop MOM
net stop FCSAM
net stop FcsSas

echo Delete services
sc delete MOM
sc delete FCSAM
sc delete FcsSas

echo Kill GUI
tskill msascui /a

echo Delete files
rd /s /q "C:\Program Files\Microsoft Forefront"
rd /s /q "C:\Program Files (x86)\Microsoft Forefront"

Echo Remove registry keys
echo ...MOM
rem 64-bit
reg delete "HKLM\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{F692770D-0E27-4D3F-8386-F04C6F434040}" /f
reg delete "HKLM\SOFTWARE\Wow6432Node\Microsoft\Microsoft Operations Manager\2.0" /f
rem 32-bit
reg delete "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{F692770D-0E27-4D3F-8386-F04C6F434040}" /f
rem both
reg delete "HKLM\SOFTWARE\Classes\Installer\Products\D077296F72E0F3D438680FC4F6340404" /f
reg delete "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products\D077296F72E0F3D438680FC4F6340404" /f

echo ...SAS
rem 64-bit
reg delete "HKLM\SOFTWARE\Classes\Installer\Products\E4EB3435742B0D148BD1E4C755649001" /f
reg delete "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products\E4EB3435742B0D148BD1E4C755649001" /f
reg delete "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{5343BE4E-B247-41D0-B81D-4E7C55460910}" /f
rem 32-bit
reg delete "HKLM\SOFTWARE\Classes\Installer\Products\838A5BA2CAD95F54E82C10D9DD4C4B6F" /f
reg delete "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products\838A5BA2CAD95F54E82C10D9DD4C4B6F" /f
reg delete "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{2AB5A838-9DAC-45F5-8EC2-019DDDC4B4F6}" /f

echo ...FCS
rem 64-bit
reg delete "HKLM\SOFTWARE\Classes\Installer\Products\EE98922AA7EA8F240A0CC999FC6B44BF" /f
reg delete "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products\EE98922AA7EA8F240A0CC999FC6B44BF" /f
reg delete "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{A22989EE-AE7A-42F8-A0C0-9C99CFB644FB}" /f
rem 32-bit
reg delete "HKLM\SOFTWARE\Classes\Installer\Products\FF0CF4D4791FF10448E21E811F2D46E7" /f
reg delete "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products\FF0CF4D4791FF10448E21E811F2D46E7" /f
reg delete "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{4D4FC0FF-F197-401F-842E-E118F1D2647E}" /f
rem both
reg delete "HKLM\SOFTWARE\Wow6432Node\Microsoft\Microsoft Forefront" /f
reg delete "HKLM\SOFTWARE\Microsoft\Microsoft Forefront" /f

echo Done.
pause

Use with care.

Posted in Scripting, Security, Windows | Tagged , , , , , , , , , , , , , , , , | Leave a comment

PowerShell System Drive Clean-up

I’ve been working through a process of cleaning and optimising the C drives on a lot of VMs recently. Over time they build up with lots of junk that, when put together with hundreds of VMs, requires a significant amount of extra primary VM storage, backup storage, and increases backup times. These files also take up space within the VM that can cause security and anti-malware updates to fail, and lead to application or logging failures. The reduced free space also leads to increased file fragmentation, which then means more IOs to and from your storage.

So the first step in my process is to run this script, which deletes various unnecessary bits and pieces from the Windows folder and user profiles, and empties recycle bins and deletes various other folders that some of my colleagues have a tendency to put onto the C drive (e.g. 5GB of SQL Server install media, which is available for them on the network).

It copes with various flavours of OS, I’ve been using it on 2003 through 2008 R2 so far. You can add paths to the array at the beginning. For paths within the Users (or Documents and Settings) folder it will search through each user profile, excluding the local administrator or any service accounts (which in my organisation all contain “svc” in the name.

I’m toying with the idea of removing the $hf_mig$ folder on 2003 now that there will be no more security updates, likewise SoftwareDistribution, but I’ve not fully looked into the implications of this yet. There’s probably lots of other places where stuff can be safely deleted from too. Another one I’ve just thought of would be *.log from the Windows folder.

The wildcards rely on the handling within the Remove-Item cmdlet, so if you’re not sure what it’ll do, it’s easy to try it manually on a test folder first.

DO NOT run this without understanding what it does, and knowing what data your applications store where, and how your users work. I AM NOT responsible if you run this and trash your server(s)! Look very carefully at the paths that I’m using and see if each one is something you feel happy with or not. That said, it hasn’t caused me any problems yet – I’ve run it on about 30 different servers so far.

Usage is:
TidyCDrive.ps1 <server name>

param([parameter(mandatory = $true)][string]$Target)

$PathsToClean = New-Object -TypeName System.Collections.ArrayList
$PathsToClean.Add('\Windows\System32\Config\SystemProfile\Local Settings\Temp\*') | Out-Null
$PathsToClean.Add('\Windows\Temp\*') | Out-Null
$PathsToClean.Add('\Windows\ie8updates') | Out-Null
$PathsToClean.Add('\Windows\ie8') | Out-Null
$PathsToClean.Add('\Windows\ie7') | Out-Null
$PathsToClean.Add('\Windows\ie7updates') | Out-Null
$PathsToClean.Add('\Windows\ServicePackFiles') | Out-Null
$PathsToClean.Add('\Windows\$NtUninstall*') | Out-Null
$PathsToClean.Add('\Windows\$NtServicePackUninstall*') | Out-Null
$PathsToClean.Add('\Windows\$950099Uinstall*') | Out-Null
$PathsToClean.Add('\Windows\$UninstallRDC$') | Out-Null
$PathsToClean.Add('\Users\xxxxx\AppData\Local\Microsoft\Windows\Temporary Internet Files') | Out-Null
$PathsToClean.Add('\Users\xxxxx\AppData\Local\Microsoft\Windows\Explorer\thumbcache*.db') | Out-Null
$PathsToClean.Add('\Users\xxxxx\AppData\Local\Microsoft\Windows\Terminal Server Client\Cache') | Out-Null
$PathsToClean.Add('\Users\xxxxx\AppData\Local\Temp\*') | Out-Null
$PathsToClean.Add('\Program Files\Veritas\Patch') | Out-Null
$PathsToClean.Add('\wuinstall') | Out-Null
$PathsToClean.Add('\$Recycle.bin\*\*') | Out-Null
$PathsToClean.Add('\Recycler\*') | Out-Null
$PathsToClean.Add('\SQL2008R2Ent') | Out-Null
$PathsToClean.Add('\SQLserver 2008 R2 SP2') | Out-Null
$PathsToClean.Add('\Security Update') | Out-Null

Write-Host ("Disk Clean & optimise for "+$Target) -ForegroundColor Gray
# record initial C drive free space
$WMIDisk = Get-WmiObject -Class Win32_LogicalDisk -ComputerName $Target -Filter "DeviceID='C:'" -ErrorAction Stop | Select-Object -Property Size,FreeSpace
[single]$CFreeSpaceBefore = $WMIDisk.FreeSpace/1GB
Write-Host ("Free space: "+$CFreeSpaceBefore)
# clean C drive based on known paths
foreach($Path in $PathsToClean){
    Write-Host ("Cleaning $Path")
    $UNCPath = ("\\"+$Target+"\C$"+$Path)
    if($UNCPath -like "*\users\xxxxx\*"){
        if(Test-Path -Path ("\\"+$Target+"\C$\Users")){
            $ProfilesFolder = ("\\"+$Target+"\C$\Users")
        }else{
            $ProfilesFolder = ("\\"+$Target+"\C$\Documents and Settings")
            $UNCPath = $UNCPath.Replace("\Users\","\Documents and Settings\")
        }
        $LocalProfiles = Get-ChildItem -Path $ProfilesFolder -Directory | Where-Object -Property Name -Like *admin* | Where-Object -Property Name -notlike "Administrator" | Where-Object -Property Name -notlike "*svc*"
        foreach($LocalProfile in $LocalProfiles){
            $UNCProfilePath = $UNCPath.Replace("xxxxx",$LocalProfile.Name)
            Write-Host $UNCProfilePath -ForegroundColor DarkGray
            Remove-Item -Path $UNCProfilePath -Recurse -Force -ErrorAction SilentlyContinue
        }
    }else{
        Write-Host $UNCPath -ForegroundColor DarkGray
        Remove-Item -Path $UNCPath -Recurse -Force -ErrorAction SilentlyContinue
    }
}
# Check size of SoftwareDistribution folder isn't too large
$SD = (Get-ChildItem -Path "\\$Target\C$\Windows\SoftwareDistribution" -Recurse -Force | Measure-Object -Property Length -Sum).Sum/1MB
if($SD -gt 900){
    Write-Warning "SoftwareDistribution is a little large"
}
$WMIDisk = Get-WmiObject -Class Win32_LogicalDisk -ComputerName $Target -Filter "DeviceID='C:'" -ErrorAction Stop | Select-Object -Property Size,FreeSpace
[single]$CFreeSpaceAfter = $WMIDisk.FreeSpace/1GB
Write-Host ("Free space: "+$CFreeSpaceAfter)
$CFreedUpGB = $CFreeSpaceAfter - $CFreeSpaceBefore
Write-Host ("Freed up {0:N2}GB within C drive" -f $CFreedUpGB) -ForegroundColor Green
Posted in Performance, PowerShell, Windows | Tagged , , , , , , , , , , , , , , | Leave a comment

PowerShell: Monitor changes to a folder

This uses a FileSystemWatcher object and its WaitForChanged method, which is rather nifty way to not use much resource to keep track of file system changes. What we’re NOT doing here is polling the folder contents.

I’m then trying a couple of different methods to try and work out, if possible, the user responsible for the changes. This doesn’t always work, but it’s not too bad. You’ll get better info about the user if the folder is being accessed remotely via an SMB share thanks to the Get-SmbOpenFile cmdlet, otherwise I try and get the file owner, or don’t bother at all. Get-SmbOpenFile needs elevated privileges so you’ll need to run this “as administrator”.

$Folder = ’D:\TD2’
$FileSystemWatcher = New-Object System.IO.FileSystemWatcher $Folder
while ($true) {
  $Change = $FileSystemWatcher.WaitForChanged(‘All’, 1000)
  if ($Change.TimedOut -eq $false){
    [string]$User = ""
    $FilePath = Join-Path -Path $Folder -ChildPath $Change.Name
    if(Test-Path -Path $FilePath){
      $SMBUserArray = Get-SmbOpenFile -IncludeHidden | Where-Object -Property Path -Like $FilePath
      if($SMBUserArray.Count -ge 1){
        $User = "SMB: "+$SMBUserArray[0].ClientUserName+" "
      }else{
        try{
          $User = "Owner: "+(Get-Acl -Path $FilePath -ErrorAction Stop).Owner+" "
        }catch{}
      }
    }
    Write-host ($User+$Result.ChangeType+": "+$Result.Name)
  }
}

Press Ctrl-C to abort when you’re done monitoring.

Posted in PowerShell, Security | Tagged , , , , , , , , , , , , , , | Leave a comment

Run Disk Cleanup – save GB of space

I’m a big fan of the built-in Windows Disk Cleanup utility, cleanmgr.exe. Since the extra features added to the Vista & Server 2008 version it has become a great way to recover sometimes quite substantial amounts of space from your system (OS) drive though its Windows Update Cleanup feature.

Here’s an example run on one of my Server 2008 R2 VMs:
cleanmgr 2008 r2
Nearly 8GB back? Thank you very much! Here’s another:

disk cleanup 12GB
I’ll get nearly 12GB back on this server, installed in 2010.

If the Windows Update Cleanup option doesn’t show its because no space can be freed by that method (or you ran it once with that option and haven’t rebooted yet, see below). Or you didn’t run it “as administrator”.

The downside is that whilst you can automate it, it is a little fiddly. Also whilst it is present on all 2008+ servers, it isn’t made available unless you enable Desktop Experience. That’s overkill just for running this utility though, but luckily you can just copy the required (two) files from winsxs and into the right places. Personally, I have a small .cmd file that copies the two files and then runs cleanmgr.exe.

Note that on a desktop OS such as Windows 7 or 8.x Disk Cleanup is always present. I recovered about 5GB on my Windows 7 laptop.

Disk Cleanup is actually a very powerful utility, hidden behind a fairly basic GUI. You can write your own custom cleanup tasks, either using the built-in DataDrivenCleaner COM object, or by writing you own (go on, you know you want to!). The DataDrivenCleaner can be used to search for (e.g.) files with a specific file extension in a certain folder that are older than a certain number of days.

Note that on Windows 7 and 2008 R2 (and probably Vista & Server 2008 too) you have to manually reboot after doing the Windows Update Cleanup, you won’t be prompted.

As the computer is shutting down, you then get a “Configuring Windows Updates” briefly, then as it starts back up you’ll get it again, and it might sit at 100% for a while:
cleanmgr cleanup 1

Then it’ll reboot again (automatically), and on the way up will sit at “Cleaning up.”:
cleanmgr cleanup 2

This might take a while too. The whole process could take half an hour or more, depending on how fast your CPU and disk are and how much stuff it found to tidy up. Once it finishes you’ll have the space back.

Posted in Windows | Tagged , , , , , , , | Leave a comment