Thursday, August 4, 2011

Scripts Calling Scripts, and So On...

I tried to come up with a clever title, but I just finished a grueling bike ride in the heat and humidity, followed by cold beer and pizza, so my brain is not on the clever channel right now.  But here goes...

I recently got a few e-mails about my recent post about running script packages from Configuration Manager and how to handle return exit codes.  Rather than dive into the syntactical minutae, I think a conceptual overview is in order.

Basically, when one thing calls another thing, and that second thing calls yet another thing, there are a variety of issues that come into play when it comes to the calling thing getting a "result" from the thing it calls.

When you create a Package and Program in Configuration Manager (or Altiris, Tivoli, wtf), which runs a .MSI installation file, it automatically invokes msiexec to do the heavy lifting on the client.  When msiexec runs the .msi (and/or .msp, or .msi + .mst, etc.), msiexec handles the return value by evaluating the execution process as it progresses.  If it bombs out, you get back an exit code that is "non zero" (remember: in most cases, a zero value indicates "success").

When your package/program executes an .EXE, the CMD/explorer shell process handles the resulting exit code.

In both msiexec and .exe situations, the handlers (msiexec and cmd/explorer) pass the result exit code back to whatever called them (i.e. Configuration Manager agent).

When you call a script, it introduces a "sort of" proxy situation.  The script handler (cmd for .bat or .cmd scripts, kix32.exe for .kix or .kx scripts, powershell for .ps1 scripts, or wscript.exe or cscript.exe for .vbs or .js scripts, and so on...) does not automatically "raise" the exit code from a scripted task up to the calling process (Config Manager agent).  Instead, the handler will return an exit code based on whether or not the script itself completed or not.  If, within the script code, you forcibly raise an exit code, it will respect that and pass that value up the stack to the calling process.

So, let's give an example:

  1. You make a Config Manager Package and Program that calls a .CMD script
  2. Inside the .CMD script you execute a msiexec /I <filename.msi> /quiet /norestart
  3. The .msi installation fails with exit code 1604
  4. The .cmd script returns 0 to the Configuration Manager agent (e.g. "success")

Why?  Because the .cmd completed successfully even though the msiexec task failed.

What to do?

The specifics of how to "raise" an exit code depend on the scripting language being used.  For .cmd and .bat script, use the EXIT statement followed by the value you wish to raise to the calling stack.  You can give an explicit value, such as 33, or you can pass the %errorlevel% value, which will temporarily hold the value of the last error.  If you check for the %errorlevel% immediately after the msiexec execution, you can capture the value, and then raise it accordingly.

Here's a short example:

@echo off
msiexec /I "%~dp0fubar.msi" /quiet /norestart
if %errorlevel%==0 (
   echo installation successful >>%logfile%
) else (
   if %errorlevel%==3010 (
      echo installation successful [reboot pending] >>%logfile%
   ) else (
      echo installation failed [exit code: %errorlevel%] >>%logfile%
      exit %errorlevel%
   )
)

The 10th line is where the exit code is raised to whatever is calling this script.  So if the msiexec process fails with error 1619, it saves that value in the CMD %errorlevel% variable.  Then in the script, we pass %errorlevel% up using the exit command.  When it fails, and the script passes the value back to the Configuration Manager agent, it reports back to the site server that it failed with error 1619.  All is well in the Universe.

What happens when a vendor's wonderful setup.exe doesn't properly return an expected exit code when it fails?  What?!!!! (insert 1980's vinyl album scratch sound here)

Did I just say that vendors might actually have produced less-than-perfect installation packages?  Is this even possible?  OH NO HE DIDN'T!!  OH SHIZNIT!  OH SNAP-STICK!  What now?

Yes.  Unfortunately, the ugly, brutal truth is that there are many, many, many such pieces of fecal matter labeled as "installation" files or packages.  Some come from widely-known, huge, corporate vendors, while a larger number come from smaller shops where they feed chained wild monkeys bags of Skittles and Kegs of Mountain Dew and whip them with Chinese egg noodles until they produce installer files.

What to do?

You have to get creative and wrap their crap in some script to perform additional checks and double-checks, and raise your own custom errors.  I call this a "crap wrap", because it wraps crap.

FWIW: I have my own definition of "crap installer" >>> Any installer that is not a 100% pure MSI file is crap.  Period.  I f-ing hate setup.exe, and setup.exe bootstrap installers.  NullSoft is crap also (silent installations are ok, but silent uninstalls are horrifically f**ked).  And anyone who sells you a .zap file should be shot on site (I'm not including those of you that have to make your own .zap files, we share the same pain). 

I make one exception to this rule: self-contained applications.  Things like Sysinternals' PSTools, where you don't really "install" anything, you simply copy/download the .exe and it's ready to go.  I wish more apps were that well-packaged actually.  Imagine if Autodesk provided Inventor 2012 as a self-contained application that required NO installation process.  OMG.  I would need a Kleenex.  (don't even try to suggest App-V or ThinApp as being in this category, they are not).

In any case, back to the subject:

When one thing calls another, you need to examine what each down-level process returns up the stack, as it were.  Using a virtual machine environment is great for this, as is using the good-ole CMD shell to perform command line diagnostics along the way.

I'd go on longer, but I'm out of brain power right now... I need more pizza.

No comments: