0

I have created an installer that will call two Custom actions. First custom action is WRITEFILETODISK that will create a config file based on the parameter passed to the installer and store it in %ProgramData%\SomeFolder. Second custom action is ResidueRemove that will does the job of cleaning any residue from %ProgramData%\SomeFolder.

Below is the snippet of custom action from my Wix File:

<Binary Id="SetupCA"  SourceFile="..\..\ext_library\SetupCACPP\SetupCACPP\bin\Release\SetupCACPP.dll"/>
<CustomAction Id="WRITEFILETODISK" Execute="immediate" BinaryKey="SetupCA" DllEntry="WriteFileToDisk" />
<CustomAction Id="ResidueRemove" Execute="immediate" BinaryKey="SetupCA" DllEntry="DeleteResidue" />
<InstallExecuteSequence>
  <Custom Action="WRITEFILETODISK" After="InstallFinalize">NOT Installed</Custom>
  <Custom Action="ResidueRemove" After="InstallFinalize">REMOVE~="ALL"</Custom>
  <!-- Rob Menshing Answer @ http://stackoverflow.com/a/321874/2634612 -->
</InstallExecuteSequence>

The ResidueRemove Custom Action is added here:

    UINT __stdcall DeleteResidue(MSIHANDLE hInstall)
{
    HRESULT hr = S_OK;
    UINT er = ERROR_SUCCESS;
    SC_HANDLE sHandleLPA, sHandleLPAM;
    SERVICE_STATUS servStatusLPA, servStatusLPAM;
    SHFILEOPSTRUCT shFileCommFolder, shFileInstalledFolder;
    LPWSTR lpaCommFolder, lpaInstalledFolder;
    std::wstring wlpaCommFolder, wlpaInstalledFolder;
    std::string errorCode;
    hr = WcaInitialize(hInstall, "DeleteResidue");
    ExitOnFailure(hr, "Failed to initialize");

    //Stop the LPA and LPA Monitor Service. Then delete the residue.
    WcaLog(LOGMSG_STANDARD, "Doing Delete Residue"); // Fails after printing this log

    hr = WcaGetProperty(L"LPCOMMAPPFOLDER",&lpaCommFolder);
    ExitOnFailure(hr, "Failure in Common Folder"); // Getting the %ProgramData%\SomeFolder here

    hr = WcaGetProperty(L"INSTALLFOLDER",&lpaInstalledFolder);
    ExitOnFailure(hr, "Failure in getting Installed Folder"); // Getting Installed Folder here

    //Path should be double Terminated for SHFILEOPSTRUCT.pFrom
    wlpaCommFolder = std::wstring(lpaCommFolder);
    wlpaCommFolder.push_back(L'\0');

    wlpaInstalledFolder = std::wstring(lpaInstalledFolder);
    wlpaInstalledFolder.push_back(L'\0');

    try
    {
        sHandleLPAM = OpenSCManager(NULL,NULL, SC_MANAGER_ALL_ACCESS);
        if(sHandleLPAM == NULL)
        {
            WcaLog(LOGMSG_STANDARD, "OpenSCManager NULL Handle");
        }
        sHandleLPAM = OpenService(sHandleLPAM, L"SomeServiceOne",SERVICE_ALL_ACCESS);
        if(sHandleLPAM == NULL)
        {
            WcaLog(LOGMSG_STANDARD, "OpenService NULL Handle");
        }
        bool res = ControlService(sHandleLPAM, SERVICE_CONTROL_STOP,(LPSERVICE_STATUS) &servStatusLPAM);
        if(!res)
        {
            WcaLog(LOGMSG_STANDARD, "ControlService Cannot Stop the service");
        }
        // Free the service Handler
        CloseServiceHandle(sHandleLPAM);
    }
    catch(std::exception& e)
    {
        WcaLog(LOGMSG_STANDARD, e.what());
    }

    try
    {
        sHandleLPA = OpenSCManager(NULL,NULL, SC_MANAGER_ALL_ACCESS);
        if(sHandleLPA == NULL)
        {
            WcaLog(LOGMSG_STANDARD, "OpenSCManager NULL Handle");
        }
        sHandleLPA = OpenService(sHandleLPA, L"SomeServiceTwo",SERVICE_ALL_ACCESS);
        if(sHandleLPA == NULL)
        {
            WcaLog(LOGMSG_STANDARD, "OpenService NULL Handle");
        }
        bool res = ControlService(sHandleLPA, SERVICE_CONTROL_STOP,(LPSERVICE_STATUS) &servStatusLPA);
        if(!res)
        {
            WcaLog(LOGMSG_STANDARD, "ControlService Cannot Stop the service");
        }
        // Free the service Handler
        CloseServiceHandle(sHandleLPA);
    }
    catch(std::exception& e)
    {
        WcaLog(LOGMSG_STANDARD, e.what());
    }

    ZeroMemory(&shFileCommFolder, sizeof(SHFILEOPSTRUCT));
    shFileCommFolder.hwnd = NULL;
    shFileCommFolder.wFunc = FO_DELETE;
    shFileCommFolder.pFrom = wlpaCommFolder.c_str();
    shFileCommFolder.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI;
    BOOL res = DirectoryExists(lpaCommFolder);
    if(res)
    {
        WcaLog(LOGMSG_STANDARD, "The directory exist");
        int result = SHFileOperation(&shFileCommFolder);
        if(!result)
            WcaLog(LOGMSG_STANDARD, "The directory should have deleted by now");
        else
        {
            errorCode = GetLastErrorStdStr();
            WcaLog(LOGMSG_STANDARD, "The directory could not be deleted %s", errorCode);
        }

    }       
    else
    {
        WcaLog(LOGMSG_STANDARD, "It Seems the Installed Folder is No more there");
    }

    ZeroMemory(&shFileInstalledFolder, sizeof(SHFILEOPSTRUCT));
    shFileInstalledFolder.hwnd = NULL;
    shFileInstalledFolder.wFunc = FO_DELETE;
    shFileInstalledFolder.pFrom = wlpaInstalledFolder.c_str();
    shFileInstalledFolder.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI;
    res = DirectoryExists(lpaInstalledFolder);
    if(res)
    {
        WcaLog(LOGMSG_STANDARD, "The directory exist");
        int result = SHFileOperation(&shFileInstalledFolder);
        if(!result)
            WcaLog(LOGMSG_STANDARD, "The directory should have deleted by now");
        else
        {
            errorCode = GetLastErrorStdStr();
            WcaLog(LOGMSG_STANDARD, "The directory could not be deleted %s", errorCode);
        }
    }       
    else
    {
        WcaLog(LOGMSG_STANDARD, "It Seems the Installed Folder is No more there");
    }


LExit:
    er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE;
    return WcaFinalize(er);
}

