Finding the last Sunday or another day of a month

I’ve had to find out programmatically when was the last Sunday of a month.
So, I remembered that the Powershell MVP Richard Siddaway has already shown a function to get the second Tuesday of a month, aka the Black Tuesday in the patchmanagement world.
I also did a little search on google and found the following nice blog post.
Howerver, I used another approach because:

  • I don’t want to use the while statement…
  • I wanted to be able to submit input from the pipeline,
  • I wanted the function to leverage new V3 features
  • I wanted to return different type of outputs: a datetime or integer
  • I wanted to add a “strong” parameters validation
  • I wanted to the function to work with any regional settings

The last goal was the most challenging. I found a very smart way to validate the Day parameter submitted as a string.
I first casted it into the .Net system.dayOfWeek class and used the new -in statement to see if the returned integer was in the array of all valid days of week stored as integer, i.e from 0 to 6.
To understand how I came up with this code, let’s try the following:

[System.DayOfWeek]1            
            
[System.DayOfWeek]::Monday -eq 1            
            
[System.Enum]::GetValues([System.DayOfWeek]) | ForEach-Object -Process {                        
    '{0} -> {1}' -f ([System.DayOfWeek]::$_.Value__),$_                        
}

Day of Week

Here’s the function:

Function Get-LastxOfMonth {            
[CmdletBinding()]            
param(            
    [parameter(Mandatory)]            
    [String]$Day,            
            
    [parameter(ParameterSetName='ByDate',Mandatory,ValueFromPipeline)]            
    [System.DateTime]$Date,            
            
    [parameter(ParameterSetName='ByString',Mandatory,ValueFromPipelineByPropertyName)]            
    [ValidateRange(1,12)]            
    [int]$Month,             
    [parameter(ParameterSetName='ByString',Mandatory,ValueFromPipelineByPropertyName)]            
    [ValidatePattern('^\d{4}$')]            
    [int]$Year,            
            
    [switch]$asDate=$false            
)            
Begin {            
    $alldays = @()            
}            
Process {            
    # Validate the Day string passed as parameter by casting it into            
    if (-not([System.DayOfWeek]::$Day -in 0..6)) {            
        Write-Warning -Message 'Invalid string submitted as Day parameter'            
        return            
    }            
            
    Switch ($PSCmdlet.ParameterSetName)            
    {            
        ByString {            
            # Do nothing, variables are already defined and validated            
        }            
        ByDate   {            
            $Month = $Date.Month            
            $Year = $Date.Year            
        }            
    }            
    # There aren't 32 days in any month so we make sure we iterate through all days in a month            
    0..31 | ForEach-Object -Process {            
        $evaldate = (Get-Date -Year $Year -Month $Month -Day 1).AddDays($_)            
        if ($evaldate.Month -eq $Month)            
        {            
            if ($evaldate.DayOfWeek -eq $Day) {            
                $alldays += $evaldate.Day            
            }            
        }            
    }            
    # Output            
    if ($asDate) {            
        Get-Date -Year $Year -Month $Month -Day $alldays[-1]            
    } else {            
        $alldays[-1]            
    }            
}            
End {}            
}

Let’s see what you can do with it:

# Find the last  Wednesday of each month of 2013            
1..12 | ForEach-Object -Process {            
    Get-LastxOfMonth -Day Wednesday -Month $_ -Year 2013 -asDate            
}            
            
# Make a mistake in the spelling of the day name            
Get-LastxOfMonth -Day FridayZ -Month 10 -Year 2010            
            
# Get the last Sunday of this month            
Get-LastxOfMonth -Day Sunday -Month 10 -Year 2012            
            
# Find the last Friday of the current month but next year            
Get-LastxOfMonth -Day Friday -Date (Get-Date).AddYears(1) -asDate            
            
# Submit a Datetime object through the pipeline            
(Get-Date).AddMonths(-4).AddYears(1) | Get-LastxOfMonth -Day Friday -asDate            
            
# Use the other parameter set that uses only a value from the pipeline by property name            
New-Object -TypeName psobject -Property @{            
    Month = 10;            
    Year = 2013;            
} | Get-LastxOfMonth -Day Friday -asDate

Getting Windows Updates installation history

Well, you probably know that there’s a Get-Hotfix cmdlet to query the list of installed updates on computer. However it has a huge drawback as you cannot determine when they have been installed as their WMI InstallDate property is empty 😦
Get-Hotfix output type

