Friday, May 8, 2009

Hunting for Kitchen Sinks with a Shotgun

This is probably going to end up being one of those "jumping the shark" moments for this blog. Maybe not. I was working on something recently and it morphed into something a little different. The net result seemed pretty interesting to me, so I thought I'd share it.

In simplest terms, it's a VBScript that churns through a specified folder and executes every single .EXE and .MSI file it finds in it. Hence the title of this post. On top of that, it wraps the files in associated command syntax to execute them each silently or unattended. It works great with .MSI and even .MSP files. But .EXE files are somewhat of an anomaly since vendors seem to avoid anything resembling a "standard" for how to invoke their stuff "silent". Since my day job is dealing with packaging software using Wise script, I see almost every "parameter" known. Some don't even support a "silent" install.

Not being content with just churning and burning, I also had to satisfy my insatiable appetite for logging the crap out of everything going on.  So each executed file has its own log output as well, which you can see when looking at the CommandWrap() function code later in this post.  In fact, after testing this mouse trap, if you run it from a CMD console or within a BAT file, you can redirect the ouput to a mother-ship log file by doing this…

C:> cscript /nologo batchrun.vbs >batchrun.log

Unlike my previous posts, I think I'm going to try to dissect this one a little bit more than usual. Break things down a bit and blabber on about what specific portions of code are doing. Where it morphed was in making it handle more than just .MSI files. But in addition to that, I ended up chasing this with PowerShell, just to see what that would look like.

So here's the VBScript code...

'****************************************************************
' Filename..: batchrun.vbs
' Author....: skatterbrainz (skatterbrainz.blogspot.com)
' Date......: 05/08/09
' Purpose...: run all apps in a given folder in batch-mode
' SQL.......: N/A
'****************************************************************
' Comments..: If [strFolder] is set to "" the script will look
' in the same folder where it resides.
' Set [TestMode] to False to make script active
'****************************************************************
' Notes.....: Carefully review [CommandWrap] function at the end
' of this script. You may need to modify some code.
'****************************************************************
Option Explicit
Dim strFolder : strFolder = "c:\"

Const FilesToRun = ".EXE .MSI .MSP .BAT"
Const TestMode = True

'----------------------------------------------------------------
' comment: DO NOT MODIFY CODE BELOW THIS POINT!!! (see notes)
'----------------------------------------------------------------
Dim t1, t2, runtime, fso, objFolder, shell, filename, objFile
Dim cmd, counter, retval, temp

If strFolder = "" Then
strFolder = ScriptFolder()
End If

If Right(strFolder, 1) <> "\" Then
strFolder = strFolder & "\"
End If

wscript.echo "info: reading folder -> " & strFolder

On Error Resume Next
Set fso = CreateObject("Scripting.FileSystemObject")
Set objFolder = fso.GetFolder(strFolder)
Set shell = CreateObject("Wscript.Shell")
If err.Number <> 0 Then
Set objFolder = Nothing
Set fso = Nothing
Set shell = Nothing
Wscript.Echo "error: script was unable to run (details below)"
Wscript.Echo err.Number & " / " & err.Description
err.Clear
Wscript.Quit(0)
End If
temp = shell.ExpandEnvironmentStrings("%temp%")
counter = 0
t1 = Timer
For each objFile in objFolder.Files
filename = Lcase(objFile.Name)
If InStr(1, FilesToRun, Ucase(Right(filename,4))) <> 0 Then
cmd = CommandWrap(strFolder & filename)
If TestMode <> True Then
If cmd <> "" Then
' run in normal window, wait for completion
retval = shell.Run(cmd, 1, True)
Wscript.Echo "info: return value was " & retval
End If
End If
Wscript.Echo "info: [cmd] " & cmd
counter = counter + 1
End If
Next

Set shell = Nothing
Set objFolder = Nothing
Set fso = Nothing

Wscript.Echo "info: " & counter & " files were discovered and executed"
t2 = Timer
runtime = (t2 - t1) * 1000
Wscript.Echo "info: total runtime was " & runtime & " msec"


