Bookmark and Share
 

 

(Note: These solutions were written for Advanced Event 7 of the 2010 Scripting Games.)

 

Advanced Event 7 (Windows PowerShell)

James Brundage is a former software developer engineer in testing for Microsoft. He has been a frequent poster on the Windows PowerShell team blog, and he maintains the Media and Microcode Blog on MSDN. 
 

The Event

For this event I was asked to build the most timeless IT admin app ever: the system monitor. In as few lines as possible, I had to make a system monitor that could:

  • Display the current username.
  • Display every program running on the machine.
  • Tell how long the user has been logged on.
  • Show memory consumption.
  • Show CPU usage.
  • Tell if the user is an administrator or not.
  • Have regular updates and an update button.

The Approach

I interpreted the point of this particular event to be showing how I would build a decent GUI in Windows PowerShell, and not showing how to make the best UI and most complete version of each of the tools listed above.

To build my GUI, I used the Windows Presentation Foundation (WPF) PowerShell Kit (WPK). WPK is available as part of the PowerShellPack on MSDN’s code gallery: http://code.msdn.microsoft.com/PowerShellPack.

The script has a total of 216 lines (with 103 lines of comments). It has five major parts:

  1. Parameters that should be there on any scripted UI function (~50 lines, including inline help for the parameters). This is anything within param().
  2. The visual layout (~70 lines). This is the argument for the –Children parameter of New-Grid.
  3. Common functions (~60 lines). These are the functions that the UI will use internally.
  4. Background data (~10 lines). This starts up a data collection that will run as long as the UI is active.
  5. Startup behavior (~15 lines). This starts timed events that will run in the UI.

Most of the time that you build UIs in Windows PowerShell with WPK of any complexity, your scripts should take on a similar structure. Now that you’ve got this in mind, here’s the script. You can use it like this:     Watch-System –AsJob


The Script