I get error after the log Delete Residue that is:

DeleteResidue:  Doing Delete Residue
MSI (s) (64:A4) [10:54:51:643]: Leaked MSIHANDLE (11) of type 790541 for thread 980
MSI (s) (64:A4) [10:54:51:643]: Note: 1: 2769 2: ResidueRemove 3: 1 
MSI (s) (64:A4) [10:54:51:643]: Note: 1: 2205 2:  3: Error 
MSI (s) (64:A4) [10:54:51:643]: Note: 1: 2228 2:  3: Error 4: SELECT `Message` FROM `Error` WHERE `Error` = 2769 
DEBUG: Error 2769:  Custom Action ResidueRemove did not close 1 MSIHANDLEs.
The installer has encountered an unexpected error installing this package. This may indicate a problem with this package. The error code is 2769. The arguments are: ResidueRemove, 1, 
Action ended 10:54:51: ResidueRemove. Return value 3.
Action ended 10:54:51: INSTALL. Return value 3.

This is replicated when the upgrade of installer is done in (Non-English) German Windows. Upon some research I found that The InstallExecuteSequence may have been authored incorrectly. Actions that change the system must be sequenced between the InstallInitialize and InstallFinalize actions. Perform package validation and check for ICE77. Does this mean My InstallExecuteSequence is wrong? The first CA WriteFileToDisk should run after installation and before the service are started (because the executable depend upon the file). The second CA should remove the residue, stop services after Uninstall is complete. What's wrong here in my code? Any help is appreciated.

Mahadeva
  • 1,584
  • 4
  • 23
  • 56

