title: Exit code "lost" from child process in Windows XP, not in Windows Server 2003
body: <p><em><strong>EDIT 3</em></strong></p>
  
  <p>OK, so it seems like this might not be an Installer issue after all.  When I make a simple batch file:</p>
  
  <pre><code>exit /b 12
  </code></pre>
  
  <p>and call it as</p>
  
  <pre><code>cmd /c test.cmd
  echo %ERRORLEVEL%
  </code></pre>
  
  <p>I get "12" on Windows Server 2003 R2, but "0" on XP.  I thought I had tested this simple test case many times before but apparently not.</p>
  
  <p>So, I've changed the tags and title but I'm leaving the other information here as there's actually a lot of useful stuff here that is not directly related to this issue.</p>
  
  <p>Thoughts?</p>
  
  <p><em><strong>Original below</em></strong></p>
  
  <p>I have a custom action written in VBScript that in turn is calling a Windows batch file (the custom action is essentially allowing the user to execute something at install time they can also run later by running the batch file - it's a convenience).  The function is below:</p>
  
  <pre><code>Function MainFunction
      strCustomActionData = Session.Property("CustomActionData")
      strSplit = Split(strCustomActionData, ";")
      strInstallDir = strSplit(0)
      strPostCopyAction = strSplit(1)
  
      strScriptLocation = strInstallDir &amp; "\MigrationMasterProcess.cmd"
  
      strFullCommand = """" &amp; strScriptLocation &amp; """ " &amp; strPostCopyAction
  
      Set objShell = CreateObject("WScript.Shell")
  
      Dim objExec
      Set objExec = objShell.Exec(strFullCommand)
  
      intReturnCode = objExec.ExitCode
  
      Set objExec = Nothing
      Set objShell = Nothing
  
      WriteMessage "Return value: " &amp; intReturnCode
  
      ' cf. http://msdn.microsoft.com/en-us/library/windows/desktop/aa371254(v=vs.85).aspx
      If (intReturnCode = 0) Then
          MainFunction = 1
      Else
          MainFunction = 3
      End If
  End Function
  </code></pre>
  
  <p>When I run the same kind of code outside of a custom action, and the batch file returns an error code (via EXIT /B), the return value is correctly captured in intReturnCode.  However, from the custom action, the exit code seems to be "lost" - I always get a 0 back (I can see this in the installer log from the WriteMessage call).  It doesn't matter if I use Exec or Run on the shell, I still get back a 0.  The script writes its own return code out before returning it (I can see this in the stdout stream from Exec) so I know it's not actually 0.  I need that return code to properly report an error back to the installer.</p>
  
  <p>Ideas?  </p>
  
  <p>For the record this is Windows Installer 3.0 on Windows XP SP3.  The installer is in Wise so I don't have a WiX snippet or I would include it, but this is the function being called.</p>
  
  <p>Also this is somewhat stripped - I've left out comments and other calls to WriteMessage as well as that function.  And yes psuedo-Hungarian is evil blah blah blah.</p>
  
  <p><em><strong>Edit</em></strong>: Here is the C version of the code.  It's giving the same exact issue:</p>
  
  <pre><code>#include &lt;Windows.h&gt;
  #include &lt;msi.h&gt;
  #include &lt;msiquery.h&gt;
  #include &lt;stdio.h&gt;
  #include &lt;stdlib.h&gt;
  #include "LaunchChildProcess.h"
  
  BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) {
      return TRUE;
  }
  
  UINT __stdcall RunMigrationAction(MSIHANDLE hModule) {
      UINT  uiStat;
      DWORD dwPropertySize = MAX_PATH * 2;
      TCHAR szValueBuf[MAX_PATH * 2]; // arbitrary but we know the strings won't be near that long
      TCHAR *szInstallDir, *szPostCopyAction;
      TCHAR *szNextToken;
      TCHAR szScriptLocation[MAX_PATH * 2];
      TCHAR szParameters[MAX_PATH * 2];
      INT   iReturnValue;
  
      LogTaggedString(hModule, TEXT("Action Status"), TEXT("Starting"));
  
      uiStat = MsiGetProperty(hModule, TEXT("CustomActionData"), szValueBuf, &amp;dwPropertySize);
      if (ERROR_SUCCESS != uiStat) {
          LogTaggedString(hModule, TEXT("Startup"), TEXT("Failed to get custom action data"));
          return ERROR_INSTALL_FAILURE;
      }
  
      LogTaggedString(hModule, TEXT("Properties given"), szValueBuf);
      LogTaggedInteger(hModule, TEXT("Property length"), dwPropertySize);
  
      if (0 == dwPropertySize) {
          return ERROR_INSTALL_FAILURE;
      }
  
      LogTaggedString(hModule, TEXT("Properties given"), szValueBuf);
  
      szInstallDir     = wcstok_s(szValueBuf, TEXT(";"), &amp;szNextToken);
      szPostCopyAction = wcstok_s(NULL,       TEXT(";"), &amp;szNextToken);
  
      LogTaggedString(hModule, TEXT("Install dir"), szInstallDir);
      LogTaggedString(hModule, TEXT("Post-copy action"), szPostCopyAction);
  
      wcscpy_s(szScriptLocation, MAX_PATH * 2, szInstallDir);
      wcscat_s(szScriptLocation, MAX_PATH * 2, TEXT("\\MigrationMasterProcess.cmd"));
  
      LogTaggedString(hModule, TEXT("Script location"), szScriptLocation);
  
      wcscpy_s(szParameters, MAX_PATH * 2, TEXT(" /C "));
      wcscat_s(szParameters, MAX_PATH * 2, szScriptLocation);
      wcscat_s(szParameters, MAX_PATH * 2, TEXT(" "));
      wcscat_s(szParameters, MAX_PATH * 2, szPostCopyAction);
  
      LogTaggedString(hModule, TEXT("Parameters to cmd.exe"), szParameters);
  
      iReturnValue = ExecuteProcess(TEXT("cmd.exe"), szParameters);
      LogTaggedInteger(hModule, TEXT("Return value from command"), iReturnValue);
  
      LogTaggedString(hModule, TEXT("Action Status"), TEXT("Finished"));
  
      return (0 == iReturnValue) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE;
  }
  
  void LogTaggedInteger(MSIHANDLE hInstall, TCHAR* szTag, INT iValue) {
      TCHAR szValue[15];
      _itow_s(iValue, szValue, 15, 10);
      LogTaggedString(hInstall, szTag, szValue);
  }
  
  void LogTaggedString(MSIHANDLE hInstall, TCHAR* szTag, TCHAR* szMessage) {
      MSIHANDLE hRecord;
      UINT uiStat;
      //TCHAR szFullMessage[4096];
      //wcscpy_s(szFullMessage, 4096, TEXT("--------------- "));
      //wcscat_s(szFullMessage, 4096, szTag);
      //wcscat_s(szFullMessage, 4096, TEXT(": "));
      //wcscat_s(szFullMessage, 4096, szMessage);
      hRecord = MsiCreateRecord(3);
      uiStat = MsiRecordSetString(hRecord, 0, TEXT("--------- [1]: [2]"));
      uiStat = MsiRecordSetString(hRecord, 1, szTag);
      uiStat = MsiRecordSetString(hRecord, 2, szMessage);
      uiStat = MsiProcessMessage(hInstall, INSTALLMESSAGE(INSTALLMESSAGE_INFO), hRecord);
      MsiCloseHandle(hRecord);
      return;
  }
  
  
  int MsiMessageBox(MSIHANDLE hInstall, TCHAR* szString, DWORD dwDlgFlags) {
      PMSIHANDLE newHandle = ::MsiCreateRecord(2);
      MsiRecordSetString(newHandle, 0, szString);
      return (MsiProcessMessage(hInstall, INSTALLMESSAGE(INSTALLMESSAGE_USER + dwDlgFlags), newHandle));
  }
  
  
  DWORD ExecuteProcess(TCHAR *szProcess, TCHAR *szParams) { 
      INT iMyCounter = 0, iPos = 0;
      DWORD dwReturnVal = 0;
      TCHAR *sTempStr = L""; 
  
      /* CreateProcessW can modify Parameters thus we allocate needed memory */
      wchar_t * pwszParam = new wchar_t[wcslen(szParams) + 1]; 
      if (NULL == pwszParam) { 
          return 1; 
      } 
  
      wcscpy_s(pwszParam, wcslen(szParams) + 1, szParams); 
  
      /* CreateProcess API initialization */
      STARTUPINFOW siStartupInfo; 
      PROCESS_INFORMATION piProcessInfo; 
      memset(&amp;siStartupInfo, 0, sizeof(siStartupInfo)); 
      memset(&amp;piProcessInfo, 0, sizeof(piProcessInfo)); 
      siStartupInfo.cb = sizeof(siStartupInfo); 
  
      if (CreateProcessW(const_cast&lt;LPCWSTR&gt;(szProcess), 
                              pwszParam, 0, 0, false, 
                              CREATE_DEFAULT_ERROR_MODE, 0, 0, 
                              &amp;siStartupInfo, &amp;piProcessInfo) != false) { 
          /* Watch the process. */
          WaitForSingleObject(piProcessInfo.hProcess, INFINITE);
          if (!GetExitCodeProcess(piProcessInfo.hProcess, &amp;dwReturnVal)) {
              dwReturnVal = GetLastError();
          }
      } else { 
          /* CreateProcess failed */
          dwReturnVal = GetLastError(); 
      } 
  
      /* Free memory */
      free(pwszParam);
      pwszParam = NULL;
  
      /* Release handles */
      CloseHandle(piProcessInfo.hProcess); 
      CloseHandle(piProcessInfo.hThread); 
  
      return dwReturnVal; 
  } 
  </code></pre>
  
  <p>When run on my Windows Server 2003 R2 Visual Studio 2008 box, I get the error code as expected:</p>
  
  <pre><code>--------- Return value from command: 5023
  </code></pre>
  
  <p>When run on my Windows XP test box, I get a 0, even though it should be an error:</p>
  
  <pre><code>--------- Return value from command: 0
  </code></pre>
  
  <p>Both machines have Windows Installer 3.1.  XP is 3.01.4001.5512, 2003 R2 is 3.01.4000.3959.</p>
  
  <p>So it's something acting different between the boxes although I have no idea what.</p>
  
  <p><em><strong>EDIT 2</em></strong></p>
  
  <p>The exact table row for the action, as generated by the Wise for Windows Installer tool, is:</p>
  
  <p>"RunMigrationActionCA","1537","Calllaunchchildprocess","RunMigrationAction","0"</p>
  
  <p>To test the immediate flag I added 0x800 to the type column and no change was seen in the end behavior.</p>
  
  <p>To be clear - this works fine on the 2003 R2 machine.  That machine is not joined to a domain, but the XP machine is.  Is there anything in group policy that could cause this behavior?  (Grasping at straws at this point.)</p>