function Watch-System {
    <#
    .Synopsis
        A Quick Systems Monitor GUI written in WPK
    .Description
        Watch-System is a quick system monitoring GUI written in WPK for the 2010 Scripting Games
    .Example
        Watch-System -AsJob
    #>
    param(
    # The name of the control       
    [string]$Name,
    # If the control is a child element of a Grid control (see New-Grid),
    # the Row parameter will be used to determine where to place the
    # top of the control. Using the -Row parameter changes the
    # dependency property [Windows.Controls.Grid]::RowProperty
    [Int]$Row,
    # If the control is a child element of a Grid control (see New-Grid)
    # the Column parameter will be used to determine where to place
    # the left of the control. Using the -Column parameter changes the
    # dependency property [Windows.Controls.Grid]::ColumnProperty
    [Int]$Column,
    # If the control is a child element of a Grid control (see New-Grid)
    # the RowSpan parameter will be used to determine how many rows
    # in the grid the control will occupy. Using the -RowSpan parameter
    # changes the dependency property [Windows.Controls.Grid]::RowSpanProperty
    [Int]$RowSpan,
    # If the control is a child element of a Grid control (see New-Grid)
    # the RowSpan parameter will be used to determine how many columns
    # in the grid the control will occupy. Using the -ColumnSpan parameter
    # changes the dependency property [Windows.Controls.Grid]::ColumnSpanProperty
    [Int]$ColumnSpan,
    # The -Width parameter will be used to set the width of the control               
    [Int]$Width,
    # The -Height parameter will be used to set the height of the control
    [Int]$Height,
    # If the control is a child element of a Canvas control (see New-Canvas),
    # the Top parameter controls the top location within that canvas
    # Using the -Top parameter changes the dependency property
    # [Windows.Controls.Canvas]::TopProperty
    [Double]$Top,
    # If the control is a child element of a Canvas control (see New-Canvas),
    # the Left parameter controls the left location within that canvas
    # Using the -Left parameter changes the dependency property
    # [Windows.Controls.Canvas]::LeftProperty
    [Double]$Left,
    # If the control is a child element of a Dock control (see New-Dock),
    # the Dock parameter controls the dock style within that panel
    # Using the -Dock parameter changes the dependency property
    # [Windows.Controls.DockPanel]::DockProperty
    [Windows.Controls.Dock]$Dock,
    # If Show is set, the UI will be displayed as a modal dialog within the current
    # thread.  If the -Show and -AsJob parameters are omitted, the control should be
    # output from the function
    [Switch]$Show,
    # If AsJob is set, the UI will displayed within a WPF job.
    [Switch]$AsJob       
    )
    # This is a quick systems monitor written in WPK for the 2010 Scripting Games.
    # Everything is enclosed within a grid.
    # Grids are a great choice for quick little user interfaces because they can be easily resized.
    # This grid has two columns (both of equal size), and it also has eight rows. All of the rows are Autosized,
    # except for the fifth row, which is the remainder of the available space.
    New-Grid -Name "SystemMonitor" -Columns 2 -Rows 'Auto','Auto','Auto','Auto',1*,'Auto','Auto','Auto' -Children {
        # The first row contains the username
        New-Label "Username"
        New-Label  -Column 1 "$env:Username"
        # The next row contains the logon time. 
        # I'll confess that this isn't the most accurate logon time method,
        # but it's the only reasonably concise method I've found that will work
        # regardless of if you're in a domain or running as a low-rights user
        New-Label -Row 1  "Logged On Since"
        New-Label -Row 1 -Column 1 -Name "LogonTime" (Get-Process Taskhost -ErrorAction SilentlyContinue |
            Select-Object -First 1 -ErrorAction SilentlyContinue -ExpandProperty StartTime)
        # The next row just contains a simple label. It uses another function from PowerShellPack,
        # Test-IsAdministrator, to help me quickly figure out if the current user is an administrator or not
        New-Label -Row 2 -Column 1 {
            if (Test-IsAdministrator) {
                "Administrator"
            } else {
                "Normal User"
            }       
        }
        # The next row is really easy. The left side is a label "Computer", and the right side
        # is the value of $env:ComputerName 
        New-Label -Row 3 "Computer"
        $computerName = $env:COMPUTERNAME   
        New-Label -Column 1 -Row 3 "$computerName"
        # Row 5 (the one that takes up all of the remaining available space) contains an expander that will
        # show the running programs. To make the display a little easier to use, the process ID will be put
        # next to the process name in a smaller font using something called a DataTemplate
        New-Expander "Programs" -MaxHeight 150 -ColumnSpan 2 -Row 4 -Name "Programs" {
            New-ListBox -ItemsSource $null -ItemTemplate {
                New-StackPanel -Orientation Horizontal -Children {
                    New-Label -Name ProcessName -FontSize 14
                    New-Label -Name Id -FontSize 8
                } | ConvertTo-DataTemplate -binding @{
                    "ProcessName.Content" = "ProcessName"
                    "Id.Content" = "Id"
                }               
            }
        }
        # Row 6 contains a progress bar that will show us the CPU use, and a label that will tell us what that progress bar does.
        # Now that just by putting a bigger value for -ZIndex, I can make the label appear on top of the progress bar
        New-ProgressBar -Row 5 -ColumnSpan 2 -Name "CPU"
        New-Label -ZIndex 1 -Row 5 -ColumnSpan 2 "% CPU in Use" -FontSize 12
        # Row 7 is the progress bar for memory consumption. Again, the label is also on the same row,
        # but has a higher ZIndex, so it will appear above the progress bar
        New-ProgressBar -Row 6 -ColumnSpan 2 -Name "Memory"
        New-Label -ZIndex 1 -Row 6 "% Memory Used" -FontSize 12   
        New-Label -ZIndex 1 -Row 6 -Column 1 -FontSize 10 -HorizontalAlignment Right -Name MemoryDetails -VerticalAlignment Top
        # Row 8 contains a pair of buttons: a manual refresh (although the program will refresh every few seconds anyways) and an exit button
        # Exit is easy, all you have to do is $window.Close()
        New-Button "E_xit" -Row 7 -Column 1 -On_Click {
            $window.Close()
        }
        # Refresh is a little trickier. Because I'll want to refresh on an interval, as well as whenever you click the button, I've made refresh
        New-Button "_Refresh" -Row 7 -On_Click {   
            New-Module $this.Parent.Resources.Commands
            Update $this.Parent
        }
    } -Resource @{
        # Because the UI will update periodically and when clicked, it makes sense to separate common
        # functionality into functions. By declaring a script block like this with all of the functions
        # that your UI will share, it's possible to simplify the development of projects in WPK.
        # In this case, I've written four helper functions:
        #     Update - the main function the UI calls to update
        #     Update-ProcessTreeView - refreshes the list of processes
        #     Update-CPUMonitor - refreshes the progress bar with % CPU utilization
        #     Update-MemoryMonitor - refreshes the memory progress bar and the memory details UI
        # Keeping commands in this format offers two distinct advantages:
        # - Commands in the same module can share variables
        # - Functions written like this can be debugged in the ISE when -Show is used
        #  (but be careful not to Stop Debugger, this will leave a phantom UI floating around)
        "Commands" = {           
            function Update-ProcessTreeView($root) {
                # Updating the process list is easy.  $root is really an expander, and $root's content is a list
                # Changing the ItemsSource of the list will update the process list
                ($root.Content.ItemsSource) = Get-Process
            }
            function Update-CPUMonitor($cpuProgressBar, $perfData) {
                # The CPU monitor is the easiest thing to update. The performance data is
                # already a percentage, so we just make sure the progress bar's value is the
                # cooked value
                $cpuProgressBar.Value = $perfData.CookedValue
            }
            function Update-MemoryMonitor($memoryProgressBar, $memoryDetails, $perfData) {           
                # The memory monitor is trickier for two reasons. First, the % available memory
                # has to be calculated from the two performance counters you can get (committed/available)
                # Second, the memory details need to be updated with a report of how much memory is used
                if (-not $perfData) { return }
                $committedMegs = $perfData[0].CookedValue / 1mb
                $availableMegs =$perfData[1].CookedValue / 1mb
                $memoryProgressBar.Value = $perfData[0].CookedValue * 100/
                    ($perfData[0].CookedValue + $perfData[1].CookedValue)
                $memoryDetails.Content = "$committedMegs MB Committed
                $availableMegs MB Available"
            }
            function Update($grid) {                       
                # The core update function is what will be called by the Update button
                # and the timed update.
               
                # Because it will be called a lot, it makes the most sense to cache the controls.
                # This can be done by using Get-Resource to find the "Controls" resource
                # (starting from the visual $grid). If no Controls resource is found,
                # Get-ChildControl can be used to locate the controls. Caching the controls
                # this way is significantly faster than locating them each time.
                $controls = Get-Resource "Controls" -Visual $grid
                if (-not $controls) {
                    $controls = @{
                        "CPU" = $grid |Get-ChildControl "CPU"
                        "Memory" = $grid | Get-ChildControl "Memory"
                        "MemoryDetails" = $grid | Get-ChildControl "MemoryDetails"
                        "Programs" = $grid | Get-ChildControl "Programs"
                    }
                    Set-Resource -Name "Controls" -Value $Controls -Visual $grid
                }
                # The data context of the grid will contain a series of counter samples
                # The first sample with the CPU, the second and third samples are committed
                # and available memory
                # Using the miracle of multiple assignment, we can simply write this as               
                $cpuData, $memoryData = $grid.DataContext.LastOutput.CounterSamples
                # $cpuData will contain the first item, $memoryData will contain the rest
                # Then we simply call each of the other update functions
                Update-CPUMonitor $controls.cpu $cpuData
                Update-MemoryMonitor $controls.memory $controls.memoryDetails $memoryData
                Update-ProcessTreeView $controls.programs
            }
        }
    } -DataContext {
        # Datacontext is a property commonly used for databinding in WPF.
        # In this case, DataContext will constantly run a script to collect the values of
        # three performance counters: % Processor Time, Committed Bytes,
        # and AvailableBytes of memory
        Get-PowerShellDataSource -Script {
            Get-Counter '\Processor(_total)\% Processor Time',
                '\Memory\Committed Bytes', '\Memory\Available Bytes' -Continuous        
        }   
    } -On_Loaded {
        # When the control is loaded, run an update every 2 seconds.
        # This update simply finds the SystemMonitor anywhere within the window,
        # imports the commands defined in the grid, and then calls update
        Register-PowerShellCommand -Name "Update" -In "0:0:2" -Run -ScriptBlock {
            $grid = $window |
                Get-ChildControl "SystemMonitor"
            New-Module (Get-Resource -Name Commands -Visual $grid)
            Update $grid
        }  
        # And update is called once to populate the list of running processes.       
        $grid = $this
        New-Module (Get-Resource Commands)
        Update $grid   
    } @psBoundParameters
}


