Silent install with Win7 and UAC plugin - nsis

I have an installer that can handle command-line parameters and /S silent installation. I recently upgraded the installer to better conform to standards for Vista / W7 UAC management by using the UAC plugin and switching the RequestExecutionLevel from admin to user.
The tests shown that the UAC plugin is working well for rights elevation in GUI mode and command line silent mode (though the "silent" installation is not actually completely "silent" due to the elevation dialog).
Now I am told by customers that while wrapping my setup into a third-party deployment tool, their tool is getting a 1223 (ERROR_CANCELLED) error code (that seems to be "elevation cancelled by user"). My setup does not return error codes directly via errorlevel, so I am guessing that this code is returned somewhere from the UAC plugin that seems to perform the elevation with a hack around the system "runas" dialog.
am I right about the origin of the returned code? Though when I cancel the elevation dialog I get a errorlevel 5 (access denied) and not 1223
to integrate the UAC plugin I am using a InitElevation macro that is call at the beginning of .onInit, is it OK?
I am not sure to understand correctly the behavior of the UAC plugin in silent mode: the key seems to be in the _() function of the plugin dll, in the 0 case of the switch. If the NSIS dilaog is not visible (or not existent?) due to silent mode, could it make fail the "runas" invocation? In my test environment, while calling the silet setup from command line it is ok, but what if called from a deployment tool?
Macro to integrate the UAC plugin:
!macro InitElevation thing
uac_tryagain:
${Debug} "Init UAC_RunElevated"
!insertmacro UAC_RunElevated
;${debug} "$$0=$0 $$1=$1 $$2=$2 $$3=$3"
${Switch} $0
${Case} 0
${IfThen} $1 = 1 ${|} Quit ${|} ;we are the outer process, the inner process has done its work, we are done
${If} $3 <> 0 ;if we are admin
System::Call "kernel32::GetCurrentProcessId()i.r0"
${If} ${UAC_IsInnerInstance}
; If we are in the elevated process, we need to get our grandparent PID for console
${GetProcessParent} $0 $ConsoleParentPID
;${Debug} "From elevated $0 process, parent process is $ConsoleParentPID"
${GetProcessParent} $ConsoleParentPID $ConsoleParentPID
;${Debug} "grand parent process is $ConsoleParentPID"
${Else}
;${Debug} "We are an already elevated process"
StrCpy $ConsoleParentPID -1
${EndIf}
${Break} ;we are admin (after elevation or not), let the show go on
${EndIf}
${If} $1 = 3 ;RunAs completed successfully, but with a non-admin user
MessageBox mb_YesNo|mb_IconExclamation|mb_TopMost|mb_SetForeground "This ${thing} requires admin privileges, try again" /SD IDNO IDYES uac_tryagain IDNO 0
${EndIf}
;fall-through and die
${Case} 1223
MessageBox mb_IconStop|mb_TopMost|mb_SetForeground "This ${thing} requires admin privileges, aborting!"
Quit
${Case} 1062
MessageBox mb_IconStop|mb_TopMost|mb_SetForeground "Logon service not running, aborting!"
Quit
${Default}
MessageBox mb_IconStop|mb_TopMost|mb_SetForeground "Unable to elevate , error $0"
Quit
${EndSwitch}
${Debug} "End UAC_RunElevated init"
!macroend
Macro invocation (beginning of .onInit):
;abort if not started by administrator
!insertmacro InitElevation "installer"
${If} ${UAC_IsInnerInstance}
${AndIfNot} ${UAC_IsAdmin}
SetErrorLevel 0x666666 ;special return value for outer instance so it knows we did not have admin rights
Quit
${EndIf}

Related

NSIS AccessControl::GrantOnFile permission failing

I am trying to create and set a directory with NSIS and the accessControl plugin like the following:
CreateDirectory "$APPDATA\${productName}"
; create fileResources directory
CreateDirectory "$APPDATA\${productName}\fileResources"
AccessControl::GrantOnFile "$APPDATA\${productName}\fileResources" "Everyone" "FullAccess"
Pop $0 ; get "Marker" or error msg
StrCmp $0 "Marker" Continue
MessageBox MB_OK|MB_ICONSTOP "Error setting access control for $APPDATA\${productName}\fileResources: $0"
Pop $0 ; pop "Marker"
Continue:
Pop $0
I am receiving the following on $0 what is that response?
I want to make a folder readable and writable by the installed program
I'm guessing that you are building a Unicode installer using NSIS v3 and that you put the wrong plugin in the plugins subdirectory, that is why the result looks chinese.
To install a plugin correctly you need to put the ANSI .dll in NSIS\Plugins\x86-ansi and the Unicode .dll in NSIS\Plugins\x86-unicode.