Now, you can see I pulled out some functions to handle some of the chores. The first function, ScriptFolder(), simply returns the path of where the script itself resides. This is used only as a fallback in case you want to just drop the .VBS file into the folder along with the .MSI and other files and have it just find them and run them. It's one way to do it. Or you can specify the folder path inside the script code near the top:






Function ScriptFolder()
ScriptFolder = Replace(Wscript.ScriptFullName, Wscript.ScriptName, "")
End Function



The next function, simply called Quotes(), wraps a text string with outer double-quotes if the string contains a space character.  This is to avoid problems when executing each of the files with the path concatenated:


Function Quotes(strVal)
If InStr(1, strVal, " ") <> 0 Then
Quotes = Chr(34) & strVal & Chr(34)
Else
Quotes = strVal
End If
End Function


Finally, the function CommandWrap(), is what performs the task of wrapping each filename inside an execution-ready command syntax to run it “silent”.  For MSI and MSP files this is pretty simple.  The syntax is consistent.  For BAT and CMD files it’s just a matter of executing them since it’s up to the BAT/CMD file itself as to how “silent” it can run.  For EXE files, you will either need to just “know” how to call them, or grab a baseball bat and beat the living shit out of your vendor reps to give you an answer.  Here goes:



Function CommandWrap(filename)
' make sure files are enclosed in double-quotes if spaces exist!
Dim fn : fn = Quotes(filename)
' get base name to use for log filename concatenation
Dim bn : bn = fso.GetBaseName(filename)
' determine command request based on filename / type
Select Case Lcase(Right(filename, 4))
Case ".exe":
CommandWrap = "%comspec% /c start /wait " & fn & _
" /Silent >" & temp & "\" & bn & ".log"
Case ".msi":
CommandWrap = "msiexec /i " & fn & " /qb! " & _
"/Lpiwaeo " & temp & "\" & bn & ".log"
Case ".msp":
CommandWrap = "msiexec /p " & fn & " /qb! " & _
"/Lpiwaeo " & temp & "\" & bn & ".log"
Case ".bat", ".cmd":
CommandWrap = "%comspec% /c start /wait " & fn & _
" >" & temp & "\" & bn & ".log"
Case Else:
CommandWrap = ""
End Select
End Function


The last step in my un-methodical approach to code-writing was to build this same contraption in another language.  Doing it in KiXtart would look remarkably similar to the VBScript example actually, so I decide to do it in PowerShell v2.0 instead.  The code isn’t as fleshed out or robust, but it’s enough to show how different it turns out.  It’s also much more compact:



$folderpath = "c:"
$filetypes = ".msi .exe .bat"

$list = -split($filetypes)
foreach ($filetype in $list) {
$quote = [char]34
$folder = get-childitem $folderpath
$files = $folder | where {$_.extension -eq $filetype}
foreach ($file in $files) {
$filename = $file.name
$filepath = $quote+$folderpath+'\'+$filename+$quote
$end = $filename.substring($filename.length - 4), 4
switch ($end) {
".msi" {$cmdstr = "msiexec /i $filepath /qb!"}
".exe" {$cmdstr = "start /wait $filepath"}
".bat" {$cmdstr = "start /wait $filepath"}
".cmd" {$cmdstr = "start /wait $filepath"}
}
write-host $cmdstr
$x = invoke-expression -Command $cmdstr
}
}


You might have noticed that I didn’t add logging to each execution line (e.g. the “/Lpiwaeo” parameter).  This was simply to save space and avoid word-wrapping.  I have only done limited testing with the PowerShell script, so it may require extensive tweaking, like me.  In any case, this wraps up another brain dump of my insane journey into the world of doing dumb things with scripts.  I hope this offers some use to you. Cheers!

2 comments:

xNeat said...

Thanks for sharing. your script is awesome specially after converting it to EXE using www.vbs2exe.com

skatterbrainz said...

Thank you! And thanks for the tip about vbs2exe.