There are other ways to get this information. Let’s examine what is the best approach.

  • From eventlog entries
  • Get-WinEvent -FilterHashtable @{LogName = "System";ID=19,20} | ForEach-Object -Process {            
        Get-EventLog -Index ($_.RecordID)  -LogName $($_.LogName)            
    }

    Get WU install info from event logs
    NB: This is very slow

  • From Windowsupdate.log
    We could use the ColorWU script I proposed on this page with the following parameter -SearchString “Successfully installed”
    NB: However it’s not exhaustive and you’ve to parse again the log for failures
    Or
    We could use the Get-Maches function I borrowed from Herr Dr. Tobias Weltner on this page and that I presented when analysing WordPress stats

    Get-Content $env:windir\windowsupdate.log -Encoding UTF8 -ReadCount 0 |             
    Get-Matches '(?\d{4}-\d{2}-\d{2}).*?(?\d{2}:\d{2}:\d{2}).*?successfully installed.*?update: (?.*?) \({0,1}KB(?\d{5,8})'

    NB: This the fastest way of parsing WindowsUpdate.log but the results are not exhaustive.
    You also have to parse the log for errors like this:

    Get-Content $env:windir\windowsupdate.log -Encoding UTF8 -ReadCount 0 |            
    Get-Matches '(?\d{4}-\d{2}-\d{2}).*?(?\d{2}:\d{2}:\d{2}).*?failed to install.*?update with error (?0x\d{8}): (?.*?) \({0,1}KB(?\d{5,8})'

    Parse WindowsUpdate for failures

  • Using the Windows Update Agent COM Object
  • $Session = New-Object -ComObject Microsoft.Update.Session            
    $Searcher = $Session.CreateUpdateSearcher()            
    $HistoryCount = $Searcher.GetTotalHistoryCount()            
    # http://msdn.microsoft.com/en-us/library/windows/desktop/aa386532%28v=vs.85%29.aspx            
    $Searcher.QueryHistory(0,$HistoryCount) | ForEach-Object -Process {            
        $Title = $null            
        if($_.Title -match "\(KB\d{6,7}\)"){            
            # Split returns an array of strings            
            $Title = ($_.Title -split '.*\((?KB\d{6,7})\)')[1]            
        }else{            
            $Title = $_.Title            
        }            
        # http://msdn.microsoft.com/en-us/library/windows/desktop/aa387095%28v=vs.85%29.aspx            
        $Result = $null            
        Switch ($_.ResultCode)            
        {            
            0 { $Result = 'NotStarted'}            
            1 { $Result = 'InProgress' }            
            2 { $Result = 'Succeeded' }            
            3 { $Result = 'SucceededWithErrors' }            
            4 { $Result = 'Failed' }            
            5 { $Result = 'Aborted' }            
            default { $Result = $_ }            
        }            
        New-Object -TypeName PSObject -Property @{            
            InstalledOn = Get-Date -Date $_.Date;            
            Title = $Title;            
            Name = $_.Title;            
            Status = $Result            
        }            
                
    } | Sort-Object -Descending:$true -Property InstalledOn |             
    Select-Object -Property * -ExcludeProperty Name | Format-Table -AutoSize -Wrap

    NB: This one is exhaustive and damn fast.
    History from WUA

Voilà 🙂

Working with scheduled tasks

When one deals with automation, a good IT pro should immediately think powershell and scheduled tasks. Scheduled tasks exist since a long time, Microsoft has made big improvements to the scheduler over the last 10 years. However, there isn’t an easy way to deal with scheduled tasks with Powershell V2. You could use a wrapper of the schtasks command or write custom functions handling COM objects. In powershell V3 Microsoft started to introduce cmdlets but they may not cover all the needs and have some limits. Here’s a non exhaustive list of these limitations:

  • You cannot easily enumerate all the tasks on the system with the Get-ScheduledJob command
  • Tasks created by the Register-ScheduledJob cmdlet are located in \Microsoft\Windows\PowerShell\ScheduledJobs
  • To create a task, you need to create first a trigger with the New-JobTrigger cmdlet
  • The new-jobtrigger cmdlet is limited compared to the GUI (taskschd.msc): only Once,Daily,Weekly,AtLogon and AtStartup are available compared to the following list on the technet page
    task triggers
    You can find other limitations presented by Jeffery Hicks on this page: http://jdhitsolutions.com/blog/2012/05/sql-saturday-129-session-material/

Let’s stop the criticism and have some hands on.
The excellent post of the HeyScripting Guy, Ed Wilson http://blogs.technet.com/b/heyscriptingguy/archive/2009/04/01/how-can-i-best-work-with-task-scheduler.aspx inspired me a lot.

After reading the above post, my idea was to be able to achieve the same result as the following commands:

# List all tasks from remote comupter            
schtasks  --% /s remotePC /query            
            
# Get the XML definition of the task            
schtasks  --%  /query /TN "\Microsoft\Windows\RAC\RacTask" /XML            
            
# Get all the task properties            
schtasks --%  /query /TN "\Microsoft\Windows\RAC\RacTask" /V /FO LIST

Sounds easy…
However, after digging into the XML schema of a task. My goal appeared to be a little bit ambitious 😦

