Silent patching and controlled reboots

Patching silently and installing applications without any user interaction… This is really hard to do and it’s really hard to get compliant machines if you do not force a reboot every now and then.
Then, if you force the user to reboot with a short timeout they will not be to happy.
So… to solve this I took some inspiration from a couple of blogposts some scripts and then some of my knowledge and put together a recipe that I think work’s really nice. 🙂

All in all, this results in a nice popup (only when a reboot is needed) that gives the user a good amount of time to decide when to reboot.
With this in place you can push out updates and applications silently and then just sit back and wait for the users to reboot when they want to.

So, this is what you need to do…

Copy the following files to %ProgramFiles%RebootIfNeeded

Create a Scheduled Task that runs once or twice every day (I have it set at 08:00 and 13:00 every weekday), and on that task create an with the following configuration:
Program: %ProgramFiles%RebootIfNeededhstart64.exe
Arguments:

/NOCONSOLE /WAIT ""%SystemRoot%system32WindowsPowerShellv1.0powershell.exe" -NoLogo -NoProfile -NonInteractive -File "%ProgramFiles%RebootIfNeededRebootIfNeeded.ps1""

(Tip: If using GPP, set item level targeting to check that the PS1-file exist before creating the task)

The script will check for pending reboots and if the computer havn’t rebooted for XX days.
You can add support for more languages, just extend the hashtable $restartDescriptions

[CmdletBinding()]
PARAM (
    $maxBootAgeDays = 35,
    $restartTimeOut = (9 * 60), # 9 hours
    $restartMaxPostpone = (48 * 60), # 48 hours
    $restartDescriptions = @{
        "en-US" = "Your computer needs to restart to receive the latest updates.";
        "sv-SE" = "Din dator behöver startas om för att få de senaste uppdateringarna.";
    },
    $defaultLanguage = "en-US"
)


Function Get-PendingReboot {
	# Local HKLM
	$HKLM = [UInt32] "0x80000002"
	$wmiRegistry = [WMIClass] "\.rootdefault:StdRegProv"

	#Default
    $PendingReboot = $false

	# CBS - Reboot Required ?
	$RegSubKeysCBS = $wmiRegistry.EnumKey($HKLM,"SOFTWAREMicrosoftWindowsCurrentVersionComponent Based Servicing")
	if ($RegSubKeysCBS.sNames -contains "RebootPending") {
        Write-Verbose "Component Based Servicing have a reboot pending"
		$PendingReboot = $true
    }
							
	# Windows Update - Reboot Required?
	$RegistryWUAU = $wmiRegistry.EnumKey($HKLM,"SOFTWAREMicrosoftWindowsCurrentVersionWindowsUpdateAuto Update")
	if ($RegistryWUAU.sNames -contains "RebootRequired") {
        Write-Verbose "Windows Update have a reboot required"
		$PendingReboot = $true
    }

	## Pending FileRenameOperations ?
	$RegSubKeySM = $wmiRegistry.GetMultiStringValue($HKLM,"SYSTEMCurrentControlSetControlSession Manager","PendingFileRenameOperations")
	If ($RegSubKeySM.sValue) {
        $RegSubKeySM.sValue | ForEach-Object { 
        	If ($_.Trim() -ne '') {
        		Write-Verbose "Pending FileRename operation: $($_)"
        	}
        }
		$PendingReboot = $true
    }

	# ConfigMgr - Pending reboot ?
	TRY {
	    $CCMClientSDK = Invoke-WmiMethod -NameSpace "ROOTccmClientSDK" -Class "CCM_ClientUtilities" -Name "DetermineIfRebootPending" -ErrorAction Stop

		If ($CCMClientSDK.IsHardRebootPending -or $CCMClientSDK.RebootPending) {
            Write-Verbose "ConfigMgr have reboot pending"
            $PendingReboot = $true
        }
	} CATCH {
        Write-Verbose "Cant talk to ConfigMgr Agent"
    }

    Write-Verbose "Pending reboot: $($PendingReboot)"
    Return $PendingReboot
}

Function Check-OldBootAge {
    PARAM (
        $maxAgeDays = 35
    )

    $BootTime = Get-WmiObject  Win32_Operatingsystem
    [Int]$days = (New-TimeSpan -Start $boottime.ConvertToDateTime($boottime.LastBootUpTime) -End (Get-Date)).TotalDays

    if ($days -ge $maxAgeDays) {
        Write-Verbose "Boot age is $($days) days (more than $($maxBootAgeDays)), reboot required"
        Return $true
    } else {
        Write-Verbose "Boot age is $($days) days (less than $($maxBootAgeDays))"
        Return $false
    }

    Return $days
}