1 Answers1

0

Firstly, you are trying to change the state of the system using an immediate mode custom action. This is not advisable and is against the windows installer best practices. It might work or it might not work(depends on your environment). My first recommendation is to change these actions to deferred mode custom actions, since you are changing the state of your system.

I do not see your entire custom action code to know what exactly are you doing with Stopping the service and deleting the folders in your function named:DeleteResidue. I do not really know under what conditions does your custom action fail and return a failure.However, what i am stating below might give you hints to solve your problem.

I guess the reason your custom action with Id= ResidueRemove fails is because you are trying to stop services or cleanup folders in an immediate mode custom action after InstallFinalize. Your services/folders might no longer be present in the system by the time you attempt to stop these services. There are a lot of standard actions that execute up until InstallFinalize that do the task of cleaning up your installed product. These standard actions precede your immediate mode custom action. By the time, you execute custom action with Id= ResidueRemove, none of your installed files will be present.They would all be cleaned up. Its also possible that the folder that you are trying to cleanup might no longer be present.

For a better understanding of windows installer sequences and the different custom action types, i did suggest reading the following article:

http://www.installsite.org/pages/en/isnews/200108/

Here are the recommended changes -Change the CustomAction Id="WRITEFILETODISK" to a deferred mode custom action. Schedule it to run after the standard action: "InstallFiles" but before the "StartServices" standard action

If you want to see the order in which the windows installer standard actions execute, please install the windows installer editor named "Orca" from the windows installer SDK. Inspect the contents of the "InstallExecuteSequence" table. Sort the actions by the "Sequence" column. Here you will see that the standard action "InstallFiles" has a lower sequence number than the standard action:"StartFiles".

You will now get a rough idea of where you might want to sequence your action.

-For Custom Action="ResidueRemove", similarly, mark this as a deferred mode custom action. Inspect the "InstallExecuteSequence" table to get an idea of where you might want to sequence this.

The standard action which cleans up the installed files and folders is :RemoveFiles and RemoveFolders respectively. Similarly, there is one more for deleting servcies named :DeleteServices.

Here is how you will use it:

<!--For each deferred action where you want to access public property values, you need to use type 51 set a property custom action and sequence them in the InstallExecute sequence-->
  <!--The name of the property in the type 51 custom action should be the name of the **deferred mode** custom action where you want to access the value-->
  <!--The value of the type 51 custom action should be the value(s) you wan to access-->
  <!--In case, you need multiple property values to be accessed, seperate the values using a delimiter such as a semicolon-->
  <CustomAction Id="SetProperty_WRITEFILETODISK" Property="WRITEFILETODISK" Value="[LPCOMMAPPFOLDER];[INSTALLFOLDER]" Execute="immediate"/>
  <CustomAction Id="WRITEFILETODISK" Execute="deferred" BinaryKey="SetupCA" DllEntry="WriteFileToDisk" />

  <CustomAction Id="SetProperty_ResidueRemove" Property="ResidueRemove" Value="[LPCOMMAPPFOLDER];[INSTALLFOLDER]" Execute="immediate" />
  <CustomAction Id="ResidueRemove" Execute="deferred" BinaryKey="SetupCA" DllEntry="DeleteResidue" />

  <Property Id="LPCOMMAPPFOLDER" Secure="yes" />
  <Property Id="IPADDRESS" Secure="yes" />
  <Property Id="SSL" Secure="yes" />

  <CustomAction Id="SetCommonFolder" Property="CheckCommonFolder" Value="[LPCOMMAPPFOLDER]" />
  <CustomAction Id="SetIPExample" Property="CheckIP" Value="[IPADDRESS]" />
  <CustomAction Id="SetSSLExample" Property="CheckSSL" Value="[SSL]" />
  <InstallExecuteSequence>
    <Custom Action="SetCommonFolder" After="ValidateProductID" />
    <Custom Action="SetIPExample" After="ValidateProductID" />
    <Custom Action="SetSSLExample" After="ValidateProductID" />

    <Custom Action="SetProperty_WRITEFILETODISK" Before="WRITEFILETODISK">NOT Installed</Custom>
    <Custom Action="WRITEFILETODISK" Before="InstallServices">NOT Installed</Custom>
    <Custom Action="SetProperty_ResidueRemove" Before="ResidueRemove">REMOVE~="ALL"</Custom>
    <Custom Action="ResidueRemove" After="RemoveFiles">REMOVE~="ALL"</Custom>
    <!-- Rob Menshing Answer @ http://stackoverflow.com/a/321874/2634612 -->
  </InstallExecuteSequence>