Anyway, playing with XML tasks triggers could be the subject of another post. So here’s the code that could replace the the first two schtasks commands:

# Requires -Version 2.0

Function Get-AllScheduledTasks 
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$false,ValueFromPipeline=$true)]
        [System.String[]]$ComputerName = $env:COMPUTERNAME
    )
    Begin {
        

        Function Get-SubFolders ($folder,[switch]$recurse)
        {
            $folder
            if ($recurse)
            {
                $TaskService.GetFolder($folder).GetFolders(0) | ForEach-Object {
                Get-SubFolders $_.Path -Recurse
                }
            } else {
                $TaskService.GetFolder($folder).GetFolders(0)
            }
   
        }
    }
    Process {

        $ComputerName | ForEach-Object -Process {
            $alltasks = @()
            $Computer  = $_
            $TaskService = New-Object -com schedule.service
            try
            {
                $TaskService.Connect($Computer) | Out-Null

            } catch {
                Write-Warning "Cannot connect to $Computer because $($_.Exception.Message)"
                return
            }

            Get-SubFolders -folder "\" -recurse | ForEach-Object -Process {

                $TaskService.GetFolder($_).GetTasks(1) | ForEach-Object -Process {
                    $obj = New-Object -TypeName PSObject -Property @{
                        ComputerName = $Computer
                        Path = Split-Path $_.Path
                        Name = $_.Name
                    }
                    $alltasks += $obj
                }
            }
            Write-Verbose -Message "There's a total of $($alltasks.Count) tasks on $Computer"
            $alltasks
        }
    }
    End {}
}

