NSIS and Windows Installers for MultiUser (Current User)

[UPDATE: all code was published to GitHub]

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?

I’ve been writing Windows Installers using NSIS since 2010, and have used InstallShield and Visual Studio Setup Projects for a few years before that.

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]

NSIS has basically two paths for doing Multi-user Installations: MultiUser.nsh and UAC plug-in

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:

  • Admin: Installer will require Admin permissions (will ask for UAC elevation and/or admin credentials)
  • 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

User runs the installer, no elevation is required unless/until it’s necessary.

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

Between your pages, just add this call to MULTIUSER_PAGE_INSTALLMODE:

!insertmacro MUI_PAGE_LICENSE "..\Package\ServanttLicense.rtf"
;...
!insertmacro MULTIUSER_PAGE_INSTALLMODE ; this will show the 2 install options, unless it's an elevated inner process (in that case we know we should install for all users)
;...
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_COMPONENTS
!insertmacro MUI_PAGE_INSTFILES 

!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES

!insertmacro MUI_PAGE_FINISH

In your main section, after writing all files (and uninstaller) just add this call (MULTIUSER_RegistryAddInstallInfo):

Section "Servantt (required)"
	SectionIn RO

	; Set output path to the installation directory.
	SetOutPath $INSTDIR
	SetOverwrite on

	; Put files there
	File "..\Servantt.UI\BIN\Release\Obfuscated\${PROGEXE}"
	File "..\Servantt.UI\BIN\Release\Obfuscated\${PROGEXE}.config"
	File "..\Servantt.UI\BIN\Release\AlphaFS.dll"
	; ...
	File "..\Package\ServanttLicense.rtf"


	WriteUninstaller "${UNINSTALL_FILENAME}"
	!insertmacro MULTIUSER_RegistryAddInstallInfo ; add registry keys
SectionEnd

In the end of your uninstall, do the same (MULTIUSER_RegistryRemoveInstallInfo):

Section "Uninstall"
	; Remove files and uninstaller
	Delete $INSTDIR\*.dll
	Delete $INSTDIR\*.exe
	Delete $INSTDIR\*.rtf
	Delete $INSTDIR\*.config

	; Remove shortcuts, if any
	;SetShellVarContext all ; all users
	Delete "$SMPROGRAMS\Servantt\*.*"

	; Remove directories used
	RMDir "$SMPROGRAMS\Servantt"
	RMDir "$INSTDIR"

	!insertmacro MULTIUSER_RegistryRemoveInstallInfo ; Remove registry keys
SectionEnd

In the shortcuts section, don’t set var context (plugin will do), and use $SMPROGRAMS:

Section "Start Menu Shortcuts"
	;SetShellVarContext all ; all users
	CreateDirectory "$SMPROGRAMS\Servantt"
	;CreateShortCut "$SMPROGRAMS\Servantt\Uninstall.lnk" "$INSTDIR\${UNINSTALL_FILENAME}" "" "$INSTDIR\${UNINSTALL_FILENAME}" 0  ; shortcut for uninstall is bad cause user can choose this by mistake during search.
	CreateShortCut "$SMPROGRAMS\Servantt\${PRODUCT_NAME} ${VERSION}.lnk" "$INSTDIR\${PROGEXE}" "" "$INSTDIR\${PROGEXE}" 0
	Delete "$SMPROGRAMS\Servantt\Servantt 1.0*"
SectionEnd

Initialize the plugin both for install and for uninstall (MULTIUSER_INIT and MULTIUSER_UNINIT):

Function .onInit
	!insertmacro MULTIUSER_INIT
FunctionEnd

Function un.onInit
	!insertmacro MULTIUSER_UNINIT
FunctionEnd

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.

/*
SimpleMultiUser.nsh - Installer/Uninstaller that allows installations "per-user" (no admin required) or "per-machine" (asks elevation *only when necessary*)
By Ricardo Drizin (contact at http://drizin.com.br)

This plugin is based on [MultiUser.nsh (by Joost Verburg)](http://nsis.sourceforge.net/Docs/MultiUser/Readme.html) but with some new features and some simplifications:
- Installer allows installations "per-user" (no admin required) or "per-machine" (as original)
- If running user IS part of Administrators group, he is not forced to elevate and install per-machine (only if necessary)
- If running user is NOT part of Administrators group, he is still able to elevate and install per-machine (I expect that power-users will have administrator password, but will not be part of the administrators group)
- UAC Elevation happens only when necessary (when per-machine is selected), not in the start of the installer
- Uninstaller block is mandatory (why shouldn't it be?)
- If there are both per-user and per-machine installations, user can choose which one to remove during uninstall
- Correctly creates and removes shortcuts and registry (per-user and per-machine are totally independent)
- Fills uninstall information in registry like Icon and Estimated Size.
- If running as non-elevated user, the "per-machine" install can be allowed (automatically invoking UAC elevation) or can be disabled (suggesting to run again as elevated user)

*/

!verbose push
!verbose 3

;Standard NSIS header files
!include MUI2.nsh
!include nsDialogs.nsh
!include LogicLib.nsh
!include WinVer.nsh
!include FileFunc.nsh
!include UAC.nsh

;Variables
Var MultiUser.Privileges ; Current user level: "Admin", "Power" (up to Windows XP), or else regular user.
Var MultiUser.InstallMode ; Current Install Mode ("AllUsers" or "CurrentUser")
Var IsAdmin ; 0 (false) or 1 (true)
Var HasPerUserInstallation ; 0 (false) or 1 (true)
Var HasPerMachineInstallation ; 0 (false) or 1 (true)
Var PerUserInstallationFolder
Var PerMachineInstallationFolder 
Var HasTwoAvailableOptions ; 0 (false) or 1 (true)
Var RadioButtonLabel1
;Var RadioButtonLabel2
;Var RadioButtonLabel3

!ifdef MULTIUSER_INSTALLMODE_INSTALL_REGISTRY_KEY & MULTIUSER_INSTALLMODE_UNINSTALL_REGISTRY_KEY & MULTIUSER_INSTALLMODE_INSTDIR_REGISTRY_VALUENAME & MULTIUSER_INSTALLMODE_INSTDIR & UNINSTALL_FILENAME & VERSION & PROGEXE & PRODUCT_NAME & COMPANY_NAME
!else
  !error "Should define all variables: MULTIUSER_INSTALLMODE_INSTALL_REGISTRY_KEY & MULTIUSER_INSTALLMODE_UNINSTALL_REGISTRY_KEY & MULTIUSER_INSTALLMODE_INSTDIR_REGISTRY_VALUENAME & MULTIUSER_INSTALLMODE_INSTDIR & UNINSTALL_FILENAME & VERSION & PROGEXE & PRODUCT_NAME & COMPANY_NAME"
!endif

!define MULTIUSER_INSTALLMODE_INSTALL_REGISTRY_KEY2 "Software\${MULTIUSER_INSTALLMODE_INSTALL_REGISTRY_KEY}"
!define MULTIUSER_INSTALLMODE_UNINSTALL_REGISTRY_KEY2 "Software\Microsoft\Windows\CurrentVersion\Uninstall\${MULTIUSER_INSTALLMODE_UNINSTALL_REGISTRY_KEY}"

!ifndef MULTIUSER_INSTALLMODE_DISPLAYNAME
  !define MULTIUSER_INSTALLMODE_DISPLAYNAME "${PRODUCT_NAME} ${VERSION}"
!endif


RequestExecutionLevel user ; will ask elevation only if necessary

; Sets install mode to "per-machine" (all users).
!macro MULTIUSER_INSTALLMODE_ALLUSERS UNINSTALLER_PREFIX UNINSTALLER_FUNCPREFIX
  ;Install mode initialization - per-machine
  StrCpy $MultiUser.InstallMode AllUsers

  SetShellVarContext all

  !if "${UNINSTALLER_PREFIX}" != UN
    ;Set default installation location for installer
    StrCpy $INSTDIR "$PROGRAMFILES\${MULTIUSER_INSTALLMODE_INSTDIR}"
  !endif

  ; Checks registry for previous installation path (both for upgrading, reinstall, or uninstall)
  ReadRegStr $PerMachineInstallationFolder HKLM "${MULTIUSER_INSTALLMODE_INSTALL_REGISTRY_KEY2}" "${MULTIUSER_INSTALLMODE_INSTDIR_REGISTRY_VALUENAME}"
  ${if} $PerMachineInstallationFolder != ""
    StrCpy $INSTDIR $PerMachineInstallationFolder
  ${endif}

  !ifdef MULTIUSER_INSTALLMODE_${UNINSTALLER_PREFIX}FUNCTION
    Call "${MULTIUSER_INSTALLMODE_${UNINSTALLER_PREFIX}FUNCTION}"
  !endif
!macroend

; Sets install mode to "per-user".
!macro MULTIUSER_INSTALLMODE_CURRENTUSER UNINSTALLER_PREFIX UNINSTALLER_FUNCPREFIX
  StrCpy $MultiUser.InstallMode CurrentUser

  SetShellVarContext current

  !if "${UNINSTALLER_PREFIX}" != UN
    ;Set default installation location for installer
    ${if} ${AtLeastWin2000}
      StrCpy $INSTDIR "$LOCALAPPDATA\${MULTIUSER_INSTALLMODE_INSTDIR}"
    ${else}
      StrCpy $INSTDIR "$PROGRAMFILES\${MULTIUSER_INSTALLMODE_INSTDIR}"
    ${endif}
  !endif

  ; Checks registry for previous installation path (both for upgrading, reinstall, or uninstall)
  ReadRegStr $PerUserInstallationFolder HKCU "${MULTIUSER_INSTALLMODE_INSTALL_REGISTRY_KEY2}" "${MULTIUSER_INSTALLMODE_INSTDIR_REGISTRY_VALUENAME}"
  ${if} $PerUserInstallationFolder != ""
    StrCpy $INSTDIR $PerUserInstallationFolder
  ${endif}

  !ifdef MULTIUSER_INSTALLMODE_${UNINSTALLER_PREFIX}FUNCTION
    Call "${MULTIUSER_INSTALLMODE_${UNINSTALLER_PREFIX}FUNCTION}"
  !endif
!macroend

Function MultiUser.InstallMode.AllUsers
  !insertmacro MULTIUSER_INSTALLMODE_ALLUSERS "" ""
FunctionEnd

Function MultiUser.InstallMode.CurrentUser
  !insertmacro MULTIUSER_INSTALLMODE_CURRENTUSER "" ""
FunctionEnd

Function un.MultiUser.InstallMode.AllUsers
  !insertmacro MULTIUSER_INSTALLMODE_ALLUSERS UN un.
FunctionEnd

Function un.MultiUser.InstallMode.CurrentUser
  !insertmacro MULTIUSER_INSTALLMODE_CURRENTUSER UN un.
FunctionEnd

/****** Installer/uninstaller initialization ******/

!macro MULTIUSER_INIT_QUIT UNINSTALLER_FUNCPREFIX
  !ifdef MULTIUSER_INIT_${UNINSTALLER_FUNCPREFIX}FUNCTIONQUIT
    Call "${MULTIUSER_INIT_${UNINSTALLER_FUNCPREFIX}FUCTIONQUIT}"
  !else
    Quit
  !endif
!macroend

!macro MULTIUSER_INIT_TEXTS
  !ifndef MULTIUSER_INIT_TEXT_ADMINREQUIRED
    !define MULTIUSER_INIT_TEXT_ADMINREQUIRED "$(^Caption) requires administrator privileges."
  !endif

  !ifndef MULTIUSER_INIT_TEXT_POWERREQUIRED
    !define MULTIUSER_INIT_TEXT_POWERREQUIRED "$(^Caption) requires at least Power User privileges."
  !endif

  !ifndef MULTIUSER_INIT_TEXT_ALLUSERSNOTPOSSIBLE
    !define MULTIUSER_INIT_TEXT_ALLUSERSNOTPOSSIBLE "Your user account does not have sufficient privileges to install $(^Name) for all users of this computer."
  !endif
!macroend