Now that you’ve seen the script, let’s walk through each of the interesting areas one by one:

Section 1—Common parameters

Just like it’s important to remember that any script you write today in Windows PowerShell you may want to put into a larger script tomorrow, any UI you write once in Windows PowerShell may be a useful part of another UI tomorrow. As such, there’s a set of common parameters I’ve found that make it easy to make rebuildable UI functions, instead of throwaway UI scripts.

These parameters are:

-Name

The name of the control. Used to find the UI within a window.

-Width/-Height

Used to define a fixed size of a control, if needed (not recommended).

-Left/-Top

Used to define where the control will be, if the control is placed in a canvas.

-Row/-Column/-RowSpan/-ColumnSpan

Used to define where the control will be, if the control is placed in a grid.

-Dock

Used to define how the control will dock to other controls, if used within a Dock Panel.

-ZIndex

The Z-index of the control, which will be used to make sure the control appears above or below other controls.

-Show/-AsJob

Without either the –Show or –AsJob switch, your function should simply output the control. When –Show is specified, the window will be popped up and Windows PowerShell will wait until the window is closed (–Show  will only work in Windows PowerShell –sta and the ISE). When –AsJob is specified, the window will pop up and Windows PowerShell will continue executing.

 

If you look at the second to last line of the script, all of the parameters the user provided are passed down to the top-level UI control using a very handy Windows PowerShell 2.0 technique called splatting (this is actually the second year I’ve used splatting in my Scripting Games example; see Scripting Games 2009: Advanced Event 5). Splatting will use a hashtable for the arguments of a function, and it just so happens Windows PowerShell automatically populates a hashtable, $psBoundParameters, with what was actually provided to the function. Because New-Grid (or any New-Control command in WPK) has all of the same ubiquitous parameters, splatting simply passes the values right on down.

