-2

The problem is following: I have my custom uninstaller called before MSI uninstall. After shutting down my application properly it calls msiexec to use Windows Installer to uninstall MSI.

It's done by executing something like "msiexec /x{PRODUCT_CODE} /promptrestart".

And here is important thing - if the system is not restarted after uninstallation, and then the user installs the app again, some of its files will be deleted after next restart, so it's not acceptable. The restart is required, however, I need prompt, automatic and unconditional restart is evil and should never ever be used.

So, the invocation above displays STUPID "uninstall / repair" dialog. I do not want it. When I use "msiexec /x{PRODUCT_CODE} /qr /promptrestart" - then it uninstalls nicely, however it refuses propt for restart afterwards.

I have read about setting ARPNOREPAIR property.

But the idiots who gave that answer wouldn't care to say WHERE and HOW that property could be set. Even... Where the property belongs, it's the property of what? MSI file?

Then, maybe is it another way to achieve this, like invoke the prompt for restart from my code, but... how? The uninstaller should remove all my files until that moment. Maybe it's possible to execute a kind of script after the uninstallation process is complete?

(One more problem, the Windows Installer doesn't delete ALL files and directories created by my app. It would be nice if I could execute some code to clean up better.)

UPDATE I see 2 paths ahead: make a script to be run once the uninstallation ends (like using Registry or Task Scheduler or IDK), use Win32 API to modify MSI file, because AFAIK it's possible to change its properties that way.

Harry
  • 3,090
  • 3
  • 26
  • 56

2 Answers2

1

Questions: Some questions first.

  • Restart Manager: Are you familiar with the Restart Manager feature of MSI? Intended to help shut down and restart applications without the need for reboots. I would give it a quick skim? I think this is your real solution?

ARP Applet vs MSI Dialogs: The ARPNOREPAIR property is set in the MSI itself - in the property table. It affects only what is seen in Windows' Add / Remove Programs applet (ARP = Add / Remove Programs), and not what you see when your MSI is invoked via command line. Then you see the dialogs defined in that MSI itself (which can be changed - not entirely trivial to do).

ARP / Add Remove Programs Applet: A quick review of this applet below:

  • Hold Windows Key and Tap R. Type: appwiz.cpl and press Enter. This opens the Add /Remove Programs Applet.
  • Select the different package entries in the list to see different settings for ARPNOREPAIR, ARPNOMODIFY, etc...

    ARP

    • If ARPNOREPAIR is set in the MSI's property table then the Repair entry is missing.
    • If ARPNOMODIFY is set in the MSI's property table then the Change entry is missing.
    • If ARPNOREMOVE is set in the MSI's property table then the Remove entry is missing.
  • If the special ARPSYSTEMCOMPONENT property is set, then the MSI will be missing from ARP altogether.

Links:

Stein Åsmul
  • 34,628
  • 23
  • 78
  • 140
  • The custom uninstaller of mine is a registry hack. It changes original UninstallString, invokes my code (so it can shut down the app properly), then calls original content, msiexec with parameters. Without the hack I had not repair option. – Harry Mar 05 '19 at 20:11
  • I depend on MoveFileEx() from kernel32.dll for deleting files in use (after restart). The file I need to remove like this is low-level driver. – Harry Mar 05 '19 at 20:14
  • Changing my toolkit to create MSI is not a good option because of my deadlines. It would take a lot of time to learn a new toolkit, then make everything work. Yes, my app use a Windows service (one), kernel-driver and uses a lot of Windows internals voodoo. There is a nice tool (an add on) to autmate making installers, however I haven't expected it would be ridiculously hard to trigger things before and after install / uninstall. Basically I came far with that. The whole installation experience works. Now I only need that prompt for restart. – Harry Mar 05 '19 at 20:20
  • 1
    Will have a look. Just off the top of my head, did you try: ``msiexec.exe /x {PROD-GUID} /QB+ REBOOTPROMPT=""`` - Basic UI with a modal dialog at the end. Not sure if the REBOOTPROMPT is even needed, will check when I get a chance. Just wanted to post that so you can test yourself if you want. It is so late that I am probably mixing up something. – Stein Åsmul Mar 06 '19 at 04:22
  • I tried `/x{productCode} /qr /promptrestart`, it's reduced UI. It works except of the restart. There is no prompt, however I can use /forcerestart and there is forced restart without prompt. In documentation stands you can't use /promptrestart with /q. Not good. – Harry Mar 06 '19 at 08:51
  • I just tried `/QB+ REBOOTPROMPT=""` - no restart prompt either. – Harry Mar 06 '19 at 09:11
  • I suppose you could insert a custom action at the end of the installation sequence that does not obey the UI level and show a prompt at the end of the setup notifying the user about the required REBOOT - if one is required. Not at all OK design-wise and application packagers in corporations will hate it since it would lock up a large-scale deployment if they forget to comment that custom action out. Specifically the dialog would likely show in a non-interactive deployment account where there is no user to dismiss the dialog and the whole deployment will fail for all computers requiring reboot. – Stein Åsmul Mar 06 '19 at 14:30
  • 1
    I don't like the above at all. For the record. What you need is [**a better MSI tool**](https://stackoverflow.com/a/50229840/129130) I think - and apply proper shutdown of your services via built-in MSI mechanisms and full control over the GUI dialogs. WiX would do, and you can get a head-start by decompiling your existing MSI with the dark.exe WiX tool. It creates WiX markup to compile an MSI from. It needs cleanup. Involved, but not rocket science. Installshield and Advanced Installer would be great too, and much easier to learn. – Stein Åsmul Mar 06 '19 at 14:30
  • Thanks, I'll consider it, the installation process gives me much more hard time than it should. I struggle with running the tray application by installer, because when it's run by SYSTEM I cannot shut it down sending WM_CLOSE from another SYSTEM process. There is a lot of such small issues costing ridiculous amount of time :/ – Harry Mar 06 '19 at 18:24
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/189546/discussion-between-stein-asmul-and-harry). – Stein Åsmul Mar 06 '19 at 18:27
-1