Function Get-UserLanguage {
    Return [String] ([System.Threading.Thread]::CurrentThread).CurrentUICulture.Name
}

# ------------------------------------------------------------------------------------------------------------
# Main script

if ( (Get-WmiObject -Query "SELECT ProductType FROM Win32_OperatingSystem").ProductType -eq 1) {

    If ( (Get-Process "ShutdownTool" -ErrorAction SilentlyContinue) ) {
        Write-Host "Already running ShutdownTool"
    } else {
        If ((Check-OldBootAge -maxAgeDays $maxBootAgeDays) -or (Get-PendingReboot)) {
            Write-Host "Reboot is required, calling restart utility"

            $userLanguage = Get-UserLanguage
            Write-Verbose "Language: $($userLanguage)"

            # Find description
            $Description = $restartDescriptions[$userLanguage]
            if ($Description -eq $null) {
                $Description = $restartDescriptions[$defaultLanguage]
            }

            $timeOutSeconds = ($restartTimeOut*60) - 1

            Write-Verbose "Restart timeout: $($timeOutSeconds) seconds"
            Write-Verbose "Max postpone: $($restartMaxPostpone) minutes"
            Write-Verbose "Description: $($Description)"


            If ((Test-Path ".ShutdownTool.exe") -eq $false) {
                Throw "Cant find ShutdownTool.exe"
            } else {
                Write-Verbose "Calling restart with ShutdownTool"
                .ShutdownTool.exe /g:$userLanguage /d:"$Description" /t:$timeOutSeconds /m:$restartMaxPostpone /r /c
            }

            # /g - Language
            # /d - description
            # /t - countdown in sec
            # /m - max postpone in min
            # /r - reboot instead of shutdown
            # /c - force & remove abort-btn
        }
    }

} else {
    Write-Verbose "Not a client OS"
}
# Done!
Advertisements

Scandinavian chars in cmd-files

Got a scriptingquestion from a colleague, wasn’t that easy to find on google.

But with Windows Search I did find an old cmd-script that had exactly that problem sorted out.

So, the question was about scandinavian chars in a cmd-script. Some paths are named with non English letters and when you use them in a script it translates to a strange char instead of the letter.

This works fine:

DEL /F /Q "%USERPROFILE%Local SettingsSome Directory*.*"

This doesn’t work since there is a scandinavian letter in the path:

DEL /F /Q "%USERPROFILE%Lokala inställningarSome Directory*.*"

To fix it you need to change codepage, like this:

CHCP 850
DEL /F /Q "%USERPROFILE%Lokala inställningarSome Directory*.*"

You might need to use different codepage depending on the language you are using.

Some more info on MSDN

Nice PowerShell prompt

Been playing around with PowerShell the last days, and with nothing else better to do I did a nice looking prompt. 🙂

This prompt will give you a marker if you run it as Administrator and another mark if you are outside the filesystem.

How to:
First you need to edit your profile so it autoload the new prompt.

notepad $profile

Then paste this code, save and start a new PowerShell command line.

function prompt
{
	# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
	#                                                                              Rikard Ronnkvist / snowland.se
	# Multicolored prompt with marker for windows started as Admin and marker for providers outside filesystem
	# Examples
	#    C:WindowsSystem32>
	#    [Admin] C:WindowsSystem32>
	#    [Registry] HKLM:SOFTWAREMicrosoftWindows>
	#    [Admin] [Registry] HKLM:SOFTWAREMicrosoftWindows>
	# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

	# New nice WindowTitle
	$Host.UI.RawUI.WindowTitle = "PowerShell v" + (get-host).Version.Major + "." + (get-host).Version.Minor + " (" + $pwd.Provider.Name + ") " + $pwd.Path

	# Admin ?
	if( (
		New-Object Security.Principal.WindowsPrincipal (
			[Security.Principal.WindowsIdentity]::GetCurrent())
		).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))
	{
		# Admin-mark in WindowTitle
		$Host.UI.RawUI.WindowTitle = "[Admin] " + $Host.UI.RawUI.WindowTitle

		# Admin-mark on prompt
		Write-Host "[" -nonewline -foregroundcolor DarkGray
		Write-Host "Admin" -nonewline -foregroundcolor Red
		Write-Host "] " -nonewline -foregroundcolor DarkGray
	}

	# Show providername if you are outside FileSystem
	if ($pwd.Provider.Name -ne "FileSystem") {
		Write-Host "[" -nonewline -foregroundcolor DarkGray
		Write-Host $pwd.Provider.Name -nonewline -foregroundcolor Gray
		Write-Host "] " -nonewline -foregroundcolor DarkGray
	}

	# Split path and write  in a gray
	$pwd.Path.Split("") | foreach {
	    Write-Host $_ -nonewline -foregroundcolor Yellow
	    Write-Host "" -nonewline -foregroundcolor Gray
	}

	# Backspace last  and write >
	Write-Host "`b>" -nonewline -foregroundcolor Gray

    return " "
}