!macro MULTIUSER_INIT_CHECKS UNINSTALLER_PREFIX UNINSTALLER_FUNCPREFIX

  ;Installer initialization - check privileges and set default install mode
  !insertmacro MULTIUSER_INIT_TEXTS

  UserInfo::GetAccountType
  Pop $MultiUser.Privileges
  ${if} $MultiUser.Privileges == "Admin"
    ${orif} $MultiUser.Privileges == "Power"
    StrCpy $IsAdmin 1
  ${else}
    StrCpy $IsAdmin 0
  ${endif}

  ; Checks registry for previous installation path (both for upgrading, reinstall, or uninstall)
  StrCpy $HasPerMachineInstallation 0
  StrCpy $HasPerUserInstallation 0
  ;Set installation mode to setting from a previous installation
  ReadRegStr $PerMachineInstallationFolder HKLM "${MULTIUSER_INSTALLMODE_INSTALL_REGISTRY_KEY2}" "${MULTIUSER_INSTALLMODE_INSTDIR_REGISTRY_VALUENAME}"
  ${if} $PerMachineInstallationFolder != ""
    StrCpy $HasPerMachineInstallation 1
  ${endif}
  ReadRegStr $PerUserInstallationFolder HKCU "${MULTIUSER_INSTALLMODE_INSTALL_REGISTRY_KEY2}" "${MULTIUSER_INSTALLMODE_INSTDIR_REGISTRY_VALUENAME}"
  ${if} $PerUserInstallationFolder != ""
    StrCpy $HasPerUserInstallation 1
  ${endif}
  
  ${if} $HasPerUserInstallation == "1" ; if there is only one installation... set it as default...
    ${andif} $HasPerMachineInstallation == "0"
    Call ${UNINSTALLER_FUNCPREFIX}MultiUser.InstallMode.CurrentUser
  ${elseif} $HasPerUserInstallation == "0" ; if there is only one installation... set it as default...
    ${andif} $HasPerMachineInstallation == "1"
    Call ${UNINSTALLER_FUNCPREFIX}MultiUser.InstallMode.AllUsers
  ${else} ; if there is no installation, or there is both per-user and per-machine...
    ${if} ${IsNT}
      ${if} $IsAdmin == "1" ;If running as admin, default to per-machine installation if possible (unless default is forced by MULTIUSER_INSTALLMODE_DEFAULT_CURRENTUSER)
        !if MULTIUSER_INSTALLMODE_DEFAULT_CURRENTUSER
          Call ${UNINSTALLER_FUNCPREFIX}MultiUser.InstallMode.CurrentUser
        !else
          Call ${UNINSTALLER_FUNCPREFIX}MultiUser.InstallMode.AllUsers
        !endif
      ${else} ;If not running as admin, default to per-user installation (unless default is forced by MULTIUSER_INSTALLMODE_DEFAULT_ALLUSERS and elevation is allowed MULTIUSER_INSTALLMODE_ALLOW_ELEVATION)
        !ifdef MULTIUSER_INSTALLMODE_DEFAULT_ALLUSERS & MULTIUSER_INSTALLMODE_ALLOW_ELEVATION
          Call ${UNINSTALLER_FUNCPREFIX}MultiUser.InstallMode.AllUsers
        !else
          Call ${UNINSTALLER_FUNCPREFIX}MultiUser.InstallMode.CurrentUser
        !endif
      ${endif}
    ${else} ; Not running Windows NT, (so it's Windows XP at best), so per-user installation not supported
      Call ${UNINSTALLER_FUNCPREFIX}MultiUser.InstallMode.AllUsers
    ${endif}
  ${endif}
  
!macroend

!macro MULTIUSER_INIT
  !verbose push
  !verbose 3

  ; se for inner (sub processo) e ainda assim não for admin... algo errado
  ${If} ${UAC_IsInnerInstance}
  ${AndIfNot} ${UAC_IsAdmin}
    ;MessageBox MB_OK "This account doesn't have admin rights"
    SetErrorLevel 0x666666 ;special return value for outer instance so it knows we did not have admin rights
    Quit
  ${EndIf}

  !insertmacro MULTIUSER_INIT_CHECKS "" ""
  !verbose pop 
!macroend

!macro MULTIUSER_UNINIT
  !verbose push
  !verbose 3
  !insertmacro MULTIUSER_INIT_CHECKS Un un.
  !verbose pop 
!macroend

/****** Modern UI 2 page ******/
!macro MULTIUSER_INSTALLMODEPAGE_INTERFACE
  Var MultiUser.InstallModePage
  Var MultiUser.InstallModePage.Text
  Var MultiUser.InstallModePage.AllUsers
  Var MultiUser.InstallModePage.CurrentUser
  Var MultiUser.InstallModePage.ReturnValue
!macroend

!macro MULTIUSER_PAGEDECLARATION_INSTALLMODE
  !insertmacro MUI_SET MULTIUSER_${MUI_PAGE_UNINSTALLER_PREFIX}INSTALLMODEPAGE ""
  !insertmacro MULTIUSER_INSTALLMODEPAGE_INTERFACE
  !insertmacro MULTIUSER_FUNCTION_INSTALLMODEPAGE MultiUser.InstallModePre_${MUI_UNIQUEID} MultiUser.InstallModeLeave_${MUI_UNIQUEID} "" ""
  !insertmacro MULTIUSER_FUNCTION_INSTALLMODEPAGE MultiUser.InstallModePre_${MUI_UNIQUEID} MultiUser.InstallModeLeave_${MUI_UNIQUEID} UN un.

  PageEx custom
    PageCallbacks MultiUser.InstallModePre_${MUI_UNIQUEID} MultiUser.InstallModeLeave_${MUI_UNIQUEID}
    Caption " "
  PageExEnd

  UninstPage custom un.MultiUser.InstallModePre_${MUI_UNIQUEID} un.MultiUser.InstallModeLeave_${MUI_UNIQUEID}
!macroend

!macro MULTIUSER_PAGE_INSTALLMODE
  ;Modern UI page for install mode
  !verbose push
  !verbose 3
  !insertmacro MUI_PAGE_INIT
  !insertmacro MULTIUSER_PAGEDECLARATION_INSTALLMODE
  !verbose pop
!macroend

!macro MULTIUSER_FUNCTION_INSTALLMODEPAGE PRE LEAVE UNINSTALLER_PREFIX UNINSTALLER_FUNCPREFIX
  Function "${UNINSTALLER_FUNCPREFIX}${PRE}"

    ${If} ${UAC_IsInnerInstance}
    ${AndIf} ${UAC_IsAdmin}
      ;MessageBox MB_OK 
      Call ${UNINSTALLER_FUNCPREFIX}MultiUser.InstallMode.AllUsers ; Inner Process (and Admin) - skip selection, inner process is always used for elevation (machine-wide)
      Abort ; // next page
    ${EndIf}
    
    ; If uninstalling, will check if there is both a per-user and per-machine installation. If there is only one, will skip the form.
    ; If uninstallation was invoked from the "add/remove programs" Windows will automatically requests elevation (depending if uninstall keys are in HKLM or HKCU)
    ; so (for uninstallation) just checking UAC_IsAdmin would probably be enought to determine if it's a per-user or per-machine. However, user can run the uninstall.exe from the folder itself, do I'd rather check.
    !if "${UNINSTALLER_PREFIX}" == UN
      ${if} $HasPerUserInstallation == "1" ; if there is only one installation... skip form.. only one uninstall available
        ${andif} $HasPerMachineInstallation == "0"
        Call ${UNINSTALLER_FUNCPREFIX}MultiUser.InstallMode.CurrentUser ; Uninstaller has only HasPerUserInstallation
        Abort ; // next page
      ${elseif} $HasPerUserInstallation == "0" ; if there is only one installation... skip form.. only one uninstall available
        ${andif} $HasPerMachineInstallation == "1"
        Call ${UNINSTALLER_FUNCPREFIX}MultiUser.InstallMode.AllUsers ; Uninstaller has only HasPerMachineInstallation
        Abort ; // next page
      ${endif} 
    !endif

    ${GetParameters} $R0
    ${GetOptions} $R0 "/allusers" $R1
    IfErrors notallusers
    ${if} $IsAdmin == "0" 
      ShowWindow $HWNDPARENT ${SW_HIDE} ; HideWindow would work?
      !insertmacro UAC_RunElevated
      Quit ;we are the outer process, the inner process has done its work (ExitCode is $2), we are done
    ${endif}
    Call ${UNINSTALLER_FUNCPREFIX}MultiUser.InstallMode.AllUsers ; Uninstaller has only HasPerMachineInstallation
    Abort ; // next page      
  notallusers:
    ${GetOptions} $R0 "/currentuser" $R1
    IfErrors notcurrentuser
    Call ${UNINSTALLER_FUNCPREFIX}MultiUser.InstallMode.CurrentUser ; Uninstaller has only HasPerUserInstallation
    Abort ; // next page
  notcurrentuser:
    
    
    !insertmacro MUI_PAGE_FUNCTION_CUSTOM PRE
    ;!insertmacro MUI_HEADER_TEXT_PAGE $(MULTIUSER_TEXT_INSTALLMODE_TITLE) $(MULTIUSER_TEXT_INSTALLMODE_SUBTITLE) ; "Choose Users" and "Choose for which users you want to install $(^NameDA)."
    
    !if "${UNINSTALLER_PREFIX}" != UN
      !insertmacro MUI_HEADER_TEXT "Choose Installation Options" "Who should this application be installed for?"
    !else
      !insertmacro MUI_HEADER_TEXT "Choose Uninstallation Options" "Which installation should be removed?"
    !endif
    
    nsDialogs::Create 1018
    Pop $MultiUser.InstallModePage

    ; default was MULTIUSER_TEXT_INSTALLMODE_TITLE "Choose Users"
    !if "${UNINSTALLER_PREFIX}" != UN
      ${NSD_CreateLabel} 0u 0u 300u 20u "Please select whether you wish to make this software available to all users or just yourself"
      StrCpy $8 "Anyone who uses this computer (&all users)" ; this was MULTIUSER_INNERTEXT_INSTALLMODE_ALLUSERS "Install for anyone using this computer"
      StrCpy $9 "Only for &me" ; this was MULTIUSER_INNERTEXT_INSTALLMODE_CURRENTUSER "Install just for me"
    !else
      ${NSD_CreateLabel} 0u 0u 300u 20u "This software is installed both per-machine (all users) and per-user. $\r$\nWhich installation you wish to remove?"
      StrCpy $8 "Anyone who uses this computer (&all users)" ; this was MULTIUSER_INNERTEXT_INSTALLMODE_ALLUSERS "Install for anyone using this computer"
      StrCpy $9 "Only for &me" ; this was MULTIUSER_INNERTEXT_INSTALLMODE_CURRENTUSER "Install just for me"
    !endif
    Pop $MultiUser.InstallModePage.Text

    ; criando os radios (disabled se não for admin/power) e pegando os hwnds (handles)
    ${NSD_CreateRadioButton} 10u 30u 280u 20u "$8"
    Pop $MultiUser.InstallModePage.AllUsers
    ${if} $IsAdmin == "0" 
      !ifdef MULTIUSER_INSTALLMODE_ALLOW_ELEVATION ; if elevation is allowed.. "(will prompt for admin credentials)" (will appear at bottom when option is chosen)
        StrCpy $HasTwoAvailableOptions 1
      !else
        SendMessage $MultiUser.InstallModePage.AllUsers ${WM_SETTEXT} 0 "STR:$8 (must run as admin)" ; since radio button is disabled, we add that comment to the disabled control itself
        EnableWindow $MultiUser.InstallModePage.AllUsers 0 # start out disabled
        StrCpy $HasTwoAvailableOptions 0
      !endif
    ${else}
      StrCpy $HasTwoAvailableOptions 1
    ${endif}
    
    ;${NSD_CreateRadioButton} 20u 70u 280u 10u "$9"
    System::Call "advapi32::GetUserName(t.r0,*i${NSIS_MAX_STRLEN})i"
    ${NSD_CreateRadioButton} 10u 50u 280u 20u "$9 ($0)"
    Pop $MultiUser.InstallModePage.CurrentUser


    nsDialogs::SetUserData $MultiUser.InstallModePage.AllUsers 1 ; Install for All Users (1, pra exibir o icone SHIELD de elevation)
    nsDialogs::SetUserData $MultiUser.InstallModePage.CurrentUser 0  ; Install for Single User (0 pra não exibir)

    ${if} $HasTwoAvailableOptions == "1" ; if there are 2 available options, bind to radiobutton change
      ${NSD_OnClick} $MultiUser.InstallModePage.CurrentUser ${UNINSTALLER_FUNCPREFIX}InstModeChange
      ${NSD_OnClick} $MultiUser.InstallModePage.AllUsers ${UNINSTALLER_FUNCPREFIX}InstModeChange
    ${endif}
    
    ${NSD_CreateLabel} 0u 110u 280u 50u ""
    Pop $RadioButtonLabel1
    ;${NSD_CreateLabel} 0u 120u 280u 20u ""
    ;Pop $RadioButtonLabel2
    ;${NSD_CreateLabel} 0u 130u 280u 20u ""
    ;Pop $RadioButtonLabel3

    
    
    ${if} $MultiUser.InstallMode == "AllUsers" ; setting defaults
      SendMessage $MultiUser.InstallModePage.AllUsers ${BM_SETCHECK} ${BST_CHECKED} 0 ; set as default
      SendMessage $MultiUser.InstallModePage.AllUsers ${BM_CLICK} 0 0 ; trigger click event
    ${else}
      SendMessage $MultiUser.InstallModePage.CurrentUser ${BM_SETCHECK} ${BST_CHECKED} 0 ; set as default
      SendMessage $MultiUser.InstallModePage.CurrentUser ${BM_CLICK} 0 0 ; trigger click event
    ${endif}
    
    !insertmacro MUI_PAGE_FUNCTION_CUSTOM SHOW
    nsDialogs::Show

  FunctionEnd

  Function "${UNINSTALLER_FUNCPREFIX}${LEAVE}"
    SendMessage $MultiUser.InstallModePage.AllUsers ${BM_GETCHECK} 0 0 $MultiUser.InstallModePage.ReturnValue

    ${if} $MultiUser.InstallModePage.ReturnValue = ${BST_CHECKED}
      ${if} $IsAdmin == "0" 
        !ifdef MULTIUSER_INSTALLMODE_ALLOW_ELEVATION ; if it's not Power or Admin, but elevation is allowed, then elevate...
          ;MessageBox MB_OK "Will elevate and quit"
          ShowWindow $HWNDPARENT ${SW_HIDE} ; HideWindow would work?
          !insertmacro UAC_RunElevated
          ;MessageBox MB_OK "[$0]/[$1]/[$2]/[$3]"
          
          ;http://www.videolan.org/developers/vlc/extras/package/win32/NSIS/UAC/Readme.html
          ;http://nsis.sourceforge.net/UAC_plug-in
          ${Switch} $0
          ${Case} 0
            ${If} $1 = 1 
              Quit ;we are the outer process, the inner process has done its work (ExitCode is $2), we are done
            ${EndIf}
            ${If} $1 = 3 ;RunAs completed successfully, but with a non-admin user
              ${OrIf} $2 = 0x666666 ;our special return, the new process was not admin after all 
              MessageBox mb_IconStop|mb_TopMost|mb_SetForeground "You need to login with an account that is a member of the admin group to continue..."
            ${EndIf}
            ${Break}
          ${Case} 1223 ;user aborted
            ;MessageBox mb_IconStop|mb_TopMost|mb_SetForeground "This option requires admin privileges, aborting!"
            ;Quit ; instead of quit just abort going to the next page, and stay in the radiobuttons
            ${Break}
          ${Case} 1062
            MessageBox mb_IconStop|mb_TopMost|mb_SetForeground "Logon service not running, aborting!" ; "Unable to elevate, Secondary Logon service not running!"
            ;Quit ; instead of quit just abort going to the next page, and stay in the radiobuttons
            ${Break}
          ${Default}
            MessageBox mb_IconStop|mb_TopMost|mb_SetForeground "Unable to elevate, error $0"
            ;Quit ; instead of quit just abort going to the next page, and stay in the radiobuttons
            ${Break}
          ${EndSwitch}        

          ShowWindow $HWNDPARENT ${SW_SHOW}
          BringToFront
          Abort ; Stay on page - http://nsis.sourceforge.net/Abort
        !else       
            ;se não é Power ou Admin, e não é permitida elevation, então nem deveria ter chegado aqui... o radiobutton deveria estar disabled
        !endif
      ${else}
        Call ${UNINSTALLER_FUNCPREFIX}MultiUser.InstallMode.AllUsers ; if it's Power or Admin, just go on with installation...
      ${endif}
    ${else}
      Call ${UNINSTALLER_FUNCPREFIX}MultiUser.InstallMode.CurrentUser
    ${endif}

    !insertmacro MUI_PAGE_FUNCTION_CUSTOM LEAVE
  FunctionEnd

  Function "${UNINSTALLER_FUNCPREFIX}InstModeChange"
    pop $1
    nsDialogs::GetUserData $1
    pop $1
    GetDlgItem $0 $hwndParent 1 ; get item 1 (next button) at parent window, store in $0 - (0 is back, 1 is next .. what about CANCEL? http://nsis.sourceforge.net/Buttons_Header )
    
    StrCpy $7 ""
    ${if} "$1" == "0" ; current user
      ${if} $HasPerUserInstallation == "1"
        !if "${UNINSTALLER_PREFIX}" != UN
          StrCpy $7 "There is already a per-user installation. ($PerUserInstallationFolder)$\r$\nWill reinstall/upgrade."
        !else
          StrCpy $7 "There is a per-user installation. ($PerUserInstallationFolder)$\r$\nWill uninstall."
        !endif
      ${else}
        StrCpy $7 "Fresh install for current user only"
      ${endif}
      SendMessage $0 ${BCM_SETSHIELD} 0 0 ; hide SHIELD
    ${else} ; all users
      ${if} $HasPerMachineInstallation == "1"
        !if "${UNINSTALLER_PREFIX}" != UN
          StrCpy $7 "There is already a per-machine installation. ($PerMachineInstallationFolder)$\r$\nWill reinstall/upgrade."
        !else
          StrCpy $7 "There is a per-machine installation. ($PerMachineInstallationFolder)$\r$\nWill uninstall."
        !endif
      ${else}
        StrCpy $7 "Fresh install for all users"
      ${endif}
      ${if} $IsAdmin == "0"
        StrCpy $7 "$7 (will prompt for admin credentials)"
        SendMessage $0 ${BCM_SETSHIELD} 0 1 ; display SHIELD
      ${else}
        SendMessage $0 ${BCM_SETSHIELD} 0 0 ; hide SHIELD
      ${endif}
    ${endif}
    SendMessage $RadioButtonLabel1 ${WM_SETTEXT} 0 "STR:$7"
    ;SendMessage $RadioButtonLabel2 ${WM_SETTEXT} 0 "STR:$8"
    ;SendMessage $RadioButtonLabel3 ${WM_SETTEXT} 0 "STR:$9"
  FunctionEnd

!macroend

; SHCTX is the hive HKLM if SetShellVarContext all, or HKCU if SetShellVarContext user
!macro MULTIUSER_RegistryAddInstallInfo
  !verbose push
  !verbose 3

  ; Write the installation path into the registry
  WriteRegStr SHCTX "${MULTIUSER_INSTALLMODE_INSTALL_REGISTRY_KEY2}" "${MULTIUSER_INSTALLMODE_INSTDIR_REGISTRY_VALUENAME}" "$INSTDIR" ; "InstallLocation"

  ; Write the uninstall keys for Windows
  ${if} $MultiUser.InstallMode == "AllUsers" ; setting defaults
    WriteRegStr SHCTX "${MULTIUSER_INSTALLMODE_UNINSTALL_REGISTRY_KEY2}" "DisplayName" "${MULTIUSER_INSTALLMODE_DISPLAYNAME}"
    WriteRegStr SHCTX "${MULTIUSER_INSTALLMODE_UNINSTALL_REGISTRY_KEY2}" "${MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_VALUENAME}" '"$INSTDIR\${UNINSTALL_FILENAME}" /allusers' ; "UninstallString"
  ${else}
    WriteRegStr SHCTX "${MULTIUSER_INSTALLMODE_UNINSTALL_REGISTRY_KEY2}" "DisplayName" "${MULTIUSER_INSTALLMODE_DISPLAYNAME} (only current user)" ; "add/remove programs" will show if installation is per-user
    WriteRegStr SHCTX "${MULTIUSER_INSTALLMODE_UNINSTALL_REGISTRY_KEY2}" "${MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_VALUENAME}" '"$INSTDIR\${UNINSTALL_FILENAME}" /currentuser' ; "UninstallString"
  ${endif}

  WriteRegStr SHCTX "${MULTIUSER_INSTALLMODE_UNINSTALL_REGISTRY_KEY2}" "DisplayVersion" "${VERSION}"
  WriteRegStr SHCTX "${MULTIUSER_INSTALLMODE_UNINSTALL_REGISTRY_KEY2}" "DisplayIcon" "$INSTDIR\${PROGEXE},0"
  WriteRegStr SHCTX "${MULTIUSER_INSTALLMODE_UNINSTALL_REGISTRY_KEY2}" "Publisher" "${COMPANY_NAME}"
  WriteRegDWORD SHCTX "${MULTIUSER_INSTALLMODE_UNINSTALL_REGISTRY_KEY2}" "NoModify" 1
  WriteRegDWORD SHCTX "${MULTIUSER_INSTALLMODE_UNINSTALL_REGISTRY_KEY2}" "NoRepair" 1
  ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 ; get folder size, convert to KB
  IntFmt $0 "0x%08X" $0
  WriteRegDWORD SHCTX "${MULTIUSER_INSTALLMODE_UNINSTALL_REGISTRY_KEY2}" "EstimatedSize" "$0"

  !verbose pop 
!macroend

!macro MULTIUSER_RegistryRemoveInstallInfo
  !verbose push
  !verbose 3

  ; Remove registry keys
  DeleteRegKey SHCTX "${MULTIUSER_INSTALLMODE_UNINSTALL_REGISTRY_KEY2}"
  DeleteRegKey SHCTX "${MULTIUSER_INSTALLMODE_INSTALL_REGISTRY_KEY2}"
 
  !verbose pop 
!macroend



!verbose pop

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:

"C:\Program Files\7-Zip\7z.exe" x UAC.zip Plugins\* -o"C:\Program Files (x86)\NSIS\"
"C:\Program Files\7-Zip\7z.exe" x UAC.zip UAC.nsh -o"C:\Program Files (x86)\NSIS\Include"

PS: I won’t update this post for every modification in the script.
The most recent version of this script can be found here with install instructions.

Hope you liked.

Want me to build an installer for your company? Contact me

comments powered by Disqus