NSIS uninstaller privileges according to installer

I have an NSIS installer for an application, which can be run as normal user. But if the user wants to install into the "Program Files" directory, it can still be accomplished by starting the installer with administrator privileges.
Now I have the problem, that the uninstaller is started with user privileges by default, even if the installation took place as administrator. This causes the uninstallation to silently fail. Even worse: It even states that the uninstall process was successful without being able to delete any files.
My question is: Is it possible to create an uninstaller during the installation, which requires (or better: requests itself with) the same privileges as the installation process?
You would have to implement this check yourself. You can check if you are admin in the installer with the UserInfo plugin and then store the result in a .ini, the registry or append the info to the uninstaller.exe:
InstallDir $temp\instdir
Section
UserInfo::GetAccountType
Pop $0
StrCmp $0 "Admin" 0 +2
StrCpy $0 1
IntOp $0 $0 & 1 ; $0 is now 1 if admin or 0 if not
SetOutPath $InstDir
WriteUninstaller "$InstDir\Uninstall.exe"
FileOpen $1 "$InstDir\Uninstall.exe" a
FileSeek $1 0 END
FileWriteByte $1 $0
FileClose $1
SectionEnd
Section Uninstall
FileOpen $1 "$ExePath" r
FileSeek $1 -1 END
FileReadByte $1 $0
FileClose $1
DetailPrint "Installer was admin: $0"
SectionEnd

How to display progress during a NSIS Installation, when installer calls the uninstaller in silent mode?

I have an NSIS installer which uninstalls the older version through this command
ExecWait "$INSTDIR\temp\uninstall.exe /S _?=$INSTDIR"
The installer is supposed to wait till uninstall of the old version is complete and then proceed with the new installation. The problem is, the user does not see any progress in the installation window for a long time (the uninstaller has to delete some huge directories, so it takes its own sweet time).
We do not want to remove the /S switch as it will pop up the uninstaller window and the user has to do a couple of clicks to proceed with uninstall and finally close the uninstaller.
Is there anyway by which i can show some progress in the installer window while executing the uninstaller in silent mode?
There is no way to get feedback when /S is used. What you could do is to make up your own parameter and tweak the uninstaller:
!include FileFunc.nsh
UninstPage uninstConfirm un.skipifsilentprogress
UninstPage instFiles
Function un.skipifsilentprogress
ClearErrors
${GetParameters} $0
${GetOptions} "$0" "/UIS" $1
${IfNot} ${Errors}
SetAutoClose true ;Make sure user does not have to click close
Abort
${EndIf}
FunctionEnd
Section uninstall
Detailprint uninstalling...
Sleep 555
Sleep 555
Sleep 555
SectionEnd
And run it with the special /UIS switch...

How do you request administrator permissions using NSIS?

