Today’s scripting challenge…
We are attempting to use SCCM 2012 as a patch management solution for our centrally supported third party applications. Great new features in SCCM 2012 allow us to write detection rules for applications to determine if superseded versions are present on the client system, and to trigger an immediate upgrade. Cool Beans. Problem is, a lot of application installers that ran reliably in our MDT “LiteTouch” environment (which is used to deploy new operating systems with no previously installed software) will not run silently or successfully on systems where previous application versions were already installed, and may currently be running.
This is an old problem for client system management… how can you update in-use files? In most cases I have seen, the admin will schedule the updates to run when no one is logged in. Unfortunately, this is an edge case for us. Most systems are off when no one is logged in. Another system is to force logoff for application updates. While this would work, it seems like a “heavy” solution… why force the user to log off to update one application that may or may not be running? Why force all applications closed on the off chance that one application will need to be terminated.
Our solution? Kill only the processes that need to be terminated to ensure application installation success. See the VBScript solution below (I flirted with writing this one in PowerShell, but the code signing requirements still intimidate me, and I may have the odd-duck XP client that still does not have PowerShell). I have tested the script on Firefox, Thunderbird, VLC, Notepad++, WinSCP, Filezilla, and KeePass. Rock On!
UPDATE: Since initial publication, I have added some logic to handle execution from “wscript”. If the script is executed from wscript.exe, console output will be suppressed. Additionally, the log file now is named “killAndExec-(exeFileName).log”. (This prevents SCCM from overwriting the log file the next time a program installer runs that also uses this script).
'KillAndExec.vbs script, J. Greg Mackinnon, 2012-09-13
' Kills processes named in the "kill" argument (comma-delimited)
' Runs the executable named in the "exec" argument
' Appends the executable arguments specified in the "args" argument (comma-delimited)
'Requires: "kill" and "exec" arguments. The executable named in the "exec" arg must be in the same directory as this script.
'Provides:
' RC=101 - Error terminating the requests processes
' RC=100 - Invalid input parameters
' Other return codes - Pass-though of return code from WShell.Exec.Run using the provided input parameters
Option Explicit
const quote = """"
'Declare Variables:
Dim aExeArgs, aKills
Dim bBadArg, bNoArgs, bNoExeArg, bNoExec, bNoKill, bNoKillArg
Dim cScrArgs
Dim iReturn
Dim oShell, oFS, oLog
Dim sBadArg, sCmd, sExe, sExeArg, sKill, sLog, sScrArg, sTemp
'Set initial values:
bBadArg = false
bNoArgs = false
bNoExeArg = false
bNoExec = false
bNoKill = false
bNoKillArg = false
iReturn = 0
'Instantiate Global Objects:
Set oShell = CreateObject("WScript.Shell")
Set oFS = CreateObject("Scripting.FileSystemObject")
'''''''''''''''''''''''''''''''''''''''''''''''''''
' Define Functions
'
Sub subHelp
echoAndLog "KillAndExec.vbs Script"
echoAndLog "by J. Greg Mackinnon, University of Vermont"
echoAndLog ""
echoAndLog "Kills named processes and runs the provided executable."
echoAndLog "Logs output to 'KillAndExec.vbs' in the %temp% directory."
echoAndLog ""
echoAndLog "Required arguments and syntax:"
echoAndLog "/kill:""[process1];[process2]..."""
echoAndLog " Specify the image name of one or more processes to terminate."
echoAndLog "/exe:""[ExecutableFile.exe]"""
echoAndLog " Specify the name of the executable to run."
echoAndLog ""
echoAndLog "Optional arguments:"
echoAndLog "/args""[arg1];[arg2];[arg3]..."""
echoAndLog " Specify one or more arguments to pass to the executable."
echoAndLog "/noKill"
echoAndLog " Switch to suppress default process termination. Used for testing."
echoAndLog "/noExec"
echoAndLog " Switch to suppress default program execution. USed for testing."
End Sub
function echoAndLog(sText)
'EchoAndLog Function:
' Writes string data provided by "sText" to the console and to Log file
' Requires:
' sText - a string containig text to write
' oLog - a pre-existing Scripting.FileSystemObject.OpenTextFile object
'If we are in cscript, then echo output to the command line:
If LCase( Right( WScript.FullName, 12 ) ) = "\cscript.exe" Then
wscript.echo sText
end if
'Write output to log either way:
oLog.writeLine sText
end function
function fKillProcs(aKills)
' Requires:
' aKills - an array of strings, with each entry being the name of a running process.
Dim cProcs
Dim sProc, sQuery
Dim oWMISvc, oProc
Set oWMISvc = GetObject("winmgmts:{impersonationLevel=impersonate, (Debug)}\\.\root\cimv2")
sQuery = "Select Name from Win32_Process Where " 'Root query, will be expanded.
'Complete the query string using process names in "aKill"
for each sProc in aKills
sQuery = sQuery & "Name = '" & sProc & "' OR "
next
'Remove the trailing " OR" from the query string
sQuery = Left(sQuery,Len(sQuery)-3)
'Create a collection of processes named in the constructed WQL query
Set cProcs = oWMISvc.ExecQuery(sQuery, "WQL", 48)
echoAndLog vbCrLf & "----------------------------------"
echoAndLog "Checking for processes to terminate..."
'Set this to look for errors that aren't fatal when killing processes.
On Error Resume Next
'Cycle through found problematic processes and kill them.
For Each oProc in cProcs
echoAndLog "Found process " & oProc.Name & "."
oProc.Terminate()
Select Case Err.Number
Case 0
echoAndLog "Killed process " & oProc.Name & "."
Err.Clear
Case -2147217406
echoAndLog "Process " & oProc.Name & " already closed."
Err.Clear
Case Else
echoAndLog "Could not kill process " & oProc.Name & "! Aborting Script!"
echoAndLog "Error Number: " & Err.Number
echoAndLog "Error Description: " & Err.Description
echoAndLog "Finished process termination function with error."
echoAndLog "----------------------------------"
echoAndLog vbCrLf & "Kill and Exec script finished."
echoAndLog "**********************************" & vbCrLf
WScript.Quit(101)
End Select
Next
'Resume normal error handling.
On Error Goto 0
echoAndLog "Finished process termination function."
echoAndLog "----------------------------------"
end function
function fGetHlpMsg(sReturn)
' Gets known help message content for the return code provided in "sReturn".
' Requires:
' Existing WScript.Shell object named "oShell"
Dim sCmd, sLine, sOut
Dim oExec
sCmd = "net.exe helpmsg " & sReturn
echoAndLog "Help Text for Return Code:"
set oExec = oShell.Exec(sCmd)
Do While oExec.StdOut.AtEndOfStream True
sLine = oExec.StdOut.ReadLine
sOut = sOut & sLine
Loop
fGetHlpMsg = sOut
end function
'
' End Define Functions
'''''''''''''''''''''''''''''''''''''''''''''''''''
'''''''''''''''''''''''''''''''''''''''''''''''''''
' Parse Arguments
If WScript.Arguments.Named.Count > 0 Then
Set cScrArgs = WScript.Arguments.Named
For Each sScrArg in cScrArgs
Select Case LCase(sScrArg)
Case "nokill"
bNoKill = true
Case "noexec"
bNoExec = true
Case "kill"
aKills = Split(cScrArgs.Item(sScrArg), ";", -1, 1)
Case "exe"
sExe = cScrArgs.Item(sScrArg)
Case "args"
aExeArgs = Split(cScrArgs.Item(sScrArg), ";", -1 ,1)
Case Else
bBadArg = True
sBadArg = sScrArg
End Select
Next
If (IsNull(sExe) or IsEmpty(sExe)) Then
bNoExeArg = True
ElseIf (IsNull(aKills) or IsEmpty(aKills)) Then
bNoKillArg = True
End If
ElseIf WScript.Arguments.Named.Count = 0 Then 'Detect if required args are not defined.
bNoArgs = True
End If
' End Argument Parsing
'''''''''''''''''''''''''''''''''''''''''''''''''''
'''''''''''''''''''''''''''''''''''''''''''''''''''
' Initialize Logging
sTemp = oShell.ExpandEnvironmentStrings("%TEMP%")
sLog = "killAndExec-" & sExe & ".log"
Set oLog = oFS.OpenTextFile(sTemp & "\" & sLog, 2, True)
' End Initialize Logging
'''''''''''''''''''''''''''''''''''''''''''''''''''
'''''''''''''''''''''''''''''''''''''''''''''''''''
' Process Arguments
if bBadArg then
echoAndLog vbCrLf & "Unknown switch or argument: " & sBadArg & "."
echoAndLog "**********************************" & vbCrLf
subHelp
WScript.Quit(100)
elseif bNoArgs then
echoAndLog vbCrLf & "Required arguments were not specified."
echoAndLog "**********************************" & vbCrLf
subHelp
WScript.Quit(100)
elseif bNoExeArg then
echoAndLog "Required argument 'exe' was not provided."
echoAndLog "**********************************" & vbCrLf
subHelp
wscript.quit(100)
elseif bNoKillArg then
echoAndLog "Required argument 'kill' was not provided."
echoAndLog "**********************************" & vbCrLf
subHelp
wscript.quit(100)
end if
' Log processes to kill:
for each sKill in aKills
echoAndLog "Process to kill: " & sKill
next
' Log executable arguments:
echoAndLog "Executable to run: " & sExe
if not (IsNull(aExeArgs) or IsEmpty(aExeArgs)) then
for each sExeArg in aExeArgs
echoAndLog "Executable argument: " & sExeArg
next
else
echoAndLog "Executable has no provided arguments."
end if
' End Process Arguments
'''''''''''''''''''''''''''''''''''''''''''''''''''
'''''''''''''''''''''''''''''''''''''''''''''''''''
'Begin Main
'
'Build full command string:
if inStr(sExe," ") then 'Spaces in the exe file
sExe = quote & sExe & quote 'Add quotations around the executable.
end if
if not (IsNull(aExeArgs) or IsEmpty(aExeArgs)) then
sCmd = sExe & " "
for each sExeArg in aExeArgs
if inStr(sExeArg," ") then
sExeArg = quote & sExeArg & quote 'Add quotations around the argument.
end if
sCmd = sCmd & sExeArg & " "
next
else
sCmd = sExe
end if
echoAndLog "Command to execute:"
echoAndLog sCmd
'Kill requested processes:
if bNoKill = false then
fKillProcs aKills
else
echoAndLog "/noKill switch has been set. Processes will not be terminated."
end if
'Run the requested command:
echoAndLog vbCrLf & "----------------------------------"
if bNoExec = false then
echoAndLog "Running the command..."
on error resume next 'Disable exit on error to allow capture of oShell.Run execution problems.
iReturn = oShell.Run(sCmd,10,True)
if err.number 0 then 'Gather error data if oShell.Run failed.
echoAndLog "Error: " & Err.Number
echoAndLog "Error (Hex): " & Hex(Err.Number)
echoAndLog "Source: " & Err.Source
echoAndLog "Description: " & Err.Description
iReturn = Err.Number
Err.Clear
wscript.quit(iReturn)
end if
on error goto 0
echoAndLog "Return code from the command: " & iReturn
if iReturn 0 then 'If the command returned a non-zero code, then get help for the code:
fGetHlpMsg iReturn
end if
else
echoAndLog "/noExec switch has been set. Executable will not run."
end if
echoAndLog "----------------------------------"
oLog.Close
wscript.quit(iReturn)
'
' End Main
'''''''''''''''''''''''''''''''''''''''''''''''''''