Also in addition , in your custom action code, you will access the property "CustomActionData", instead of accessing the properties LPCOMMAPPFOLDER,INSTALLFOLDER. The value of custom action data will be what you have set in the type 51 custom action.

Also i dont think you need those SetCommonFolder etc custom actions. What purpose are they serving? We have property tags that i have used.

Hope this helps.

Kiran Hegde
  • 680
  • 4
  • 14
  • I have updated the code. Could you take a loot at it? – Mahadeva Aug 04 '15 at 06:44
  • If I change the execution to `deferred` I do not get the `Properties` passed to the Installer. Thus `WriteFileToDisk` cannot create a proper config file. Also I need to create rollback action too. How should I approach this? – Mahadeva Aug 04 '15 at 07:32
  • To access the values of public properties in deferred actions, you need to make use of a special windows installer property called:CustomActionData. Take a look at: https://www.firegiant.com/wix/tutorial/events-and-actions/at-a-later-stage/. For each of your custom actions, you need one type 51(Set a property custom action) to be able to access the values of properties. A rollback custom action generally precedes a deferred action that it is trying to rollback. For rollback actions, please remember to set the value of the "Execute" attribute to "rollback" – Kiran Hegde Aug 04 '15 at 07:56
  • The installer is supposed to be installed as `msiexec /i installer.msi IP="1.1.1.1" PORT="8000"` Deferred action could not get `IP` or `PORT` or the `INSTALLFODLER`. How do I set the value for deferred CA? – Mahadeva Aug 04 '15 at 08:32
  • Have you made these properties secure? Else, the values of these public properties will not be passed to the Execute sequence. As stated above, if you want to access the values of properties in deferred custom actions, you need to use CustomActionData. https://www.firegiant.com/wix/tutorial/events-and-actions/at-a-later-stage/ gives you an example of how to access the values of properties in Deferred execution actions. – Kiran Hegde Aug 04 '15 at 08:55
  • I have modified my wix file as given [here] (https://gist.github.com/mesarvagya/ef79e381a265cb561959). I have created `CheckCommonFolder`, `CheckIP` in that wix file. When install, I could see them in properties, but I cannot get it using `WcaGetProperty`. Should I use `MsiGetProperty` instead of it? – Mahadeva Aug 04 '15 at 09:44
  • Should I get the data using `WcaGetProperty(L"CustomActionData",&data);` and split it using `;`. In `C#` I could use `session.CustomActionData["Some Data"]`. What's its equivalent in C++ – Mahadeva Aug 05 '15 at 09:34
  • Based on some research, I did [this](https://gist.github.com/mesarvagya/e6a753d04152697694de) . While the C# custom action did found the `CustomActionData`, the C++ code gave me error `Error 0x80004005: Failed to get previous size of property data string. WriteFileToDisk: Error 0x80004005: Failed to get the customactionData`? What's wrong here? I think i am getting near the solution. – Mahadeva Aug 06 '15 at 07:46
  • Well, can you try in a sample project to retrieve the CustomActionData using C++? Or else, debug your code and you should be able to find out. – Kiran Hegde Aug 12 '15 at 11:42
  • Nope. I have asked a similar question [here](http://stackoverflow.com/questions/31993322/getting-customactiondata-from-c). – Mahadeva Aug 14 '15 at 06:15
  • Could you please try once with MsiGetProperty and see if you can get it to work? http://stackoverflow.com/questions/16135732/c-custom-action-returns-empty-string. If MisGetProperty works, then we know there is a problem with the way we are using Wca.. functions. Also i found some documentation for Wca functions for Wix. Take a look at:http://blogs.technet.com/b/alexshev/archive/2009/05/21/documentation-on-wix-api.aspx – Kiran Hegde Aug 18 '15 at 06:21