If you add additional parameters to the function, make sure you remove them from $psBoundParameters before you splat them to your top-level control.

Section 2—UI Layout

In this function (and in most I end up writing), the top control is: New-Grid.  I find grids to be an incredibly rich container provided by WPF, but you can watch this video to learn about all of the different types of containers available to use in WPK.

I find grids to be great because you can draw them out several times on a piece of paper or a whiteboard before you start (even if you have my limited artistic skills). Make sure that any control you want to update dynamically has a –Name property defined.

Let’s go through the layout, row by row. Bear in mind while reading the code that rows are 0-indexed. (-Row 1 is on row 2).

Row 1 contains a label on the left, and a username on the right. The username is gotten quickly and easily, by using $env:Username (the environmental variable, UserName).

Row 2 contains a logged on since label, and a vicious hack of logon time on the right. This uses a well-known process that low-rights users can see, and was the shortest way I have found of getting a logon time that I can see as a low-rights user. Bear in mind that this is only what I could find in a short amount of research that would work in a short amount of code, and that no one should ever use this as an authoritative “last logon time.”

Row 3 is a little more sparse. It uses another function that comes with the PowerShellPack (Test-IsAdministrator) to show the string “Administrator” if the user is an administrator or “Normal User” if they are not.

Row 4 is also simple. It contains a label for the computer, and on the right, a string with the short WINS name of the computer.

Row 5 gets a little more complicated. This is our process view. It spans both columns and is a control called an “expander”. Expanders contain a heading and content. In this case, the heading is “Programs”, and the content is a list box that will display the process name in 12-point font and the process ID in 10-point font.

Row 6 is pretty simple but has a new control and a new trick. The new control is a progress bar, which simply shows a progress bar to the user. The other control on this row is a label “% CPU in Use”. This label has a ZIndex of 1, which makes sure that it’s above the progress bar.

Row 7 is a lot like row 6. The progress bar has a different name, and there’s one more label as well. The new label has a name, -MemoryDetails, and no content. We’ll put some content in it later, when we have data.

Row 8 is a pair of buttons. On the left is Refresh, and on the right is Exit. If you look at the script, you’ll see a _ before each of the letters. This is a pretty universal convention in Windows UI. It means that if you press ALT + that key, the button will be clicked.

In the systems monitor, the grid is pretty simple: It needs two columns of equal width (-Columns 2), and eight rows, seven of which are as big as they need to be (‘Auto’), and one of which is the remaining space (1*).

The grid contains mainly simple UI elements. It has several labels (some used as captions, others used to hold values), an expander that holds the list of processes, and a pair of progress bars to show the CPU utilization and memory consumption. 

The last row of items on the grid contains a pair of buttons: one that will exit the UI, and another to refresh the content.

Section 3—Common commands

A trick I often use to share functionality between different parts of a UI is packaging common functionality (or common data, like additional parameters to the function) into the UI’s –Resource parameter. The system monitor’s resource parameter simply contains four helper functions (three that update parts of the UI and one that calls the other three).

By using common code in this way, several scenarios become easy:

  • Creating multiple buttons/menu items to do the same thing
  • Performing a UI action on an interval with Register/Start-PowerShellCommand
  • Debugging parts of the UI as it runs (just put a breakpoint in the functions, and be careful not to Stop Debugging).
  • Controlling the UI from your main Windows PowerShell script with Update-WPFJob

In this case, we make use of the first two advantages (the Update button and the update that happens every two seconds both call the same Update function). I made use of the debugging trick at least once or twice while making the script.

Each of the update functions is simple and documented, so I won’t bother going into that much depth on them here.

Section 4—Background data

A significant percentage of the UI that I have made in Windows PowerShell involves a degree of background data processing. When you need to process background data in WPK, you can do so with the –DataContext parameter and Get-PowerShellDataSource. In this case, the data source will run using the built-in Windows PowerShell 2.0 cmdlet Get-Counter to collect performance data continuously. This is possible with this nifty Windows PowerShell one-liner:

Get-Counter '\Processor(_total)\% Processor Time', '\Memory\Committed Bytes', '\Memory\Available Bytes' -Continuous        


If you’re writing your own UI, you can either use the –DataBinding parameter to bind to certain properties from the UI, or you can sift through the results in an update, the way the System’s Monitoring script does.

Section 5—Startup

The –On_Loaded event is called whenever the control is first loaded, and before it is displayed. Any last-minute processing happens here. In the systems monitor, the only thing left to do is start a timer to update the UI and do an initial update.

WPK’s Register-PowerShellCommand –Run –Once –In “0:0:2” will take care of running the update every couple of seconds. Because we put the common functionality into one place, all the timed event has to do is find the systems monitor, import the module in the resources, and run update.

The initial update has it easier. It knows what the root of the systems monitor UI is ($this), and can just import the commands and run update.

Wrapping up

Step back for a second and realize how cool Windows PowerShell is. In fewer than 250 lines of code, we’ve built a UI that does a half dozen things in a way that we can run both as a widget and as part of a bigger script. When you combine the rich front end of WPF with the power of Windows PowerShell, amazing things can happen in tiny amounts of time.

You can use this walkthrough of how I approached the systems monitor event as a guide to building reusable scripts in WPK, or you can copy and paste pieces of it, adding on new things as needed. I hope this opens your eyes to the possibilities of scripted UI.

The following image shows the results of running the script.

Image showing results of running script 

 

Advanced Event 7 (VBScript)

Photo of Jeremy Engel

Jeremy Engel, Senior Systems Engineer

MCITP: Enterprise Administrator

MCITP: Enterprise Messaging Administrator 2010

 

As a general rule of thumb, I find it best to do everything asked of me in a test, even the bonus questions. It may have something to do with a misguided masochistic sense of pride, but on the whole I feel that if someone asks you to solve a problem, it is solvable. Side-stepping the metaphysical questions that statement poses, I will now direct your attention to my script.