So, there is an "ugly hack" which solves the exact problem:

First - we need an executable, that isn't affected by the installer. This one is easy, we just copy that one installed exe to a TEMP directory and run it from there.

The next step is to that file must wait unit the uninstall phase is done. There are a couple of ways of doing so. You can observe the installer process, you can observe the file system if main program file is deleted. Considering the pace of common installer operations, polling once a second is a good enough option.

The next step is optional - you remove remaining files created by application, empty directories and such.

The next step is reboot itself, MessageBox.Show() from PresentationFramework is fine to ask user, when user answer OK or YES, then reboot itself can be performed in many ways, I use ExitWindowsEx() from user32.dll since it's probably what msiexec calls internally.

Here's example code:

if (MessageBox.Show(RestartPromptMsg, "", MessageBoxButton.OKCancel, MessageBoxImage.Exclamation) == MessageBoxResult.OK) {
    NativeMethods.ExitWindowsEx(
        NativeMethods.Flags.Reboot,
        NativeMethods.Reason.MajorApplication | NativeMethods.Reason.MinorInstallation | NativeMethods.Reason.FlagPlanned
    );
}

Of course some parameters could be passed to our special clean up executable, so it could do some extra things, like skip the restart prompt if it's not really required.

The last step is to delete our executable itself. It's easy, but it's tricky. Again I hope my example code would help:

var cleanUpTempPath = Path.Combine(Path.GetTempPath(), CleanUpExe);
File.Copy(CleanUpPath, cleanUpTempPath, overwrite: true);
Process.Start(new ProcessStartInfo {
    FileName = "cmd",
    Arguments = $"/c (\"{cleanUpTempPath}\" -purge \"{InstallerDir}\") & (del \"{cleanUpTempPath}\")",
    UseShellExecute = false,
    CreateNoWindow = true
});

We use cmd.exe special feature, the power of & and (). Commands separated with & gets executed when previous command exits. So when our clen up exe completes, it's gets deleted by the same cmd instance which called it. Remember to quote all paths, they can contain spaces. Remember to enclose a command with arguments in parentheses, because otherwise the & operator would be seen as a parameter to the previous command, not the cmd.exe.

I tested it in my big production application and it works as charm. The code examples don't work when just pasted, if you're looking for complete code, just google for it, there are plenty of working examples on pinvoke.net and StackOverflow.

Harry
  • 3,090
  • 3
  • 26
  • 56