Old dogs can learn new tricks indeed

NSIS and Windows Installers for MultiUser (Current User)

A few weeks ago, I received a nice feedback from a customer who had just tried my last product. Well, he was a bit disappointed with some things (mainly the bad performance, compared to the commercial alternative, and also the lack of documentation). But the good part of the feedback is that he told me that my product did the job for his needs, and gave me lots of suggestions and good criticism.

His feedback made me dedicate a few days fixing some bugs, improving some things, and doing deeper tests on this product. That led me to find some bugs with the installer I had been using for years on other products. I couldn’t recall exactly when it happened, but somewhen I broke my installer, which stopped installing start menu shortcuts correctly (it was installing only for the Administrator, not for all users). That problem forced me to give some attention to my NSIS installer script, and (after fixing the problem) I decided it was time to implement something that was in my backlog for a long time: Multi-user Installations.

Multi-user Installations are when some user installs something even without having administrator permissions, so that the installation is stored in users’s folder and user’s registry, and is not available to other machine users. In other words, program is NOT stored in Program Files, as opposed to what happens in regular per-machine installations.

Why NSIS?

When I decide to use NSIS, major options were InstallShield, NSIS and Wix. Since I was not willing to invest that money in InstallShield (almost 3.000 USD), my choice was between NSIS and Wix. Since NSIS is script-based (and has a compiler!), and Wix is xml-based, my choice was pretty obvious. [Edit: nowadays there is Wix# which uses C-Sharp scripting. But I think it’s still not as mature as NSIS]

MultiUser.nsh

MultiUser.nsh is a script (an include) that you should incorporate into your main script. In general it has poor documentation (which seems to be a general problem with most NSIS plugins, although NSIS documentation is pretty decent), and I didn’t find it quite exactly doing what I wanted, or easy to understand.

Basically, you should define in a constant (!def) what level of permission your installer will require:

Power: It was something that existed only in (or up to?) Windows XP, which was something like “you can install programs, and configure services, but you can’t do anything harmful to the computer”. It doesn’t exist anymore since XP.

Highest: Installer will ask elevation only if your user is also part of Administrators. Else it will run as a regular user.

User: Installer will not request special permissions.

A very brief summary of what this script does:

In your script you should have an initialization function (.onInit) which should include (!insertmacro) the code block from MULTIUSER_INIT

MULTIUSER_INIT will check if the running user has the minimum permission declared (abort if not)

MULTIUSER_INIT will set up variables (like default folders for INSTALL DIR, folder for START MENU shortcuts, registry hive, etc) according to some rules:

If permission required is “User”, install will always be made on a per-user. (Unless the the installer is explicitely invoked as admin)

If permission required is “Highest” and user is not an administrator, install will always be made on a per-user. (Unless he explicitely runs the installer as admin)

If permission required is Power or Admin, or Highest and user is an Administrator, then user will have the option to choose between installing per-user or per-machine. Default will be per-machine, unless redefined by constant MULTIUSER_INSTALLMODE_DEFAULT_CURRENTUSER or by command line arguments. Those choices are shown in page MULTIUSER_PAGE_INSTALLMODE

While this seems like a reasonable logic, I wanted something different: I wanted to ask for elevation only when necessary, to avoid scaring users. That would allow users to be more confident into installing on a per-user basis and knowing that it will not install anything more invasive.
And more important, I expect that power-users will have administrator password, but will not be part of the administrators group. Also, I don’t expect that they know in advance that the installer should be run as administrator - this should be asked only when necessary.
I also wanted to have the per-user install and per-machine install as completely independent installations, allowing users even to have different versions of the same software, so that they can freely try new versions on limited user-scope. None of that features was available in MultiUser.nsh plugin.

UAC plug-in

UAC plug-in is not only an include script but also a DLL that allows you to call some low-level Windows UAC functions.
First time I used the UAC plug-in was a few weeks ago, when I noticed that the “launch program” option at the end of the installer was always being run as Administrator, which was causing problems with some users.
(Users reported that all configuration made during the first run was lost when they launched the program for the second time).

In order to have final “launch” to always run in the context of the current user (even if installation was elevated) I had to do small changes to my NSIS script:

;!define MUI_FINISHPAGE_RUN "$INSTDIR\${PROGEXE}" ; this was doing the first run in elevated mode..
!define MUI_FINISHPAGE_RUN ; we have to keep this in order to show the checkbox and the "launch app now"
!define MUI_FINISHPAGE_RUN_FUNCTION LaunchApplication ; now this function runs in the context of the running user... even if elevated for the install
!define MUI_FINISHPAGE_RUN_TEXT "Launch ${PRODUCT_NAME}"
; ....
; ...
;http://nsis.sourceforge.net/ShellExecAsUser_plug-in
;http://stackoverflow.com/questions/32533397/nsis-registry-plugin-not-found
Function LaunchApplication ; Launching your app as the current user:
SetOutPath $INSTDIR
ShellExecAsUser::ShellExecAsUser "" "$INSTDIR\${PROGEXE}" ""
FunctionEnd

After that successful experience with the UAC plug-in (and the not so successful with the MultiUser.nsh), I decided to rewrite the original MultiUser.nsh using the UAC plug-in to do exactly what I need.

Final Result of my Installer

If the ALLOW_ELEVATION is NOT defined and user is NOT running as admin, only per-user installation is offered:

If the user is running as admin or if ALLOW_ELEVATION is defined, both options are offered:

PS: If running as regular user, default is to suggest a per-user install, unless DEFAULT_ALLUSERS is defined

Reinstallations/Upgrades will always suggest to use the existing installation:

If there are both per-user and per-machine installations, uninstaller will ask which one should be removed.

The “add/remove programs” will show individual installations (one is stored in HKLM and other in HKCU):

If you choose to uninstall the per-machine installation (first row) from this “add/remove” screen, Windows will automatically request elevation, so (unless you also have a per-user installation on the Administrator account) it will automatically remove the per-machine installation.

If you choose uninstall the per-user installation (second row) from this “add/remove” screen, installer will run on user context, so the “which installation to remove” screen will only be shown if there is also a per-machine installation, or else this screen is skipped.

PS: This “choose installation to remove” screen was designed to appear in the uninstall (when there are multiple installations) because when user launches the “uninstall.exe” from the folder we cannot automatically tell if he wants to remove the per-user or per-machine. However, later (when writing this) I realized that I could add some argument to the “UninstallString” so that when uninstaller was launched from the “add/remove programs” we could know in advance which install should be removed, and skip this screen at all.

Using my plug-in

In your main NSIS script you will need a few modifications (see constants defined, and include MultiUserDrizin before MUI2.nsh):

;!define MULTIUSER_EXECUTIONLEVEL Highest ; disabled.. because MultiUserDrizin.nsh will set the correct permission (user)
;http://metageta.googlecode.com/svn-history/r537/build/installer/buildmetageta.nsi
!define APP_NAME "Servantt"
!define UNINSTALL_FILENAME "uninstall.exe"
!define MULTIUSER_INSTALLMODE_INSTDIR "${APP_NAME}" ; suggested name of directory to install (under $PROGRAMFILES or $LOCALAPPDATA)
!define MULTIUSER_INSTALLMODE_INSTALL_REGISTRY_KEY "${APP_NAME}" ; registry key for INSTALL info, placed under [HKLM|HKCU]\Software (can be ${APP_NAME} or some {GUID})
!define MULTIUSER_INSTALLMODE_UNINSTALL_REGISTRY_KEY "${APP_NAME}" ; registry key for UNINSTALL info, placed under [HKLM|HKCU]\Software\Microsoft\Windows\CurrentVersion\Uninstall (can be ${APP_NAME} or some {GUID})
!define MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_VALUENAME "UninstallString"
!define MULTIUSER_INSTALLMODE_INSTDIR_REGISTRY_VALUENAME "InstallLocation"
;!define MULTIUSER_INSTALLMODE_DISPLAYNAME "${APP_NAME} ${VERSION} ${PRODUCT_EDITION}" ; this is optional... name that will be displayed in add/remove programs (default is ${APP_NAME} ${VERSION})
!define MULTIUSER_INSTALLMODE_ALLOW_ELEVATION ; allow requesting for elevation... if false, radiobutton will be disabled and user will have to restart installer with elevated permissions
!define MULTIUSER_INSTALLMODE_DEFAULT_ALLUSERS ; only available if MULTIUSER_INSTALLMODE_ALLOW_ELEVATION
; if you don't have UAC plug-in installed, add plugin directories to the search path
!addplugindir /x86-ansi "..\..\NsisMultiUser\Plugins\x86-ansi\"
!addplugindir /x86-unicode "..\..\NsisMultiUser\Plugins\x86-unicode\"
; for the header, you could either include the path (full or relative), or you could just add the include directory to the search path
;!include "..\..\NsisMultiUser\Include\NsisMultiUser.nsh"
!addincludedir "..\..\NsisMultiUser\Include\"
!include "NsisMultiUser.nsh"
!include "MUI2.nsh"
!include UAC.nsh
!include LogicLib.nsh

Full code for my Plugin

This is the full code for my plugin. Since I’m not fluent in NSIS language, I must confess that I used the same structure from MultiUser.nsh, but removed lots of things that I didn’t need and added lots of other features to have the requirements that I described above. So please forgive me if code seems overly complex.

The most recent version of this script can be found here with install instructions.

Just save this script into “C:\Program Files (x86)\NSIS\Include\” folder.

For installing the UAC plugin, just unzip the NSH into the “\Includes” folder, and the “\Plugins” folder into “C:\Program Files (x86)\NSIS\Plugins”. If you like automated installs (like me) this is what you want: