Wednesday, February 23, 2011

Launch IE and Wait for it to be Closed

I really thought this was going to be easier than it turns out to be.  First off, the Wscript.Shell RUN() method is useless with IE when it comes to specifying Wait = True.  It ignores it.  If you make one script call another (via Wscript.Run or Execute, doesn't matter) it works, but not if you deploy the script "per-user" via SCCM, in which case, again, the Wait = True is ignored in the parent script.  So, script A launches script B, but instead of waiting for B to complete, script A just continues on while B is still working.  NOT what I want to happen.  The net result is crap and breaks the workflow entirely.  The solution is WMI and the Win32_ProcessStartup class (and a little scripting and some coffee).


The way this works is that it assumes the web page (running on a trusted intranet server) takes user input via a form and then evokes some sort of modification to the user account in Active Directory via an LDAP expression.  As an example, I'm using an account attribute called "customAttrib" and checking if it is empty (is-null or empty-string) or contains a string value (more than one character).  If the value is empty, the web form is launched and the script waits until the user closes the IE session.  It doesn't matter how many other IE windows or tabs are open.  It fetches the processID for the one it launches and watches to see when it vanishes from the process stack (using a do-while loop).  When the process is closed, the script continues and simply re-checks the attribute to see if it was modified.  The end-game being that it checks if the user successfully completed the form, or simply closed it and tried to ignore it (bad news for the user).  Enjoy!


'****************************************************************
' Filename..: IE_Wait.vbs
' Author....: skatterbrainz.blogspot.com (you know who)
' Date......: 02/23/2011
' Purpose...: launch web page and wait for it to close
'****************************************************************
' comment: check for previous attrib in AD (LDAP query)
' comment: launch ie and navigate to the web page
' comment: check AD again to see if user completed the form
' comment: if not, force a logoff
'****************************************************************


Const enableLogoff = True
Const dndc = "LDAP://DC=contoso,DC=msft"


'----------------------------------------------------------------
' comment: DO NOT MODIFY ANY CODE BELOW THIS POINT!!!
'----------------------------------------------------------------


Const ADS_SCOPE_SUBTREE = 2
Const SW_HIDE = 0
Const SW_NORMAL = 1
Const SW_SHOWMINIMIZED = 2
Const SW_SHOWMAXIMIZED = 3


Const wbemFlagForwardOnly = 32
Const wbemFlagBidirectional = 0
Const wbemFlagReturnImmediately = 16
Const wbemFlagReturnWhenComplete = 0
Const wbemQueryFlagPrototype = 2
Const wbemFlagUseAmendedQualifiers = 131072


Const osLogoff = 0
Const osForcedLogoff = 4
Const osShutdown = 1
Const osForcedShutdown = 5
Const osRestart = 2
Const osForcedRestart = 6


Const strCommand = "C:\Program Files\Internet Explorer\iexplore.exe http://intranet.contoso.msft/stuff"


Dim wshNetwork, uid, objShell, groupPriority, wmi_flags


wmi_flags = wbemFlagForwardOnly + wbemFlagReturnImmediately


Set wshNetwork = CreateObject("Wscript.Network")
Set objShell   = CreateObject("Wscript.Shell")


uid = wshNetwork.UserName
Set wshNetwork = Nothing


custVal = GetAttribute(uid, "customAttrib")


If IsNull(groupPriority) Then   
    LaunchWebForm()


    custVal = GetAttribute(uid, "customAttrib")


    If IsNull(custVal) Then
        MsgBox "Form was not filled out properly!" & _
            vbCRLF & "You will now be logged off...", vbOkOnly+vbCritical, "Web Form"
            
        If enableLogoff = True Then
            Logoff()
        End If
        
    End If
End If


Sub LaunchWebForm()
    Dim objWMIService, objStartup, objConfig, objProcess
    Dim intReturn, query, colItems, objItem, intProcessID
    Dim colMonitoredProcesses, objLatestProcess, processEnded
    
    Set objWMIService = GetObject("winmgmts:" _
        & "{impersonationLevel=impersonate}!\\.\root\cimv2")


    ' comment: configure the new process as visible
    Set objStartup = objWMIService.Get("Win32_ProcessStartup")
    Set objConfig = objStartup.SpawnInstance_
    objConfig.ShowWindow = SW_NORMAL


    ' comment: create a new process (iexplore.exe)
    Set objProcess = objWMIService.Get("Win32_Process")
    intReturn = objProcess.Create(strCommand, Null, objConfig, intProcessID)


    If intReturn <> 0 Then
        'wscript.echo "fail: unable to launch process!"
        wscript.quit(1)
    End If



    'wscript.echo "info: process id is " & intProcessID


    Set objWMIService = GetObject("winmgmts:\\.\root\CIMV2") 


    query = "SELECT ProcessId FROM Win32_Process WHERE ProcessId='" & intProcessID & "'"


    Set colItems = objWMIService.ExecQuery(query,,wmi_flags) 
    For Each objItem in colItems
        intProcessID = objItem.ProcessId
    Next
        
    If intProcessID <> "" Then
        'wscript.echo "info: waiting for process terminate..."
        
        Set colMonitoredProcesses = objWMIService.ExecNotificationQuery _
            ("Select * From __InstanceDeletionEvent Within 1 Where TargetInstance ISA 'Win32_Process'")


        Do Until processEnded = True
            Set objLatestProcess = colMonitoredProcesses.NextEvent
            If objLatestProcess.TargetInstance.ProcessID = intProcessID Then
                processEnded = True
            End If
        Loop


        If processEnded = True Then
            'wscript.echo "info: process was terminated"
        End If
    Else
        'wscript.echo "fail: unable to obtain process id..."
    End If
End Sub


'----------------------------------------------------------------
' function: get LDAP user attribute from AD
'----------------------------------------------------------------


Function GetAttribute(uid, att)
    Dim query, objConnection, objCommand, objRecordSet, retval
    On Error Resume Next
    
    query = "SELECT " & att & " FROM '" & dndc & "' " & _
        "WHERE objectCategory='user' AND sAMAccountName='" & uid & "'"
    
    Set objConnection = CreateObject("ADODB.Connection")
    Set objCommand    = CreateObject("ADODB.Command")
    
    objConnection.Provider = "ADsDSOObject"
    objConnection.Open "Active Directory Provider"
    
    Set objCommand.ActiveConnection = objConnection
    
    objCommand.Properties("Page Size") = 1000
    objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE
    
    objCommand.CommandText = query
    
    Set objRecordSet = objCommand.Execute
    
    objRecordSet.MoveFirst
    Do Until objRecordSet.EOF
        retval = objRecordSet.Fields(att).value    
        objRecordSet.MoveNext
    Loop
    GetAttribute = retval
End Function


'----------------------------------------------------------------
' function: force logoff from local computer
'----------------------------------------------------------------


Function Logoff()
    Logoff = -1
    wscript.Echo "Logging off..."
    On Error Resume Next
    Set objWMI = GetObject("winmgmts:{impersonationLevel=impersonate,(Shutdown)}!\\.\root\cimv2")
    Set colOs = objWMI.ExecQuery("Select * from Win32_OperatingSystem")
    If err.Number = 0 Then
        For Each objOs in colOs
            ' See: http://msdn.microsoft.com/en-us/library/aa394058(VS.85).aspx
            Logoff = objOs.Win32Shutdown(osForcedLogoff,0)
            ' WScript.Echo objOs.Name
        Next
    End If
End Function

1 comment:

Natalie said...

Hi David,

Great script! It's the only one I can find to monitor and wait until a command is done before it moves on. :)

However, one little problem: it isn't waiting in my script. It shows me the process ID but nothing more.

I added some debug echoes and confirm it gets here:

If intProcessID <> "" Then

It also executes the echo statement(s) next, but it does not seem to go past the next line:

Set colMonitoredProcesses = objWMIService.ExecNotificationQuery _
& ("Select * From __InstanceDeletionEvent Within 1 Where TargetInstance ISA 'Win32_Process'")


Any ideas what I could be doing wrong?