I am quite new with NSIS.
I am trying to request administrator permissions in order to run the installer, as it messes around a bit with registries.
My problem with "RequestExecutionLevel" and "MULTIUSER_EXECUTIONLEVEL" is that they both absolutely block any non-Admin user from opening the installer, even when selecting "Run as Administrator" in the context menu.
I have tried using the RunAs DLL, but I have not found a single thread as to what to put in the $command variable passed to "RunAsW" function.
Here is my (pretty hacked-up) code:
StrCpy $0 0
StrCpy $1 ""
System::Call 'RunAs::GetAdministrators(w r1, *i .r0) i .r2 ? u'
System::Alloc 64
Pop $4
StrCpy $4 $2
StrCpy $5 ""
loop:
IntCmp $0 0 endloop
System::Call '*$4(w .r3)'
StrCpy $5 "$5|$3"
endloop:
System::Free $4 ; we free the memory used by the array
StrCpy $5 "$5" "" 1
!insertmacro MUI_INSTALLOPTIONS_WRITE "Settings.ini" "Field 1" "ListItems" $5
!insertmacro MUI_INSTALLOPTIONS_DISPLAY "Settings.ini"
!insertmacro MUI_INSTALLOPTIONS_READ $1 "UserPass" "Field 1" "State"
!insertmacro MUI_INSTALLOPTIONS_READ $2 "Settings.ini" "Field 2" "State"
StrCpy $3 "%%LOGONSERVER%%"
StrCpy $3 0
StrCpy $4 0
System::Call 'RunAs::RunAsW(w r1, w r2, w r3, *w .r4) i .r0 ? u'
MessageBox MB_OK $0
IntCmp $0 1 success
Quit
success:
!insertmacro MUI_LANGDLL_DISPLAY
A lot of it is just guess work and trial and error. (btw - I also tried running through a loop to get all Administrators, but it seems the DLL was intended only for 32-bit machines, so...).
Anyway, my question is:
Does anybody know of a way (using "RunAs" or otherwise) to open a dialog requesting Username and password, check the credentials and continue with the installation only if they check out?
Also, I know there is a way to set up an installer so that it comes with that nice shield icon on it that lets users know that Admin permission will be requested. Does anybody know how to do that?
Any help would be very much appreciated, as this is the only thing currently preventing the deployment of my app.
Outfile RequireAdmin.exe
RequestExecutionLevel admin ;Require admin rights on NT6+ (When UAC is turned on)
!include LogicLib.nsh
Function .onInit
UserInfo::GetAccountType
pop $0
${If} $0 != "admin" ;Require admin rights on NT4+
MessageBox mb_iconstop "Administrator rights required!"
SetErrorLevel 740 ;ERROR_ELEVATION_REQUIRED
Quit
${EndIf}
FunctionEnd
Page InstFiles
Section
SectionEnd
is the basic code I usually recommend to make sure the installer is running as an Administrator.
IMHO it does not make sense to prompt for credentials on a custom page unless only parts of the install process requires administrator access and the other part requires access to the users profile. If this applies to you then you should take a look at the UAC plug-in (It is a bit complicated to use and makes it impossible for your exe file to get the shield overlay icon)
I don't think the RunAs plug-in works correctly on Vista+ when UAC is on so trying to get it to work might be a dead end...
The recommended way to get the shield is to request elevation in the exe manifest, RequestExecutionLevel admin does that. If you don't use RequestExecutionLevel at all in your script your installer might be detected as a legacy installer and it will also get the shield overlay.
In Windows Vista, if an executable file requires elevation to launch,
then the executable's icon should be "stamped" with a shield icon to
indicate this fact. The executable's application manifest must mark
"requireAdministrator" to designate the executable as requiring a full
administrative access token. The shield icon overlay will also be
automatically placed on executables that are deemed to require
elevation as per the installer detection heuristics. For example, a
file named setup.exe will automatically receive a shield icon overlay
even if the executable does not have an embedded application manifest.

Checking if the application is running in NSIS before uninstalling

I am new to NSIS, and I need to know that in the uninstaller, how I can check if the application (which is in C++) is running and close it before uninstalling.
Here is a slightly more friendly version for using NSProcess that requests the app to close rather than terminates it (Owen's answer)
${nsProcess::FindProcess} "${APP_EXE}" $R0
${If} $R0 == 0
DetailPrint "${AppName} is running. Closing it down"
${nsProcess::CloseProcess} "${APP_EXE}" $R0
DetailPrint "Waiting for ${AppName} to close"
Sleep 2000
${Else}
DetailPrint "${APP_EXE} was not found to be running"
${EndIf}
${nsProcess::Unload}
Use the NsProcess plugin. Download it here -> NSProcess
How to use it? As simple as:
${nsProcess::KillProcess} "${APP_EXE}" $R4
where APP_EXE is the name of your application...
The download will also tell you how to use it... :)
Depending on the application, you have a couple of choices:
If your application has a window with a somewhat unique class name, you could use FindWindow
If your application creates a named kernel object (Mutex etc) you can check for it by calling the correct native win32 API with the system plugin
Use a 3rd party plugin like FindProcDLL
Just make sure that the first thing that install or un-install does is to delete all xyz.tmp files in %TEMP (or any other app writable directory) before the below for loop runs. No plugins required.
!macro IsRunning
ExecWait "cmd /c for /f $\"tokens=1,2$\" %i in ('tasklist') do (if /i %i EQU xyz.exe fsutil file createnew $TEMP\xyz.tmp 0)"
IfFileExists $TEMP\xyz.tmp 0 notRunning
;we have atleast one main window active
MessageBox MB_OK|MB_ICONEXCLAMATION "XYZ is running. Please close all instances and retry." /SD IDOK
Abort
notRunning:
!macroEnd

Resources