Off topic: Powershell to rename pictures

I’m trying hard to convert myself from VBScript to Powershell… so in the need of a powerfull filerenamer I wrote one in Powershell.

You need to download the Powershell Pack from http://code.msdn.microsoft.com/PowerShellPack

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#                                                                                           Rikard Ronnkvist / snowland.se
#  Will rename JPG and AVI files, uses EXIF-tag on JPG-images and filedate on AVI-files.
#
#  Files will be named:
#    some pathYYYYMMYYYYMMDD_HHMMSS_00.JPG
#               ^^^^^^ - Optional, if you have createSubdir set to True
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
$basePath = "F:PicturesImport dir"
$createSubdir = $True
$testMode = $False
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Download from:  http://code.msdn.microsoft.com/PowerShellPack
Import-Module PowerShellPack

# Add * to use when searching
$searchPath = $basePath + "*"

# Search for files
Write-Host "           Searching: " -ForegroundColor DarkGray -NoNewline
Write-Host $basePath -ForegroundColor Yellow

$allFiles  = Get-ChildItem -Path $searchPath -Include *.AVI,*.JPG -Exclude folder.jpg

Write-Host "               Found: " -ForegroundColor DarkGray -NoNewline
Write-Host $allFiles.Count -ForegroundColor Yellow -NoNewline
Write-Host " files" -ForegroundColor DarkGray

$fNum = 0
# Loop thru all files
foreach ($file in $allFiles )
{
	$fNum++
	# If it is an jpg use the exif-data, otherwise use date on file	
	if ($file.Extension -eq ".JPG") {
		$imgInfo = $file | Get-Image | Get-ImageProperty
		$fileDate = $imgInfo.dt
	} else {
		$fileDate = $file.LastWriteTime
	}

	if ($createSubdir -eq $True) {
		# Set new filepath
		$fileDir = $basePath + "" + $fileDate.ToString("yyyyMM")

		# Check directory
		if (!(Test-Path($fileDir))) {
			# Create a new subdirectory
			if ($testMode -ne $True) {
				$newDir = New-Item -Type directory -Path $fileDir
				Write-Host "            Creating: " -ForegroundColor DarkGray -NoNewline
				Write-Host $fileDir -ForegroundColor Red
			}
		}
	} else {
		# Use current directory
		$fileDir = $basePath
	}

	# Set new name to current to get "False" on first while
	$newPath = $file.Fullname

	$i = 0
	while (Test-Path $newPath) {
		# Set new filename
		$newPath = $fileDir + "" + $fileDate.ToString("yyyyMMdd_HHmmss") + "_" + $i.ToString("00") + $file.Extension
		$i++
	}

	# Write som info
	Write-Host $fNum.ToString().PadLeft(4) -ForegroundColor DarkYellow -NoNewline
	Write-Host " / " -ForegroundColor DarkGray -NoNewline
	Write-Host $allFiles.Count.ToString().PadRight(4) -ForegroundColor Yellow -NoNewline
	Write-Host "   Moving: " -ForegroundColor DarkGray -NoNewline
	Write-Host $file.Name -ForegroundColor Cyan -NoNewline
	Write-Host " -> " -ForegroundColor DarkGray -NoNewline
	Write-Host $newPath -ForegroundColor Green

	# Move and rename the file
	if ($testMode -ne $True) {
		Move-Item $file.Fullname $newPath
	}
}

Ending up with something like this:

Oh, and you can add -Recurse on the Get-ChildItem row…

Outlook and meeting reminders

I do not like the fact that the person sending a meeting request is the one deciding how long before the meeting a reminder would be set.

Since I don’t like the popup on my phone, I’m not using reminders.

So to get rid of incoming reminders I wrote a small script.

Press ALT+F11 and paste this code:

Sub snwRemoveMeetingReminder(Item As Outlook.MeetingItem)
    If TypeOf Item Is Outlook.MeetingItem Then
        Item.ReminderSet = False
        Item.Save
        
        Set Appt = Item.GetAssociatedAppointment(True)
        If Not Appt Is Nothing Then
            Appt.ReminderSet = False
            Appt.Save
        End If
    End If