Function Get-Task
{
[CmdletBinding()]
    param (
    [parameter(ValueFromPipeline=$false,ValueFromPipelineByPropertyName=$true,Mandatory=$false)]
    [system.string[]] ${ComputerName} = $env:computername,

    [parameter(ValueFromPipeline=$false,ValueFromPipelineByPropertyName=$true,Mandatory=$false,
               HelpMessage="The task folder string must begin by '\'")]
    [ValidatePattern('^\\')]
    [system.string[]] ${Path} = "\",

    [parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
    [ValidateNotNullOrEmpty()]
    [system.string[]] ${Name} = $null
    )
    Begin {}
    Process
    {
        $resultsar = @()
        $ComputerName | ForEach-Object -Process {
            $Computer = $_
            $TaskService = New-Object -com schedule.service
            try
            {
                $TaskService.Connect($Computer) | Out-Null
            } catch {
                Write-Warning "Failed to connect to $Computer"
            }
            if ($TaskService.Connected)
            {
                Write-Verbose -Message "Connected to the scheduler service of computer $Computer"
                    Foreach ($Folder in $Path)
                    {
                        Write-Verbose -Message "Dealing with folder task $Folder"
                        $RootFolder = $null
                        try
                        {
                            $RootFolder = $TaskService.GetFolder($Folder)
                        } catch {
                            Write-Warning -Message "The folder task $Folder cannot be found"
                        }
                        if ($RootFolder)
                        {
                            Foreach ($Task in $Name)
                            {
                                $TaskObject = $null
                                try
                                {
                                    Write-Verbose -Message "Dealing with task name $Task"
                                    $TaskObject = $RootFolder.GetTask($Task)
                                } catch {
                                    Write-Warning -Message "The task $Task cannot be found under $Folder"
                                }
                                if ($TaskObject)
                                {
                                    switch ($TaskObject.NextRunTime) {
                                        (Get-Date -Year 1899 -Month 12 -Day 30 -Minute 00 -Hour 00 -Second 00) {$NextRunTime = "None"}
                                        default {$NextRunTime = $TaskObject.NextRunTime}
                                    }
                                    
                                    switch ($TaskObject.LastRunTime) {
                                        (Get-Date -Year 1899 -Month 12 -Day 30 -Minute 00 -Hour 00 -Second 00) {$LastRunTime = "Never"}
                                        default {$LastRunTime = $TaskObject.LastRunTime}
                                    } 
                                                                       
                                    # Author
                                    switch (([xml]$TaskObject.XML).Task.RegistrationInfo.Author)
                                    {
                                        '$(@%ProgramFiles%\Windows Media Player\wmpnscfg.exe,-1001)'   { $Author = 'Microsoft Corporation'}
                                        '$(@%systemroot%\system32\acproxy.dll,-101)'                   { $Author = 'Microsoft Corporation'}
                                        '$(@%SystemRoot%\system32\aepdu.dll,-701)'                     { $Author = 'Microsoft Corporation'}
                                        '$(@%SystemRoot%\system32\aitagent.exe,-701)'                  { $Author = 'Microsoft Corporation'}
                                        '$(@%systemroot%\system32\appidsvc.dll,-201)'                  { $Author = 'Microsoft Corporation'}
                                        '$(@%systemroot%\system32\appidsvc.dll,-301)'                  { $Author = 'Microsoft Corporation'}
                                        '$(@%SystemRoot%\System32\AuxiliaryDisplayServices.dll,-1001)' { $Author = 'Microsoft Corporation'}
                                        '$(@%SystemRoot%\system32\bfe.dll,-2001)'                      { $Author = 'Microsoft Corporation'}
                                        '$(@%SystemRoot%\system32\BthUdTask.exe,-1002)'                { $Author = 'Microsoft Corporation'}
                                        '$(@%systemroot%\system32\cscui.dll,-5001)'                    { $Author = 'Microsoft Corporation'}
                                        '$(@%SystemRoot%\System32\DFDTS.dll,-101)'                     { $Author = 'Microsoft Corporation'}
                                        '$(@%SystemRoot%\system32\dimsjob.dll,-101)'                   { $Author = 'Microsoft Corporation'}
                                        '$(@%systemroot%\system32\dps.dll,-600)'                       { $Author = 'Microsoft Corporation'}
                                        '$(@%SystemRoot%\system32\drivers\tcpip.sys,-10000)'           { $Author = 'Microsoft Corporation'}
                                        '$(@%systemroot%\system32\defragsvc.dll,-801)'                 { $Author = 'Microsoft Corporation'}
                                        '$(@%systemRoot%\system32\energy.dll,-103)'                    { $Author = 'Microsoft Corporation'}
                                        '$(@%SystemRoot%\system32\HotStartUserAgent.dll,-502)'         { $Author = 'Microsoft Corporation'}
                                        '$(@%SystemRoot%\system32\kernelceip.dll,-600)'                { $Author = 'Microsoft Corporation'}
                                        '$(@%systemRoot%\System32\lpremove.exe,-100)'                  { $Author = 'Microsoft Corporation'}
                                        '$(@%SystemRoot%\system32\memdiag.dll,-230)'                   { $Author = 'Microsoft Corporation'}
                                        '$(@%SystemRoot%\system32\mscms.dll,-201)'                     { $Author = 'Microsoft Corporation'}
                                        '$(@%systemRoot%\System32\msdrm.dll,-6001)'                    { $Author = 'Microsoft Corporation'}
                                        '$(@%systemroot%\system32\msra.exe,-686)'                      { $Author = 'Microsoft Corporation'}
                                        '$(@%SystemRoot%\system32\nettrace.dll,-6911)'                 { $Author = 'Microsoft Corporation'}
                                        '$(@%systemroot%\system32\osppc.dll,-200)'                     { $Author = 'Microsoft Corporation'}
                                        '$(@%systemRoot%\System32\perftrack.dll,-2003)'                { $Author = 'Microsoft Corporation'}
                                        '$(@%systemroot%\system32\PortableDeviceApi.dll,-102)'         { $Author = 'Microsoft Corporation'}
                                        '$(@%SystemRoot%\system32\profsvc,-500)'                       { $Author = 'Microsoft Corporation'}
                                        '$(@%SystemRoot%\system32\RacEngn.dll,-501)'                   { $Author = 'Microsoft Corporation'}
                                        '$(@%SystemRoot%\system32\rasmbmgr.dll,-201)'                  { $Author = 'Microsoft Corporation'}
                                        '$(@%systemroot%\system32\regidle.dll,-600)'                   { $Author = 'Microsoft Corporation'}
                                        '$(@%systemroot%\system32\sdclt.exe,-2193)'                    { $Author = 'Microsoft Corporation'}
                                        '$(@%systemroot%\system32\sdiagschd.dll,-101)'                 { $Author = 'Microsoft Corporation'}
                                        '$(@%systemroot%\system32\sppc.dll,-200)'                      { $Author = 'Microsoft Corporation'}
                                        '$(@%systemroot%\system32\srrstr.dll,-321)'                    { $Author = 'Microsoft Corporation'}
                                        '$(@%systemroot%\system32\upnphost.dll,-215)'                  { $Author = 'Microsoft Corporation'}
                                        '$(@%SystemRoot%\system32\usbceip.dll,-600)'                   { $Author = 'Microsoft Corporation'}
                                        '$(@%systemroot%\system32\w32time.dll,-202)'                   { $Author = 'Microsoft Corporation'}
                                        '$(@%systemroot%\system32\wdc.dll,-10041)'                     { $Author = 'Microsoft Corporation'}
                                        '$(@%SystemRoot%\system32\wer.dll,-293)'                       { $Author = 'Microsoft Corporation'}
                                        '$(@%SystemRoot%\System32\wpcmig.dll,-301)'                    { $Author = 'Microsoft Corporation'}
                                        '$(@%SystemRoot%\System32\wpcumi.dll,-301)'                    { $Author = 'Microsoft Corporation'}
                                        '$(@%systemroot%\system32\winsatapi.dll,-112)'                 { $Author = 'Microsoft Corporation'}
                                        '$(@%SystemRoot%\system32\wat\WatUX.exe,-702)'                 { $Author = 'Microsoft Corporation'}
                                        default {$Author = $_ }                                   
                                    }
                                    # Created
                                    switch (([xml]$TaskObject.XML).Task.RegistrationInfo.Date)
                                    {
                                        ''      {$Created = 'Unknown'}
                                        default {$Created = Get-Date -Date ([xml]$TaskObject.XML).Task.RegistrationInfo.Date }
                                    }
                                    
                                    # Triggers
                                    # ([xml]$TaskObject.XML).Task.Triggers.Count
                                    # Inject here dev. about triggers

                                    # Status
                                    # http://msdn.microsoft.com/en-us/library/windows/desktop/aa383617%28v=vs.85%29.aspx
                                    switch ($TaskObject.State)
                                    {
                                        0 { $State = 'Unknown'}
                                        1 { $State = 'Disabled'}
                                        2 { $State = 'Queued'}
                                        3 { $State = 'Ready'}
                                        4 { $State = 'Running'}
                                        default {$State = $_ }
                                    }

                                    Switch (([xml]$TaskObject.XML).Task.Settings.Hidden)
                                    {
                                        false { $Hidden = $false}
                                        true  { $Hidden = $true }
                                        default { $Hidden = $false}
                                    }
                                    $resultsar += New-Object -TypeName PSObject -Property @{
                                        Created = $Created
                                        ComputerName = $Computer
                                        Author = $Author
                                        Name = $TaskObject.Name
                                        Path = $Folder
                                        State = $State
                                        Enabled = $TaskObject.Enabled
                                        LastRunTime = $LastRunTime
                                        LastTaskResult = $TaskObject.LastTaskResult
                                        # NumberOfMissedRuns = $TaskObject.NumberOfMissedRuns
                                        NextRunTime = $NextRunTime
                                        # Definition = $TaskObject.Definition
                                        Xml = $TaskObject.XML
                                        Hidden = $Hidden
                                    }
                                }
                            }
                        }
                    }
            }
        }
        return $resultsar
    } 
    End {}
}

On line 44, you can see that I’m using GetTasks(1) to enumerate all tasks including hidden ones.

Let’s demonstrate quickly what can be done with the above 2 functions.

# Get all the tasks from 2 computers,            
# get all their properties and select hidden one            
# and show only a subset of selected properties            
'192.168.0.100',$env:computername | Get-AllScheduledTasks |             
Get-Task | Where { ($_.Hidden)} |            
Select-Object -Property Computername,Name,Path,Hidden            
            
# List all the tasks, where their name match 'monitor'            
# get all their properties and display a subset of properties            
Get-AllScheduledTasks | Where { $_.Name -match 'Monitor' } |             
Get-Task | Format-Table -Property Path,Name,State,Hidden

monitor tasks

# Get the task from Adobe at the root folder            
# and show all its properties except its XML definition            
Get-Task -Path "\" -Name "Adobe Flash Player Updater" |             
Select-Object -ExcludeProperty XML -Property *            

Adobe FlashPlayer udpate task

# Get all the tasks and their properties            
# where the author isn't either Microsoft nor empty            
# and display a subset of their properties            
Get-AllScheduledTasks |  Get-Task |             
Where { ($_.Author -notmatch 'Microsoft') -and $_.Author } |            
 ft -Property Path,Name,State,Hidden,Author            

Non MSFT tasks

# Get the tasks created by Register-ScheduledJob            
# Just extract the command it launches from their XML definition            
Get-AllScheduledTasks |             
Where { $_.Path -eq '\Microsoft\Windows\PowerShell\ScheduledJobs'} |            
Get-Task | ForEach-Object -Process {            
    'TaskName : {0}' -f $_.Name            
    'Command  : {0}' -f ([xml]$_.XML).Task.Actions.Exec.Command            
    'Arguments: {0}' -f (([xml]$_.XML).Task.Actions.Exec | Select-Object -ExpandProperty Arguments)            
}

Scheduled jobs tasks

Test for Administrative Privileges

about admin privileges
Link to the discussion on oreilly.com

Lee Holmes is absolutely right.

I usually do:

Function Test-Foo {            
[CmdletBinding()]            
Param()            
Begin {            
 # Make sure we run as admin                                    
    $usercontext = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()                                    
    $IsAdmin = $usercontext.IsInRole([Security.Principal.WindowsBuiltInRole]"Administrator")                                                       
    if (-not($IsAdmin))                                    
    {                                    
        Write-Warning "Must run powerShell as Administrator to perform these actions"                                    
        break                        
    }              
}            
Process  {            
    'Process block goes here'            
}             
End {}            
}                          

That works fine on EN-US computers but not on French, German,…ones.

He gave us the solution.

So I quickly did the following to enumerate of the integer and their corresponding string

            
[System.Enum]::GetValues([System.Security.Principal.WindowsBuiltInRole]) | ForEach-Object -Process {            
    '{0} -> {1}' -f ([System.Security.Principal.WindowsBuiltInRole]::$_.Value__),$_            
}            
            

enum rid

To use the ‘bool IsInRole(int rid)’ method and as we know that 544 means Administrator, I do:

Function Test-Bar {            
[CmdletBinding()]            
Param()            
Begin {            
 # Make sure we run as admin                                    
    $usercontext = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()                                    
    $IsAdmin = $usercontext.IsInRole(544)                                                       
    if (-not($IsAdmin))                                    
    {                                    
        Write-Warning "Must run powerShell as Administrator to perform these actions"                                    
        break                        
    }              
}            
Process  {            
    'Process block goes here'            
}             
End {}            
}                          

The 2nd approach by using well-known SID will also work as the BUILTIN\Administrators group has the following SID: S-1-5-32-544:

$usercontext = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()                                    
$IsAdmin = $usercontext.IsInRole([System.Security.Principal.SecurityIdentifier]'S-1-5-32-544')                                                       
$IsAdmin

NB: Well-known security identifiers can be found on that page: http://support.microsoft.com/kb/243330

Show-ProcessTree

I wondered for fun if it’s possible for powershell to show the process tree like procexp.exe or pslist.exe -t do.
I also wanted to have the same display with indentation as the Get-WindowsFeature cmdlet.
I’ve found the ParentProcessID property in the Win32_Process WMI class that would help me build the tree.
Then I figured out using procexp.exe that top level processes don’t have their parent process existing anymore and that PID 0 is very special.
I knew also that I’ll have to write a sub function to recurse. Except the built-in -Recurse parameter on some cmdlets, I’ve seen only 2 advanced functions so far on the following pages that implement a recursion:

I borrowed the Indent function from the above page and after a few tests abusing some of the new powershell V3 features like implicit foreach or the nice new -in comparison operator, I came up with a working code.

I’ve also seen that a Microsft PFE from Germany, Michael Frommhold, proposed a vbscript with the tree view in BEG8 of the 2010 scripting games:

# Requires -Version 3.0            
            
Function Show-ProcessTree  {            
[CmdletBinding()]            
Param()            
    Begin {            
        # Identify top level processes            
        # They have either an identified processID that doesn't exist anymore            
        # Or they don't have a Parentprocess ID at all            
        $allprocess  = Get-WmiObject -Class Win32_process            
        $uniquetop  = ($allprocess).ParentProcessID | Sort-Object -Unique            
        $existingtop =  ($uniquetop | ForEach-Object -Process {$allprocess | Where ProcessId -EQ $_}).ProcessID            
        $nonexistent = (Compare-Object -ReferenceObject $uniquetop -DifferenceObject $existingtop).InPutObject            
        $topprocess = ($allprocess | ForEach-Object -Process {            
            if ($_.ProcessID -eq $_.ParentProcessID){            
                $_.ProcessID            
            }            
            if ($_.ParentProcessID -in $nonexistent) {            
                $_.ProcessID            
            }            
        })            
        # Sub functions            
        # Function that indents to a level i            
        function Indent {            
            Param([Int]$i)            
            $Global:Indent = $null            
            For ($x=1; $x -le $i; $x++)            
            {            
                $Global:Indent += [char]9            
            }            
        }            
        Function Get-ChildProcessesById {            
        Param($ID)            
            # use $allprocess variable instead of Get-WmiObject -Class Win32_process to speed up            
            $allprocess | Where { $_.ParentProcessID -eq $ID} | ForEach-Object {            
                Indent $i            
                '{0}{1} {2}' -f $Indent,$_.ProcessID,($_.Name -split "\.")[0]            
                $i++            
                # Recurse            
                Get-ChildProcessesById -ID $_.ProcessID            
                $i--            
            }            
        } # end of function            
    }            
    Process {            
        $topprocess | ForEach-Object {            
            '{0} {1}' -f $_,(Get-Process -Id $_).ProcessName            
            # Avoid processID 0 because parentProcessId = processID            
            if ($_ -ne 0 )            
            {            
                $i = 1            
                Get-ChildProcessesById -ID $_            
            }            
        }            
    }             
    End {}            
}

Now to simulate a pslist -t output with the above function, I just did:

do            
    {            
        Clear-Host             
        Show-ProcessTree            
        Start-Sleep -Seconds 1            
    }            
    while ($true)            

Note that I’ve produced this code for fun as a PoC. I know there are better ways to achieve this.
I should also probably start digging in format output views and see how the Get-WindowsFeature implements a tree view.

Switch from Windows 2012 Core to GUI mode…hands on!

Usually, you’d first install a server with a GUI, complete its configuration and remove its UI for all the good reasons related to security.

But if you’ve started by installing a Core version and try to add back its GUI, you may encounter the famous error 0x800f0906 that you also get when you try to install .Net 3.5 and that was reported by Mike F Robbins here.

Whenever you do

Install-WindowsFeature -Name "Server-Gui-Mgmt-Infra" -Restart:$false

You end with:


Install-WindowsFeature : The request to add or remove features on thespecified server failed.
Installation of one or more roles, role services, or features failed.
The source files could not be downloaded.
Use the "source" option to specify the location of the files that are required to restore the feature. For more information on specifying a source location,
see http://go.microsoft.com/fwlink/?LinkId=243077. Error: 0x800f0906

Actually by invoking Install-WindowsFeature like this, we ask it to get the source online from Microsoft as we did not specified a local source as parameter.

You may wonder if there’s a connection problem or a name resolution issue.
Actually there isn’t. The result 200 (status: OK) is returned by the following command:

Invoke-WebRequest -Uri http://www.google.com -UseBasicParsing

So, we can deduct that the online source is not available. Let’s try to use a local source instead of using the default online parameter

The following technet page http://technet.microsoft.com/en-us/library/hh831786.aspx says that

To use Windows PowerShell to convert from a Server Core installation to a Server with a GUI installation

Determine the index number for a Server with a GUI image (for example, SERVERDATACENTER, not SERVERDATACENTERCORE) with Get-WindowsImage -ImagePath \install.wim.

Run Install-WindowsFeature Server-Gui-Mgmt-Infra,Server-Gui-Shell –Restart –Source c:\mountdir\windows\winsxs

Alternatively, if you want to use Windows Update as the source instead of a WIM file, use this Windows PowerShell cmdlet:

Install-WindowsFeature Server-Gui-Mgmt-Infra,Server-Gui-Shell –Restart

…but that’s not totally accurate.

Fortunately the following recent blog post help us solve the famous error 0x800f0906

The first step is to get the original install source. Instead of putting the DVD in a physical drive, you just do the following on a hyper-V server:

Set-VMDvdDrive -Path D:\Downloads\SW_DVD5_Win_Svr_Std_and_DataCtr_2012_64Bit_English_Core_MLF_X18-27588.ISO -VMName MyVMName            
Get-WindowsImage -ImagePath D:\sources\install.wim

Then there’s no need to mount it, you just have to invoke directly the install-WindowsFeature cmdlet like this:

Install-WindowsFeature server-gui-mgmt-infra -source:wim:d:\sources\install.wim:2 -Restart:$false

Why it failed on the first place ?

It failed because the feature is not available and its source files have been removed. This corresponds to the following status
get-windowsfeature default state on core
dism payload removed

I just used the following commands to get the above screenshots

dism /online /get-features            
Get-WindowsFeature | ft Name,InstallState

But the result of the Install-WindowsFeature has another explanation:
install-WindowsFeature
It tells us that it failed because Windows Update wasn’t yet configured on the core server…

Wait, let’s go back 2 seconds to the basics we’ve just figured out.
There are 3 states for features and here is the corresponding states between dism and the get-windowsfeature cmdlet:

DISM states: Get-WindowsFeature states:
Enabled Installed
Disabled Available
Disabled with Payload Removed Removed

Humm, there’s more!

The product manager said in the MS forum on March that

If you wanted to create a centralized network install point that you could point to, you could extract the wim and then merge the contents of the sxs directory on the media with the winsxs directory in /windows/winsxs to have a single source that could install all features. Hope this makes sense! Let me know if you have any more questions or feedback about Features on Demand.

…sounds cool 🙂

# Extract all the required source directories            
            
# 1. Create an empty directory             
mkdir c:\mount            
# 2. Mount the Standard image            
Mount-WindowsImage -Path C:\mount -ImagePath D:\sources\install.wim -Index 2 -ReadOnly            
# 3. Get the WinSXS from the install.wim            
robocopy  C:\mount\Windows\WinSxS \\DISTRIBSERVER\Share\WS2012\STD_WINSXS /S /r:0 /Z            
# 4. Dismount the WIM file            
Get-WindowsImage -Mounted | Dismount-WindowsImage -Discard            
# 5. Mount the Datacenter image            
Mount-WindowsImage -Path C:\mount -ImagePath D:\sources\install.wim -Index 4 -ReadOnly            
# 6. Get the WinSXS from the install.wim            
robocopy  C:\mount\Windows\WinSxS \\DISTRIBSERVER\Share\WS2012\DTC_WINSXS /S /r:0 /Z            
# 7. Dismount the WIM file            
Get-WindowsImage -Mounted | Dismount-WindowsImage -Discard            
# 8. Copy the SXS folder from the ISO image            
robocopy  D:\sources\sxs  \\DISTRIBSERVER\Share\WS2012\SXS /S /R:0 /Z

Simulating some robocopy from the STD_WINSXS to the DTC_WINSXS folder shows that there are very few differences. There are some additions and only the FileMaps folder content is different. In other words, it’s the perfect candidate for a partition with deduplication on.

Get/Set/Clear proxy

Mike F Robbins (@mikefrobbins) asked if there’s a way to set a proxy with a powershell cmdlet on Windows 2012 core edition.

netsh proxy

Well, as Mike said you need now to use netsh winhttp context to achieve this task as proxycfg.exe has been removed.

But you can also do this with the following helper functions that I wrote for this purpose 🙂

# Requires -Version 3.0            
            
Function Clear-WinHTTPproxy {            
[CmdletBinding()]            
Param()            
Begin {            
    # Make sure we run as admin                        
    $usercontext = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()                        
    $IsAdmin = $usercontext.IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")                                           
    if (-not($IsAdmin))                        
    {                        
        Write-Warning "Must run powerShell as Administrator to perform these actions"                        
        break            
    }            
    $head = 40,0,0,0,0,0,0,0,1,0,0,0            
    $none = 0,0,0,0,0,0,0,0            
}            
Process {            
    $HT = @{            
        Path = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\Connections";            
        Name = "WinHttpSettings";            
        PropertyType = "Binary";            
        Value = ($head+$none);            
        Force = $true;            
        ErrorAction = "Stop";            
    }            
    try{             
        New-ItemProperty @HT | Out-Null            
    } catch {            
        Write-Warning -Message "Failed to set proxy because $($_.Exception.Message)"            
    }            
    Get-WinHttpProxy            
}             
End {}            
}            
            
Function Get-WinHttpProxy {            
[CmdletBinding()]            
Param()            
Begin{}            
Process {            
   $binval = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\Connections" -Name WinHttpSettings).WinHttPSettings            
   $proxylength = $binval[12]            
   if ($proxylength -gt 0) {            
       $proxy = -join ($binval[(12+3+1)..(12+3+1+$proxylength-1)] | % {([char]$_)})            
       $bypasslength = $binval[(12+3+1+$proxylength)]            
       if ($bypasslength -gt 0) {            
            $bypasslist = -join ($binval[(12+3+1+$proxylength+3+1)..(12+3+1+$proxylength+3+1+$bypasslength)] | % {([char]$_)})            
        } else {            
            $bypasslist = '(none)'            
        }            
       "Current WinHTTP proxy settings:`n"            
       '    Proxy Server(s): {0}' -f $proxy            
       '    Bypass List    : {0}' -f $bypasslist            
    } else {            
        @'
Current WinHTTP proxy settings:

    Direct access (no proxy server).
'@            
    }            
}            
End{}            
}            
            
Function Set-WinHttpProxy {            
[cmdletbinding()]            
Param(            
[Parameter(mandatory)][system.string]$proxyserver=$null,            
[System.String]$bypasslist=$null            
)            
Begin{            
    # Make sure we run as admin                        
    $usercontext = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()                        
    $IsAdmin = $usercontext.IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")                                           
    if (-not($IsAdmin))                        
    {                        
        Write-Warning "Must run powerShell as Administrator to perform these actions"                        
        break            
    }            
}            
Process {            
    # Define 3 arrays            
    $proxylength = $proxyserver.Length,0,0,0            
    $bypasslength = $bypasslist.Length,0,0,0            
    $head = 40,0,0,0,0,0,0,0,3,0,0,0            
    $HT = @{            
        Path  = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\Connections";            
        Name = "WinHttpSettings";            
        PropertyType = "Binary";            
        Value  = ($head+$proxylength+$proxyserver.ToCharArray()+$bypasslength+$bypasslist.ToCharArray())             
        Force = $true;            
        ErrorAction = "Stop";            
    }            
    try {            
        New-ItemProperty @HT | Out-Null            
    } catch {            
        Write-Warning -Message "Failed to set proxy because $($_.Exception.Message)"            
    }            
    Get-WinHttpProxy            
}            
End {}            
}

Here’s how to use them with their netsh equivalent:

# Reset proxy            
netsh --% winhttp reset proxy            
Clear-WinHTTPproxy            
            
# Show proxy            
netsh --% winhttp show proxy            
Get-WinHttpProxy            
            
# Set proxy            
netsh --% winhttp set proxy proxy-server="http=myproxy;https=sproxy:88" bypass-list="*.foo.com"            
Set-WinHttpProxy -proxyserver "http=myproxy;https=sproxy:88" -bypasslist "*.foo.com"            

NB: The above function only query the WinHttpSettings value in the registry. It doesn’t take into account whether there’s a GPO and/or a current user level (HKCU hive) based proxy.