The biggest challenge I faced when writing this script was getting accurate information from the computers. The computer information wasn’t a problem at all, but when it came to determining the currently logged on users, it was a lot harder than I first expected. I started off using the WMI class Win32_NetworkLoginProfile, and that worked great. Or so I thought. But as I began to analyze the data, I was getting I found a bunch of problems:

·         The LastLogon property returned ridiculous information.

·         The LastLogoff property returned essentially Null information.

·         It returned all users and not just the ones currently logged on.

·         Remote query was even more useless as all the information came back Null.


For that last one, I tried changing my WMI query privileges, but decided it was useless as all the other data I was getting.

So…I cheated. I know, I know. I feel horrible. But it dawned on me that each currently logged-in user should have an explorer.exe process of their very own. I know that there are instances when one explorer.exe process got buggy and another one started, or if an administrator is bored, he might randomly kill his friends’ explorer.exe processes, which would cause issues with my script. But hey, you have bigger issues to worry about than my script if you’re having Windows Explorer issues. Now I just needed to see if the Win32_Process class had what I needed. So I pulled out my handy dandy Windows PowerShell and did the following:

$process = Get-WmiObject Win32_Process | Where-Object {$_.Name -eq "explorer.exe"}
$process | Get-Member
$process.CreationDate
$process.GetOwner() | Get-Member


Why did I go over to the Dark Side and user Windows PowerShell? Well, for starters, Windows PowerShell rocks. But what I really love about Windows PowerShell is its transparency. What I mean by that is that there is this wonderful little gem of a function called Get-Member. It’s basically a shorthand MSDN library. I can look up a class, get its members, and play with it to figure out what it can do for me. It makes coding so much faster, even when I’m coding in VBScript!

Because I couldn’t use the Privileges property from Win32_NetworkLoginProfile, I had to make my own eye-rollingly more complicated function to figure out what the user’s privileges were. I also wrote a function to convert the WMI DateTime strings into a real Date, and then another function to get how long the user had been logged on. With allowing remote computer queries, I wanted to do some error checking on the computer name, so I added an IsPingable test function. Finally, I added the operating system and system uptime fields, and then wrapped it in a pretty HTA bow.

Here is the complete script.

<html>

<!-- Created by Jeremy Engel -->

<head>
<title>System Information</title>
<HTA:APPLICATION
  APPLICATIONNAME="System Information"
  ID="SystemInformation"
  ICON=""
  SCROLL="yes"
  SINGLEINSTANCE="yes"
  NAVIGABLE="yes"
  CAPTION="yes"/>
</head>

<style>