End Sub

Then add the the script to incoming meeting requests.
090927_outlook_rule

Now the script will remove reminders on every incoming meeting request… It’s a client side rule, so you need to have Outlook running to get it to work.

Outlook inbox cleaner

I like to keep my inbox clean… so every now and then I sort out mails to different folders. That is boooring.

So I wrote a little script. 🙂

Sub InboxCleaner()
    Dim oNamespace As Outlook.NameSpace
    Dim oInboxFolder As Outlook.MAPIFolder
    Dim oDestFolder As Outlook.MAPIFolder
    Dim oItem As Object
    Dim i, iMove, iNoMove As Integer
    Dim sMsg, sFolder As String
    Dim bDoMove As Boolean
    
    Set oNamespace = Application.GetNamespace("MAPI")
    Set oInboxFolder = oNamespace.GetDefaultFolder(olFolderInbox)
    
    sMsg = ""
    iNoMove = 0
    iMove = 0
    
    For i = oInboxFolder.Items.Count To 1 Step -1
        Set oItem = oInboxFolder.Items(i)
        
        If InStr(oItem.SenderEmailAddress, "@") <> 0 Then
            sFolder = oItem.SenderEmailAddress ' Get sender address
            sFolder = Right(sFolder, Len(sFolder) - InStrRev(sFolder, "@")) ' Only domain name
            sFolder = Left(sFolder, InStr(sFolder, ".") - 1) ' Skip everything after the first dot
            sFolder = UCase(Left(sFolder, 1)) & Right(sFolder, Len(sFolder) - 1) ' Upper case first letter
            
            On Error Resume Next
            ' This row you might want to customize... I have a folders like  MailboxCustomersTheCustomerName
            Set oDestFolder = oInboxFolder.Folders.Parent.Parent.Folders("Customers").Folders(sFolder)
            
            If Err.Number <> 0 Then
                sMsg = sMsg & "Missing folder: " & vbTab & oItem.SenderEmailAddress & "  (" & sFolder & ")" & vbCrLf
                Set oDestFolder = Nothing
                Err.Clear
                bDoMove = False
                iNoMove = iNoMove + 1
            Else
                sMsg = sMsg & "Move:           " & vbTab & oItem.SenderEmailAddress & " -> " & sFolder & vbCrLf
                bDoMove = True
                iMove = iMove + 1
            End If
            On Error GoTo 0
            
            If bDoMove Then
            	' Comment out this line if you only want to test
                oItem.Move oDestFolder
            End If
        End If
    Next
    
    sMsg = sMsg & vbCrLf & _
        vbCrLf & _
        "Processed " & oInboxFolder.Items.Count & " items in inbox..." & vbCrLf & _
        "Moved:          " & vbTab & iMove & vbCrLf & _
        "Missing folder: " & vbTab & iNoMove & vbCrLf & _
        "Skipped:        " & vbTab & (oInboxFolder.Items.Count - (iMove + iNoMove))
    
    MsgBox sMsg, vbOKOnly, "Inbox Cleaner / Rikard Ronnkvist / snowland.se"

End Sub

Download: inboxcleaner.vbs

Update: Screendump of the result.
090323_inboxcleaner

vRD 2008 – A short review

I have been using vRD 1.5 for a while and did just purchase a license for vRD 2008.

I love the program…

When you are in need of multiple credentials for RDP-sessions (different customers, domains or whatver) vRD rocks!

Since I work as a consultant I set up one credential for each customer and then one folder (Linking to the right credential) for each customer.
And in that folder I can create servers, subfolders and so on.

In short: If you have multiple servers and/or multiple credentials, download it and try it out!

From Visonapp,com about vRD:

vRD 2008 features unparalleled ease of use, accelerated remote desktop access to servers and workstations, detailed logging features, and a new interface that allows administrators to view all connected machines simultaneously.
Considerably simplifying the administration of their systems, more than 75,000 administrators worldwide consider vRD an indispensable tool for recurring administrative tasks.

More info and download: visionapp.com

Visionapp Remote Desktop 2008

A few notes to the developers for the next version:

  • General
    • When using Alt+Tab, the vRD-window is selected instead of the tab with the machine and by that you can’t use ALT+TAB and start using the keyboard on the remote host.
    • Shortcut-key to each tab (Say ALT+F1 for tab 1)
  • Options
    • Thumbnail size – I want a larger (aprox 2 x Large)
  • Properties for a connection
    • I want a shortcut-key, faster way to connect
  • Management Board URL
  • Small stuff
    • From about-box, when clicking the URLs you start MSIE instead of standard browser… just annoying