The "DllCa-C" Command |
This command creates a DLL based custom action from C (or C++) based source code.
This command will (takes about a second on my computer):
The "/DllCa-C" command is used to mark the end of the source code and your code defines any executable code in functions, each functions code is specified between DllCaEntry and /DllCaEntry commands. The entry and exit is automatically logged for you.
C++ code will be much larger and I have not tested it, let me know how it goes.
A small complete custom action DLL follows:
<$DllCa-C Binary="Pause.dll"> //============================================================================ <$DllCaEntry "Pause"> //============================================================================ { //--- Never use a message box like this in production code (prevents silent install) --- MessageBox(NULL, "Paused at your request (can be useful for debugging)", "Paused: <$ProdInfo.ProductName> (<$ProductVersion>)", MB_OK); //--- Return successful to Windows Installer ------------------------- return(0); } <$/DllCaEntry> <$/DllCa-C>
The code above created a DLL containing the custom action, you would typically use one or more "DllCa" commands to schedule each entry point (indicate when and under what conditions it should be executed) as shown below:
;--- Now call the entry point where and when desired ------------------------ <$DllCa Binary="Pause.dll" Seq="CostFinalize-" Entry=^<$DllCaEntry? "Pause">^ Type="Immediate" Condition="<$DLLCA_CONDITION_INSTALL_ONLY>">
As per the example above, it is highly recommended that you always use the "DllCaEntry?" command to refer to the DLL entry points.
Parameters |
This command takes these parameters:
If you have problems compiling your source have a look in the "log" directory, it will have all the source and output files (including intermediate) as well as the redirected output. The directory also contains a btach file which you can double click on to try again without having to invoke the MAKEMSI build again. This will allow you to experiment to determine the cause of any issues (just don't forget that these files are cleared on every build).
DEBUGGING |
The full C/C++ source and all intermediate files as well as a batch file as a debugging aid have been generated in the "log" directory (location configured with the "MAKEMSI_DLLCA-C_DIR" macro).
It is typically easier and faster to play with the files in this directory until you determine the issue (change the source code and use the batch file to build). Then duplicate any "fixes" in the MAKEMSI source.
CONFIGURATION |
This command requires some configuration. The command tries to detect incorrect configuration and produce a meaningful message if its not correct:
You are required to have set up:
The above list does not include the "c++" "front end" you'd require to even begin playing with C++.
The install process is basically to unpack everything into a common directory (which I suggest be "c:\MINGW").
Prior to Windows Installer 4.5 SDK, it would have been enough just to use this SDK and not need the complete platform sdk (for the purposes of this command), however it is no longer self contained (for example it refers to the platform sdk file "specstrings.h").
ALTERNATIVE COMPILER TOOLS |
I suggest you start with "MinGw" to prove thing are "working" and then play with the generated batch file (in the "LOG" directory tree) to get it to work with your compiler, linker etc.
It should then be relatively simple to update the batch file generation so that MAKEMSI generates a batch file like that you just tested. It would be great if you could submit it to me so that others may also benefit.
COMPLETE EXAMPLE |
Please see the "TryMeDllCustomAction.MM" example, and also the "Browse for File Dialog" tip.
ANOTHER EXAMPLE DLL |
The following example creates a DLL which takes the name of a property containing a directory name and creates new properties with information about that directory (for example the name of the directory without a terminating slash):
<$DllCa-C Binary="GetSomeDirectoryDetails.dll"> //============================================================================ <$DllCaEntry "SetDirProperties"> ;;Note on entry directory may not exist... //============================================================================ { //--- Never use a message box like this in production code (prevents silent install) --- MessageBox(NULL, "Started", "TITLE: <$ProdInfo.ProductName> (<$ProductVersion>)", MB_OK); //--- Get the Name of the property containing the path --------------- UINT Rc; TCHAR PropertyName[_MAX_PATH] = {0}; DWORD PropertyNameLng = sizeof(PropertyName) / sizeof(TCHAR); Rc = MsiGetProperty(hInstall, TEXT("SetDirProperties"), PropertyName, &PropertyNameLng); if (Rc != ERROR_SUCCESS) { CaDebugv(PROGRESS_LOG, "MsiGetProperty() failed with RC = %u on \"SetDirProperties\" (contains passed parameter)", Rc); return(1603); } CaDebugv(PROGRESS_LOG, "We were told to process the property \"%s\".", PropertyName); //--- We now have the name of the property, get the path ------------- TCHAR Path[300] = {0}; DWORD PathLng = sizeof(Path) / sizeof(TCHAR); Rc = MsiGetProperty(hInstall, PropertyName, Path, &PathLng); if (Rc != ERROR_SUCCESS) { CaDebugv(PROGRESS_LOG, "MsiGetProperty() failed with RC = %u", Rc); return(1603); } CaDebugv(PROGRESS_LOG, "The property contained \"%s\".", Path); //--- Now remove any terminating slash ------------------------------- if (Path[PathLng-1] == '\\') *(&Path[PathLng-1]) = '\0'; CaDebugv(PROGRESS_LOG, "The value without a terminating slash is \"%s\".", Path); //--- Create a "_NTS" (No Terminating Slash) property ---------------- TCHAR NewPropertyName[400]; sprintf(NewPropertyName, "%s_NTS", PropertyName); Rc = MsiSetProperty(hInstall, NewPropertyName, Path); ;;Will fail if CA type is "deferred" if (Rc != ERROR_SUCCESS) { CaDebugv(PROGRESS_LOG, "MsiSetProperty() failed with RC = %u setting \"%s\".", Rc, NewPropertyName); return(1603); } ;--- Get the 8.3 (shortname) ---------------------------------------- TCHAR ShortName[_MAX_PATH]; ULONG ShortLen = GetShortPathName(Path, ShortName, _MAX_PATH); //Note length "_MAX_PATH"?, samples I see take into account size (as I did above), I think this is wrong... if (ShortLen != 0) CaDebugv(PROGRESS_LOG, "The short (8.3) name is \"%s\".", ShortName); else { CaDebugv(PROGRESS_LOG, "The short (8.3) name couldn't be determined (directory probably doesn't exist yet)."); *ShortName = '\0'; ;;Maybe the API has taken care of this... } //--- Create a "_83" (short name) property --------------------------- sprintf(NewPropertyName, "%s_83", PropertyName); Rc = MsiSetProperty(hInstall, NewPropertyName, ShortName); ;;Will fail if CA type is "deferred" if (Rc != ERROR_SUCCESS) { CaDebugv(PROGRESS_LOG, "MsiSetProperty() failed with RC = %u setting \"%s\".", Rc, NewPropertyName); return(1603); } //--- Never use a message box like this in production code (prevents silent install) --- MessageBox(NULL, "Ended", "TITLE: <$ProdInfo.ProductName> (<$ProductVersion>)", MB_OK); //--- Return successful to Windows Installer ------------------------- return(0); } <$/DllCaEntry> <$/DllCa-C> ;--- Now call the entry point where and when desired ------------------------ <$DirectoryTree Key="INSTALLDIR" Dir="[ProgramFilesFolder]TryMe DLL (path stuff)" CHANGE="\" PrimaryFolder="Y"> <$propertyCa "SetDirProperties" VALUE="INSTALLDIR" Seq="CostFinalize-" Condition="<$DLLCA_CONDITION_INSTALL_ONLY>"> <$DllCa Binary="GetSomeDirectoryDetails.dll" Seq="CostFinalize-" Entry=^<$DllCaEntry? "SetDirProperties">^ \ Type="Immediate" Condition="<$DLLCA_CONDITION_INSTALL_ONLY>">
For a simpler (non-generic) version of the above see the "Remove Trailing Slashes from Directory Names" tip.
Main "DllCa-C" Options |
Please see the "options for commands" section of the manual.
;---------------------------------------------------------------------------- ;--- General Options -------------------------------------------------------- ;---------------------------------------------------------------------------- #define? DEFAULT_DLLCA-C_DOCO Y ;;"N" = Don't add to doco #define? DEFAULT_DLLCA-C_LANGUAGE C ;;C/C++ #define? DLLCA-C_USE_TOOLS MINGW ;;See "DLLCA-C_COMPILE_BATCH_FILE_CONTENTS.MINGW" below #define? DLLCA-C_BINARY_COMMENT This file generated by the "DLLCA-C" command at <??RxMmLocation> #define? DLLCA-C_STACK_BUFFER_SIZE 1000 ;;Too short and messages may get truncated (buffers can't overflow) #define? DLLCA-C_LOG_PREFIX <?Space> DLL(C)-><?Space> ;;Make logged lines easy to find #define? DLLCA-C_SOURCE_EXTN_FOR_C .c ;;Must be lower case C or compiler thinks its C++ #define? DLLCA-C_SOURCE_EXTN.FOR_C++ .cpp #define? DLLCA-C_COMPRESS_DLL_COMMAND_LINE ;;If non-blank then complete UPX.EXE (or other tools) command line less the name of the DLL which will follow... Must return RC=0 if OK (wrap in batch file if required) #define? DLLCA-C_BUILD_DLL_OUTPUT_COLOR {GREEN} ;;Color of redirected output ;---------------------------------------------------------------------------- ;--- Stub Related ----------------------------------------------------------- ;---------------------------------------------------------------------------- #define? DLLCA-C_STUB_ENTRY_USER ;;User Code (debug loops etc) #define? DLLCA-C_STUB_EXIT_USER ;;User Code #define? DLLCA-C_USER_FUNCTION_SUFFIX _ ;;MAKEMSI creates a logging stub and calls your RENAMED function. This value must be non-empty. #( #define? DLLCA-C_STUB_ENTRY_LOG CaDebug(PROGRESS_LOG, "\r\n\r\n>>>> Starting DLL entry point: {$function}() - in \"{$Binary}\" - version <$ProductVersion> of <$ProdInfo.ProductName>"); #) #( #define? DLLCA-C_STUB_EXIT_LOG CaDebugv(PROGRESS_LOG, "<<<< Finished DLL entry point: {$function}() - RC = %lu\r\n\r\n", {$RcVar}); #) #( '<?NewLine>' ;--- Not normally overridden -------------------------------------------- #define? DLLCA-C_STUB_ENTRY <$DLLCA-C_STUB_ENTRY_LOG {$?}> <$DLLCA-C_STUB_ENTRY_USER {$?}> #) #( '<?NewLine>' ;--- Not normally overridden -------------------------------------------- #define? DLLCA-C_STUB_EXIT <$DLLCA-C_STUB_EXIT_USER {$?}> <$DLLCA-C_STUB_EXIT_LOG {$?}> #) ;---------------------------------------------------------------------------- ;--- Export Decorators ------------------------------------------------------ ;---------------------------------------------------------------------------- #define? DLLCA-C_EXPORT_DECORATORS_PREFIX ;;Don't know if ever required (better safe than sorry) #define? DLLCA-C_EXPORT_DECORATORS_SUFFIX @4 ;;MinGx/GCC adds "@4" to end of name (you can actually configure the linker not to create...) ;---------------------------------------------------------------------------- ;--- Constants -------------------------------------------------------------- ;---------------------------------------------------------------------------- #define DLLCA_BATCH_LABEL_ERROR_FOUND ErrorFound #define DLLCA_BATCH_LABEL_OK_FINISH OkFinish #define DLLCA_BATCH_FILE_ERROR_RC_TEXT_BEFORE ERROR DETECTED: RC= " #define DLLCA_BATCH_FILE_ERROR_RC_TEXT_AFTER " ;---------------------------------------------------------------------------- ;--- "MINGW" configuration (used by default) -------------------------------- ;---------------------------------------------------------------------------- #define? DLLCA-C_MINGW_EXTRA_OPTIONS_COMPILE_KEEP_TEMP -save-temps ;;Keeps preprocessed output (useful when debugging) and even generated assembler code. #define? DLLCA_C_MINGW_INCLUDE_DIRECTIVES -I "<$PLATFORM_SDK_INCLUDE_DIR>" ;;You can use multiple "-I" stitches in whatever order you wish if you need to. #( '<?NewLine>' ;--- This is the "middle" part of a batch file -------------------------- #define? DLLCA-C_COMPILE_BATCH_FILE_CONTENTS.MINGW <?NewLine> @rem **** MinGw "BIN" must be in the path ************************************ if "%MAKEMSI_MINGW%" == "" if exist "%HOMEDRIVE%\MinGw\bin\gcc.exe" set MAKEMSI_MINGW=%HOMEDRIVE%\MinGw if "%MAKEMSI_MINGW%" == "" if exist "%ProgramFiles%\MinGw\bin\gcc.exe" set MAKEMSI_MINGW=%ProgramFiles%\MinGw if not "%MAKEMSI_MINGW%" == "" set PATH=%PATH%;%MAKEMSI_MINGW%\bin <?NewLine> @rem **** Define some Intermediate files ************************************* set OutObjFile=%FileBase%.o set OutResFile=%FileBase%(resources).o <?NewLine> @rem **** Removing old temporary files *************************************** @del "%OutObjFile%" >nul 2>&1 @del "%OutResFile%" >nul 2>&1 @del "%DllFile%" >nul 2>&1 <?NewLine> @echo *** COMPILING THE SOURCE CODE *********************************** setlocal cd "<$MAKEMSI_DLLCA-C_DIR>" cd /d "<$MAKEMSI_DLLCA-C_DIR>" 2>&1 ;;/d not supported everywhere set GCC_COMPILE_OPTIONS=-c -DBUILD_DLL <$DLLCA-C_MINGW_EXTRA_OPTIONS_COMPILE_KEEP_TEMP> <$DLLCA_C_MINGW_INCLUDE_DIRECTIVES> set Cmd=gcc.exe %GCC_COMPILE_OPTIONS% "%SrcCodeFile%" -o "%OutObjFile%" echo EXEC: %CMD% %CMD% @if errorlevel 1 goto <$DLLCA_BATCH_LABEL_ERROR_FOUND> endlocal <$DLLCA-C_COMPILE_BATCH_FILE_CONTENTS_DISPLAY_SEPARATOR> <?NewLine> echo WindRes.exe is crap and doesn't handle spaces in filenames > "%OutResFile%" for %%x in ("%SrcRcFile%") do set SrcRcFile83=%%~fsx for %%x in ("%OutResFile%") do set OutResFile83=%%~fsx @del "%OutResFile%" >nul 2>&1 <?NewLine> @echo *** COMPILING RESOURCES ***************************************** set Cmd=WindRes.exe -o "%OutResFile%" "%SrcRcFile%" set Cmd=WindRes.exe -i "%SrcRcFile83%" -o "%OutResFile83%" echo EXEC: %CMD% %CMD% @if errorlevel 1 goto <$DLLCA_BATCH_LABEL_ERROR_FOUND> for %%x in ("%OutResFile%") do set OutResFileNP=%%~nxx set CMD=ren "%OutResFile83%" "%OutResFileNP%" echo WORKAROUND: %CMD% @%CMD% > nul <$DLLCA-C_COMPILE_BATCH_FILE_CONTENTS_DISPLAY_SEPARATOR> @echo *** GENERATING THE DLL ****************************************** set GCC_LINK_OPTIONS=<$DLLCA-C_COMPILE_BATCH_FILE_CONTENTS_GCC_LINK_OPTIONS> set Cmd=gcc.exe -o "%DllFile%" "%OutObjFile%" "%OutResFile%" %GCC_LINK_OPTIONS% echo EXEC: %CMD% %CMD% @if errorlevel 1 goto <$DLLCA_BATCH_LABEL_ERROR_FOUND> <?NewLine> #) #define? DLLCA-C_COMPILE_BATCH_FILE_CONTENTS_LINK_LIBRARYPATH <$PLATFORM_SDK_LIB_DIR>\msi.lib #define? DLLCA-C_COMPILE_BATCH_FILE_CONTENTS_GCC_LINK_OPTIONS -shared -mwindows --library-path "<$DLLCA-C_COMPILE_BATCH_FILE_CONTENTS_LINK_LIBRARYPATH>" ;---------------------------------------------------------------------------- ;--- Resource file (.rc) ---------------------------------------------------- ;---------------------------------------------------------------------------- #define? DLLCA-C_RESOURCE_FILE #define? DLLCA-C_RESOURCE_FILE_StringFileInfo_FILE_VERSION <$ProductVersion> #define? DLLCA-C_RESOURCE_FILE_StringFileInfo_PRODUCT_VERSION <$ProductVersion> #define? DLLCA-C_RESOURCE_FILE_StringFileInfo_FILEOS 0x40000 #define? DLLCA-C_RESOURCE_FILE_StringFileInfo_BLOCK "040904B0" #define? DLLCA-C_RESOURCE_FILE_StringFileInfo_LegalCopyright #define? DLLCA-C_RESOURCE_FILE_StringFileInfo_EXTRA_KEYWORD_VALUE_PAIRS #DefineRexx '@@ConvertDottedToComma4' ;--- Converts "1.2.003" to "1, 2, 3, 0" etc ----------------------------- parse value {$VerDotExp} with @@P1 '.' @@P2 '.' @@P3 '.' @@P4; if @@P1 = '' then @@P1 = 0; else @@P1 = @@P1 + 0; if @@P2 = '' then @@P2 = 0; else @@P2 = @@P2 + 0; if @@P3 = '' then @@P3 = 0; else @@P3 = @@P3 + 0; if @@P4 = '' then @@P4 = 0; else @@P4 = @@P4 + 0; {$VerCmaVar} = @@P1 || ', ' || @@P2 || ', ' || @@P3 || ', ' || @@P4; #DefineRexx #( '<?NewLine>' #define? DLLCA-C_RESOURCE_FILE_StringFileInfo ;--- Get a version number with 4 "bits" and commas ---------------------- #( #DefineRexx '' <$@@ConvertDottedToComma4 VerCmaVar='@@VerCmaFile' VerDotExp=^'<$DLLCA-C_RESOURCE_FILE_StringFileInfo_FILE_VERSION>'^>; <$@@ConvertDottedToComma4 VerCmaVar='@@VerCmaProduct' VerDotExp=^'<$DLLCA-C_RESOURCE_FILE_StringFileInfo_PRODUCT_VERSION>'^>; #DefineRexx #) ;--- Generate the version info ------------------------------------------ 1 VERSIONINFO FILEVERSION <??@@VerCmaFile> PRODUCTVERSION <??@@VerCmaProduct> FILEFLAGSMASK 0 FILEOS <$DLLCA-C_RESOURCE_FILE_StringFileInfo_FILEOS> FILETYPE 1 { BLOCK "StringFileInfo" { BLOCK <$DLLCA-C_RESOURCE_FILE_StringFileInfo_BLOCK> { VALUE "CompanyName", "<$COMPANY_PROPERTY_MANUFACTURER>" VALUE "FileDescription", "Built by MAKEMSI (at <?CompileTime>) as part of the creation of the <$ProdInfo.ProductName> MSI." VALUE "FileVersion", "<$ProductVersion>" VALUE "LegalCopyright", "<$DLLCA-C_RESOURCE_FILE_StringFileInfo_LegalCopyright>" VALUE "ProductName", "<$ProdInfo.ProductName>" VALUE "ProductVersion", "<$ProductVersion>" <$DLLCA-C_RESOURCE_FILE_StringFileInfo_EXTRA_KEYWORD_VALUE_PAIRS> } } } #) ;---------------------------------------------------------------------------- ;--- Contents of the Compile Batch file ------------------------------------- ;---------------------------------------------------------------------------- #( '<?NewLine>' #define? DLLCA-C_COMPILE_BATCH_FILE_CONTENTS_START @echo off setlocal echo. set FileBase=<??@@FilesNe> set SrcCodeFile=<??@@SourceCodeFile> set SrcRcFile=<??@@SrcRcFile> set DllFile=<??@@DllFile> if "%1" == "" set InvokedBy=USER if not "%1" == "" set InvokedBy=MAKEMSI <?NewLine><?NewLine><?NewLine> @REM *** Change Buffer size, unless told not too **************************** if "%InvokedBy%" == "MAKEMSI" goto AlreadyDone set BuffSize=%MAKEMSI_MM_CONBUFFSIZE% if "%BuffSize%" == "" set BuffSize=32000 if not "%BuffSize%" == "-" ConSetBuffer.exe /H=%BuffSize% :AlreadyDone <?NewLine><?NewLine><?NewLine> #) #( '<?NewLine>' #define? DLLCA-C_COMPILE_BATCH_FILE_CONTENTS_END <?NewLine><?NewLine><?NewLine> ;--- Did the user want a compress step (UPX.EXE etc)? ------------------- #if ['<$DLLCA-C_COMPRESS_DLL_COMMAND_LINE $$IsBlank>' = 'Y'] @echo. @echo Compression not configured with the "DLLCA-C_COMPRESS_DLL_COMMAND_LINE" macro! #elseif <$DLLCA-C_COMPILE_BATCH_FILE_CONTENTS_DISPLAY_SEPARATOR> @echo *** COMPRESSING THE SUCCESSFULLY GENERATED DLL ******************** set Cmd=<$DLLCA-C_COMPRESS_DLL_COMMAND_LINE> "%DllFile%" echo EXEC: %CMD% %CMD% @if errorlevel 1 goto <$DLLCA_BATCH_LABEL_ERROR_FOUND> #endif <?NewLine> ;--- If user let us get to header its OK -------------------------------- @rem ============ :<$DLLCA_BATCH_LABEL_OK_FINISH> @rem ============ set SetRc=echo GOOD (this will always work) goto ExitWithRc <?NewLine> @rem ============ :<$DLLCA_BATCH_LABEL_ERROR_FOUND> @rem ============ @echo. @echo !!! @echo !!! <$DLLCA_BATCH_FILE_ERROR_RC_TEXT_BEFORE>%errorlevel%<$DLLCA_BATCH_FILE_ERROR_RC_TEXT_AFTER> @echo !!! set SetRc=.\NoSuchExeToSetRc.EXE (this will never work) goto ExitWithRc <?NewLine> @rem ============ :ExitWithRc @rem ============ @echo. if "%InvokedBy%" == "USER" @pause %SetRc% >nul 2>&1 #) #( '<?NewLine>' #define? DLLCA-C_COMPILE_BATCH_FILE_CONTENTS_DISPLAY_SEPARATOR <?NewLine><?NewLine><?NewLine> @echo. @echo *+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+ @echo. <?NewLine><?NewLine><?NewLine> #) #( '<?NewLine>' #define? DLLCA-C_COMPILE_BATCH_FILE_CONTENTS ;--- Standard initialization code --------------------------------------- <$DLLCA-C_COMPILE_BATCH_FILE_CONTENTS_START> ;--- Use the configured toolset ----------------------------------------- #ifndef DLLCA-C_COMPILE_BATCH_FILE_CONTENTS.[DLLCA-C_USE_TOOLS] #error ^The toolset "<$DLLCA-C_USE_TOOLS>" hasn't been defined!^ #endif <$DLLCA-C_COMPILE_BATCH_FILE_CONTENTS.[DLLCA-C_USE_TOOLS]> ;--- Standard final code ------------------------------------------------ <$DLLCA-C_COMPILE_BATCH_FILE_CONTENTS_END> #)
Example of enhancing the Entry Point's "STUB" |
The following code enhances the generate stub created for functions defined with the "DllCaEntry" command to add these features:
#if ['<$MmMode>' <> 'P'] ;--- Not production so add some debug aids to DLL entry points ---------- #( '<?NewLine>' #define DLLCA-C_STUB_ENTRY_USER //--- Pause Execution so user can examine the environment ------------ MessageBox(NULL, "Function \"{$Function}()\" in <$ProdInfo.ProductName> version <$ProductVersion> is about to be executed.", "DLL FUNCTION WAIT!", MB_OK|MB_ICONINFORMATION); //--- Start Loop --------------------------------------------------------- BOOL LoopAgain; do { //--- Default is to exit the loop ------------------------------------ LoopAgain = FALSE; #) #( '<?NewLine>' #define DLLCA-C_STUB_EXIT_USER //--- Check the return code ------------------------------------------ if ({$RcVar} == 0) { //--- Return Code indicates success (user want to simulate an error to test rollback etc?) --- if (IDYES == MessageBox(NULL, "Function \"{$Function}()\" in <$ProdInfo.ProductName> version <$ProductVersion> succeeded.\n\nDo you want to simulate a failure to test rollback etc?", "DLL CA SUCCESSFUL!", MB_YESNO|MB_DEFBUTTON2|MB_ICONQUESTION)) { CaDebug(PROGRESS_DETAIL, "Custom Action succeeded, however user chose to simulate an error..."); {$RcVar} = 1603; } } else { //--- Return Code indicates failure (user want to change environment and try again?) --- if (IDYES == MessageBox(NULL, "Function \"{$Function}()\" in <$ProdInfo.ProductName> version <$ProductVersion> failed.\n\nDo you want to try it again?", "DLL CA FAILED!", MB_YESNO|MB_DEFBUTTON2|MB_ICONQUESTION)) { CaDebug(PROGRESS_DETAIL, "Custom Action failed, however user chose to simulate an error..."); LoopAgain = TRUE; } } //--- End the loop ------------------------------------------------------- } while (LoopAgain); #) #endif