body { background-color:#659EC7;font-family:Cambria;font-size:11pt;margin-top:10px;margin-left:20px;margin-right:20px;margin-bottom:10px; }
button { margin:0px 7px 0px 0px; cursor:hand; width:80px }
input .text { background-color:Buttonface; }

</style>

<script language="VBScript">

Option Explicit

Dim compName, strDomain, objWMISvc, dtsBoot
Dim userTable : Set userTable = CreateObject("Scripting.Dictionary")

Private Function IsPingable(ByVal strHost)
  IsPingable = False
  Dim objFSO   : Set objFSO = CreateObject("Scripting.FileSystemObject")
  Dim objShell : Set objShell = CreateObject("WScript.Shell")
  Dim pingText : pingText = objShell.ExpandEnvironmentStrings("%TEMP%")&"\pingtest.txt"
  objShell.Run "cmd /c ipconfig /flushdns",0,True
  objShell.Run "cmd /c ping -a -n 1 -4 "&strHost&" > "&pingText,0,True
  Dim strPingText : strPingText = objFSO.OpenTextFile(pingText,1).ReadAll
  If InStr(strPingText,"unreachable") > 0 Then Exit Function
  Dim s : s = InStr(strPingText,"(")+1
  If s > 1 Then
    Dim e : e = InStr(s,strPingText,"%")
    Dim intLoss : intLoss = CInt(Mid(strPingText,s,e-s))
    If intLoss = 0 Then IsPingable = True
  End If
  objFSO.DeleteFile(pingText)
End Function

Private Function ConvertDateTime(ByVal strDate)
  Dim strYear  : strYear = Left(strDate,4)
  Dim strMonth : strMonth = Mid(strDate,5,2)
  Dim strDay   : strDay = Mid(strDate,7,2)
  Dim strHour  : strHour = Mid(strDate,9,2)
  Dim strMin   : strMin = Mid(strDate,11,2)
  Dim strSec   : strSec = Mid(strDate,13,2)
  ConvertDateTime = CDate(strMonth&"/"&strDay&"/"&strYear&" "&strHour&":"&strMin&":"&strSec)
End Function

Private Function GetDuration(ByVal intTime)
  Dim strDays : strDays = "0"
  Dim strHours : strHours = "0"
  Dim strMins : strMins = "0"
  If intTime >= 86400 Then
    strDays = CInt(intTime/86400)
    intTime = intTime Mod 86400
  End If
  If intTime >= 3600 Then
    strHours = CInt(intTime/3600)
    intTime = intTime Mod 3600
  End If
  If intTime >= 60 Then
    strMins = CInt(intTime/60)
    intTime = intTime Mod 60
  End If
  GetDuration = strDays&"d"&strHours&"h"&strMins&"m"&intTime&"s"
End Function

Private Function GetCPUUse
  Dim objProc, colList : Set colList = objWMISvc.ExecQuery("Select * from Win32_PerfFormattedData_PerfOS_Processor Where Name='_Total'",,48)
  For Each objProc In colList : GetCPUUse = objProc.PercentProcessorTime : Next
End Function

Private Function GetPrivileges(ByVal strUser)
  GetPrivileges = "User"
  Dim objMember, objGroup : Set objGroup = GetObject("WinNT://"&compName&"/Administrators,group")
  For Each objMember In objGroup.Members
    If objMember.Name = strUser Then
      GetPrivileges = "Administrator"
      Exit Function
    End If
    On Error Resume Next
    Dim objUser, intType : intType = VarType(objMember.Members)
    On Error Goto 0
    If intType = 9 Then
      For Each objUser In objMember.Members
        If objUser.Name = strUser Then
          GetPrivileges = "Administrator"
          Exit Function
        End If
      Next
    End If
  Next
End Function

Private Sub DisplayComputerInfo(ByVal isRepeat)
  Dim objOS, colOSes : Set colOSes = objWMISvc.ExecQuery("Select * from Win32_OperatingSystem",,48)
  For Each objOS in colOSes
    If Not isRepeat Then
      OS.InnerHTML = objOS.Caption
      dtsBoot = ConvertDateTime(objOS.LastBootUpTime)
    End If
    RAM.InnerHTML = Round(objOS.TotalVisibleMemorySize/(1024^2),2)&" GB ("&Round(objOS.FreePhysicalMemory/(1024^2),2)&" GB Free)"
  Next
  If Not isRepeat Then
    Dim objComputer : Set objComputer = GetObject("winmgmts:{impersonationLevel=Impersonate}!\\"&compName&"\root\cimv2:Win32_ComputerSystem.Name='"&compName&"'")
    strDomain = objComputer.Domain
    Domain.InnerHTML = strDomain
  End If
  CPUUse.InnerHTML = GetCPUUse&"%"
  Dim procList : Set procList = objWMISvc.ExecQuery("Select * from Win32_Process")
  ProcessCount.InnerHTML = procList.Count
  SystemUptime.InnerHTML = GetDuration(DateDiff("s",dtsBoot,Now))
End Sub

Private Sub DisplayUserInfo(ByVal isRepeat)
  If Not isRepeat Then
    userTable.RemoveAll
    Dim explorer, explorers : Set explorers = objWMISvc.ExecQuery("Select * from Win32_Process Where Name='explorer.exe'")
    If explorers.Count = 0 Then
      UserInfo.InnerHTML = "There are no users currently logged into this computer."
      Exit Sub
    End If
    For Each explorer In explorers
      Dim strUser, strDomain
      explorer.GetOwner strUser,strDomain
      userTable.Add strDomain&"\"&strUser,Array(GetPrivileges(strUser),ConvertDateTime(explorer.CreationDate))
    Next
  End If
  UserInfo.InnerHTML = ""
  For Each strUser In userTable
    UserInfo.InnerHTML = UserInfo.InnerHTML&"<b>User Name:&nbsp;</b>"&strUser&"<br><b>User Level:&nbsp;</b>"&userTable(strUser)(0)&"<br>"&_
                         "<b>Logon Time:&nbsp;</b>"&GetDuration(DateDiff("s",userTable(strUser)(1),Now))&"<p>"
  Next
End Sub

Private Sub ShowLoading
  Domain.InnerHTML = "<i>Loading...</i>"
  OS.InnerHTML = "<i>Loading...</i>"
  RAM.InnerHTML = "<i>Loading...</i>"
  CPUUse.InnerHTML = "<i>Loading...</i>"
  ProcessCount.InnerHTML = "<i>Loading...</i>"
  SystemUptime.InnerHTML = "<i>Loading...</i>"
  UserInfo.InnerHTML = "<br><i>Loading...</i>"
  CreateObject("WScript.Shell").Run "cmd /c",0,True ' Just a wee little pause to let the page load
End Sub

Private Sub RefreshInfo
  Document.Body.Style.Cursor = "wait"
  Dim isRepeat : isRepeat = True
  If ComputerName.Value <> compName Then
    If Not IsPingable(ComputerName.Value) Then
      MsgBox "ERROR: "&ComputerName.Value&" is either offline or not a valid computer name on this network."
      ComputerName.Value = compName
      Document.Body.Style.Cursor = "default"
      Exit Sub
    End If
    isRepeat = False
    compName = ComputerName.Value
    Set objWMISvc = GetObject("winmgmts:{impersonationLevel=impersonate,(Restore,Security)}!\\"&compName&"\root\cimv2")
    ShowLoading
  End If
  DisplayComputerInfo isRepeat
  DisplayUserInfo isRepeat
  Document.Body.Style.Cursor = "default"
End Sub

Public Sub Window_OnLoad
  Dim w : w = 550
  Dim l : l = 400
  Dim x1 : x1 = (window.screen.width-w)/2
  Dim y1 : y1 = (window.screen.height-l)/2
  window.ResizeTo w,l
  window.moveTo x1,y1
  Dim objNetwork : Set objNetwork = CreateObject("Wscript.Network")
  ComputerName.Value = objNetwork.ComputerName
  ShowLoading
  RefreshInfo
End Sub

</script>

<body>
  <table width="100%">
    <tr>
      <td width="70%">
        <b>Computer Name:&nbsp;</b><input class="text" type="text" name="ComputerName" size="20" maxLength="20">
      </td>
      <td width="30%" align="right">
        <button type="button" id="RefreshButton" onClick="RefreshInfo">Refresh</button>
      </td>
    </tr>
  </table><p>
  <table width="100%">
    <tr>
      <b style="font-size:14pt">Computer Information<b><hr>
      <td width="100%" valign="top">
        <b>Domain Name:&nbsp;</b><span id="Domain"></span><br>
        <b>Operating System:&nbsp;</b><span id="OS"></span><br>
        <b>Physical Memory:&nbsp;</b><span id="RAM"></span><br>
        <b>CPU Utilization:&nbsp;</b><span id="CPUUse"></span><br>
        <b>Process Count:&nbsp;</b><span id="ProcessCount"></span><br>
        <b>System Uptime:&nbsp;</b><span id="SystemUptime"></span><br>
      </td>
    </tr>
  </table><p>
  <table width="100%">
    <tr>
      <b style="font-size:14pt">User Information<b><hr>
      <td width="100%" valign="top"><div height="100%" id="UserInfo"></div></td>
    </tr>
  </table>
</body>

</html>



The following image shows the results of running the script.

Image showing results of running script 


If you want to know exactly what we will be looking at tomorrow, follow us on
Twitter or Facebook. If you have any questions, send e-mail to us at scripter@microsoft.com or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

 

Ed Wilson and Craig Liebendorfer, Scripting Guys