1

Previous installation of my product was done in Visual Studio 2008 as deployment project (vdproj). User had the option to install per user (just me) or per machine (everyone). I'm doing new installation in WiX, and if product was installed per machine then new installation will uninstall previous version. BUT, if product was installed per user in %ProgramFiles% then new installation will not uninstall previous, making a mess of two installations in the same folder.

I want to detect and stop the installation in this situation. No automatic uninstall, just detect and stop. The solution should handle the case if one user did previous per user installation, and another user is doing new per machine. I thought DirectorySearch and FileSearch elements could be used, but they run before target folder is determined in UI.

If WiX code is involved please provide small example. I'll learn from it.

EDIT: If I add a property using AppSearch features DirectorySearch and FileSearch, and then check a condition with Condition element this would check the condition at installation startup. This is not what I want. The user may change the target folder, so I need to check the existence of files in target folder in some step after the target folder can no longer be changed.

In my naivety I also tried to read the value of property mentioned in previous paragraph with SetProperty in some custom action, but that does not work either. The value is read alright, but it's the value that was determined at the start of setup.

Proper solution should probably include the answer given to another question. I'm currently toying with JScript, as per tip given in WiX tricks community wiki. Will probably make it a .dll to avoid anti-virus meddling, mentioned in Rob Mensching's blog post.

Community
  • 1
  • 1
Dialecticus
  • 16,400
  • 7
  • 43
  • 103

2 Answers2

3

An automatic uninstall wouldn't be possible anyways. Major Upgrades can't transition from Per-User to Per-Install and due to mutex considerations and scope considerations ( the per-user install could have been a different user ) you can't simply call an uninstall during your install anyways.

Because the install could have been done in another user context, MSI API calls to check meta state (like ComponentSearch ) won't work.

This means all you have left is AppSearch to look for your file. A snippet like this will populate the property FOUNDMYFILE with the full path of MyFile.dll if it is found:

<Property Id="FOUNDMYFILE">
  <DirectorySearch Id="FindMyFile" Path="[ProgramFilesFolder]MyCompany\MyProduct">
    <FileSearch Name="MyFile.dll"/>
  </DirectorySearch>
</Property>

The next thing to do is block the install if MyFile.dll is found and your product hasn't already been installed.

<Condition Message="[ProductName] cannot install due to prexisting
per-user installation.">FOUNDMYFILE and Not Installed</Condition>

Now this condition will prevent installation if you are not installed but the DLL is found. It won't prevent subsequent repair and uninstall.

However, that will only work for Minor Upgrades. For Major Upgrades the ProductCode has changed and from the installer's perspective it's not installed even though a previous version is. In that case you'd need to say something like.

<Condition Message="[ProductName] cannot install due to prexisting
 per-user installation.">FOUNDMYFILE and Not MAJORUPGRADEPROPERTY and
 Not Installed</Condition>
Christopher Painter
  • 54,556
  • 6
  • 63
  • 100
  • Thank you for your answer. It's useful, but it's not the solution for me. I've updated the question, and will work on it more. – Dialecticus Dec 05 '12 at 16:25
  • You could keep your CA simple by authoring it as a control event that gets run on the Next button after the destination has been selected and sets a property if a DLL is found. Then you could have another control event with a condition to perform a SpawnDialog (modal) if the expression evaluates to true. You'd also want similar scheduling of the CA in the UI and Execute Sequenc along with Type 19 Error Custom Actions to perform the same guarding during a silent installation. Sorry, it's a lot of ground to cover when answering a narrow question. – Christopher Painter Dec 05 '12 at 16:40
1

Here's how I've done it in the end. I'm calling custom action queued after RemoveExistingProducts:

<Property Id='ERRORIFFILESFOUND_MESSAGE'>!(loc.OlderVersionNotRemoved)</Property>

<CustomAction Id="CA.ErrorIfFilesFound"
  BinaryKey="SetupSupport.dll" DllEntry="ErrorIfFilesFound"
  Execute="immediate" Return="check" />

<CustomAction Id='CA.ErrorIfFilesFound.SetDirectory' 
  Property='ERRORIFFILESFOUND_DIRECTORY' Value='[INSTALLDIR]' />

<InstallExecuteSequence>
  <Custom Action='CA.ErrorIfFilesFound.SetDirectory' After='RemoveExistingProducts' />
  <Custom Action='CA.ErrorIfFilesFound' After='CA.ErrorIfFilesFound.SetDirectory'>
    Not MAJORUPGRADEPROPERTY and Not Installed
  </Custom>
</InstallExecuteSequence>

After this point old installation is removed if possible, so if there are files remaining in target folder then something's wrong. This moment happens rather late, but I don't know of a way to detect if existing files are going to be uninstalled. Code in custom action just checks if target folder exists and is not empty, and if so calls it quits (ExitOnFailure error handling removed for brevity):

UINT WINAPI ErrorIfFilesFound(MSIHANDLE hInstall)
{
    HRESULT hr = S_OK;
    LPWSTR pwszDirectory = NULL;
    LPWSTR pwszMessage = NULL;

    hr = ::WcaInitialize(hInstall, "ErrorIfFindFile");

    hr = ::WcaGetProperty(L"ERRORIFFILESFOUND_DIRECTORY", &pwszDirectory);

    if (PathIsDirectory(pwszDirectory) && !PathIsDirectoryEmpty(pwszDirectory))
    {
        hr = ::WcaGetProperty(L"ERRORIFFILESFOUND_MESSAGE", &pwszMessage);

        ProcessInstallMessage(hInstall,
            INSTALLMESSAGE_ERROR, pwszMessage, pwszDirectory);

        hr = -1;
    }

LExit:
    ReleaseStr(pwszMessage);
    ReleaseStr(pwszDirectory);

    UINT err = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE;
    return ::WcaFinalize(err);
}

void ProcessInstallMessage(MSIHANDLE hInstall,
    INSTALLMESSAGE type, TCHAR* lpszMessage, TCHAR* lpszParam)
{
    UINT rc;
    MSIHANDLE hMsg;
    UINT uiFieldNumber = lpszParam == NULL ? 0 : 1;

    hMsg = MsiCreateRecord(uiFieldNumber);

    if (hMsg != 0)
    {
        if ((rc = MsiRecordSetString(hMsg, 0, lpszMessage)) != ERROR_SUCCESS)
        {}
        else if (lpszParam != NULL &&
            (rc = MsiRecordSetString(hMsg, 1, lpszParam)) != ERROR_SUCCESS)
        {}
        else
        {
            rc = MsiProcessMessage(hInstall, type, hMsg);
        }

        rc = MsiCloseHandle(hMsg);
    }
}
Dialecticus
  • 16,400
  • 7
  • 43
  • 103