Checking for Registry in NSIS - nsis

I'm using NSIS to make an executable for a project I'm doing in Visual C++ 2012 Express. Because I'm a beginner, I started with the Setup Wizard in HM NIS Edit.
I'm trying to bundle the Visual C++ 2012 Redistributable with my program, but every time the installer is re-ran, the Redistributable pops up with a "Repair" & "Remove" option, and that looks annoying.
So I decided to write a little bit of NSIS script, and this is the start of the script so far:
Var STR
Section CheckForReg
ClearErrors
ReadRegDWORD $0 HKLM "SOFTWARE\Classes\Installer\Dependencies\{8e70e4e1-06d7-470b-9f74-a51bef21088e}" "Version"
ifErrors 0 Blank
StrCpy $STR "$INSTDIR\vcredist_x86.exe"
GoTo End
Blank:
StrCpy $STR ""
End:
SectionEnd
This piece is called at the very start of the script, and the global variable STR is applied to:
!define MUI_FINISHPAGE_RUN $STR
Shortly after.
Obviously this is a really silly way to do it, but I don't need too much out of it.
The issue is that the CheckForReg always thinks the registry doesn't exist, and doesn't move to the label Blank. As a note, I'm manually checking the registry every time, and the registry entry looks like:
[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Installer\Dependencies\{8e70e4e1-06d7-470b-9f74-a51bef21088e}]
"Version"="11.0.51106.1"
"DisplayName"="Microsoft Visual C++ 2012 Redistributable (x86) - 11.0.51106"
So the question is: Where am I going wrong with this? It looks very simple, but obviously I have something backwards.
-- Removed Code Dump

You can only read DWORDs with ReadRegDWORD!
!include LogicLib.nsh ; So we don't have to use all these labels
StrCpy $STR ""
ReadRegStr $0 HKLM "SOFTWARE\Classes\Installer\Dependencies\{8e70e4e1-06d7-470b-9f74-a51bef21088e}" "Version"
${If} $0 == ""
StrCpy $STR "$INSTDIR\vcredist_x86.exe"
${EndIf}

Related

NSIS File Associations change in Windows 7

I have a requirement to (force) change the file association for a particular file type (extenison ".theext" ) to open with "myapp.exe" when installing an app using NSIS.
I've read a few suggestions of how to achieve this, so currentky this is what I have in my NSIS script:
DeleteRegKey HKCR ".theext"
DeleteRegKey HKLM ".theext"
DeleteRegKey HKCU ".theext"
WriteRegStr HKCR ".theext" "" "theextfile"
WriteRegStr HKCR "theextfile" "" "My App Document"
WriteRegStr HKCR "theextfile\DefaultIcon" "" "$INSTDIR\${EXENAME}.exe,0"
WriteRegStr HKCR "theextfile\shell\open\command" "" '"$INSTDIR\${EXENAME}.exe" "%1"'
WriteRegStr HKCR "theextfile\shell\print\command" "" '"$INSTDIR\${EXENAME}.exe" /p "%1"'
WriteRegStr HKLM "Software\RegisteredApplications" "${EXENAME}" "$INSTDIR\${EXENAME}.exe"
WriteRegStr HKCU "Software\RegisteredApplications" "${EXENAME}" "$INSTDIR\${EXENAME}.exe"
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.theext\OpenWithList" "a" "$INSTDIR\${EXENAME}.exe"
To test this, I set the file assoc using WIndows 7 Explorer to GVIM.exe.
Since doing this, every time I run the installer, Windows 7 still opens the file on double click using GVim, and not "MyApp.exe".
But when I check the file associatin as follows, all seems fine:
ftype | findstr /i theext
Gives:
theextfile="C:\Program File (x86)\My App\myapp.exe" "%1"
You are doing everything you are supposed to do and if the extension is not already registered by somebody else you will become the default. You are not really supposed to delete the old keys first though (it can screw up the system but it will never help you become default). Forcing something is evil, the user is supposed to be in control.
Because people forced this in the past Microsoft starting making it harder to change the default. The undocumented FileExts key stores the users chosen default in the UserChoice sub-keys. In newer versions of Windows (8+?) the default is verified with some secret hash so you cannot override it.
The IApplicationAssociationRegistration interface does not work in newer version of Windows but it might work in Windows 7:
!include Win\COM.nsh
!include WinCore.nsh
!insertmacro ComHlpr_CreateInProcInstance ${CLSID_ApplicationAssociationRegistration} ${IID_IApplicationAssociationRegistration} r0 ""
${If} $0 P<> 0
${IApplicationAssociationRegistration::SetAppAsDefault} $0 '("MyApp", ".myext", ${AT_FILEEXTENSION})'
${IUnknown::Release} $0 ""
${EndIf}
In Windows 8 all you can do is launch the generic UI:
!include Win\COM.nsh
!insertmacro ComHlpr_CreateInProcInstance ${CLSID_ApplicationAssociationRegistrationUI} ${IID_IApplicationAssociationRegistrationUI} r0 ""
${If} $0 P<> 0
${IApplicationAssociationRegistrationUI::LaunchAdvancedAssociationUI} $0 '("Wordpad")' ; Replace with your name from the RegisteredApplications key
${IUnknown::Release} $0 ""
${EndIf}
In Windows 10 even this is gone, it will just display a toast telling the user to change their settings if you call LaunchAdvancedAssociationUI.
ftype does not know the true default, the default is only known when Windows actually runs the association code in the shell. IApplicationAssociationRegistration::QueryCurrentDefault is better at guessing the default but even it can fail if the default is actually a COM shell extension that overrides the default.

NSIS roll-back installer if ExecWait command gets a specific return code

I'm currently using the following script to install drivers along with my application:
!macro customInstall
ExecWait '"$INSTDIR\resources\DPInst.exe" /sw'
!macroend
However, if DPInst returns >= 0x80010000, this means one or more of the driver installs has failed so I need to roll-back the installation and quit. Any idea how I would do this?
ExecWait can store the process exit code in the 2nd parameter. Not much you can do to roll it back, it is best just to do it early in the install phase:
!include LogicLib.nsh
Section
SetOutPath "$instdir\resources"
File "whatever\DPInst.exe"
ExecWait '"$INSTDIR\resources\DPInst.exe" /sw' $0
${If} $0 U>= 0x80010000
Delete "$INSTDIR\resources\DPInst.exe"
RMDir $instdir\resources
RMDir $instdir
MessageBox mb_iconstop "Error blah blah"
Abort
${EndIf}
SectionEnd

Signing NSIS Uninstaller from Linux or Mac

I am porting our NSI installer to Linux and Mac instead of Windows to better integrate with our Maven build system.
We need to sign our installer and uninstaller. This was done as suggested at http://nsis.sourceforge.net/Signing_an_Uninstaller, but I just realized that it tries to run the tempinstaller to force it to produce the uninstaller.exe which can then be signed.
Obviously this trick doesn't work too well on *Nix systems and make this part of the process non-portable.
Does anyone has a better solution. I'm no expert at NSIS and wondering if there is a clever way to get the uninstall.exe so that it can be signed?
I don't think there is a real solution to this.
The installer and uninstaller uses the same exe code and only checks a flag (FH_FLAGS_UNINSTALL in firstheader) on startup to see if it is a uninstaller. Just flipping this bit is not enough though, the program would fail the CRC check and even if you bypass that the uninstaller data is compressed so you would have to decompress that to the correct location in the file. To actually accomplish this you would have to write a custom tool. You can see this operation in the NSIS source in exec.c if you search for EW_WRITEUNINSTALLER.
We need to sign our installer and uninstaller. This was done as suggested at http://nsis.sourceforge.net/Signing_an_Uninstaller, but I just realized that it tries to run the tempinstaller to force it to produce the uninstaller.exe which can then be signed. [...] this trick doesn't work too well on *Nix systems and make this part of the process non-portable.
If you exploit a stub installer for uninstall operations (no payload), this appears to be possible.
It will spawn an uninstall.exe process from the $TEMP folder, which is then capable of deleting $INSTDIR.
This script will create a stub (un)installer which can then be Authenticode Signed. It will compile on Windows, MacOS and Linux.
Caveats:
You'll have to manually bundle this into the installer (trivial)
You'll have to manage your own uninstall registry entries (trivial)
The look and feel may not match NSIS's default for uninstallers
You'll see the installer open twice (first from $INSTDIR, second from $TEMP). This is a child process which allows uninstall.exe to delete itself, similar to how NSIS does it in the Section "Uninstall".
You'll need a secondary .nsi script dedicated to uninstall operations, cumbersome if you have a lot of shared logic between your install/uninstall sections.
Worse, you'll have to AVOID the "Uninstall" section title, as you'll be placed into the same problem as the OP when that bytecode is generated.
When explicitly running from $TEMP some relative file logic will be incorrect. The example passes these back in as a $DELETE_DIR, $DELETE_EXE respectively.
The code:
!include MUI2.nsh
!include x64.nsh
!include LogicLib.nsh
!include FileFunc.nsh
!include WinMessages.nsh
!define MUI_PRODUCT "My App"
!define MUI_VERSION "1.0.0"
; Masquerade the title
!define MUI_PAGE_HEADER_TEXT "Uninstall My App"
!define MUI_PAGE_HEADER_SUBTEXT "Remove My App from your computer"
!define MUI_INSTFILESPAGE_FINISHHEADER_TEXT "Uninstallation Complete"
!define MUI_INSTFILESPAGE_FINISHHEADER_SUBTEXT "Uninstall was completed successfully."
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_LANGUAGE "English"
!insertmacro GetParameters
RequestExecutionLevel admin
CRCCheck On
OutFile "uninstall.exe"
Name "Uninstall"
Var /GLOBAL RESPAWN
Var /GLOBAL DELETE_DIR
Var /GLOBAL DELETE_EXE
Section
; Masquerade as uninstall
SendMessage $HWNDPARENT ${WM_SETTEXT} 0 "STR:Uninstall"
${GetParameters} $0
${GetOptions} "$0" "/RESPAWN=" $RESPAWN
${GetOptions} "$0" "/DELETE_DIR=" $DELETE_DIR
${GetOptions} "$0" "/DELETE_EXE=" $DELETE_EXE
${If} $RESPAWN != ""
; We're running from $TEMP; Perform the uninstall
!define yay "We're running from $EXEPATH, yay, we can remove the install directory!$\n$\n"
!define myvars "$\tRESPAWN$\t$RESPAWN$\n$\tDELETE_EXE$\t$DELETE_EXE$\n$\tDELETE_DIR$\t$DELETE_DIR"
MessageBox MB_OK "${yay}${myvars}"
; Your uninstall code goes here
; RMDir /r $DELETE_DIR\*.*
; Delete "$DESKTOP\${MUI_PRODUCT}.lnk"
; Delete "$SMPROGRAMS\${MUI_PRODUCT}\*.*"
; RmDir "$SMPROGRAMS\${MUI_PRODUCT}"
; Delete Uninstaller And Unistall Registry Entries
; DeleteRegKey HKEY_LOCAL_MACHINE "SOFTWARE\${MUI_PRODUCT}"
; DeleteRegKey HKEY_LOCAL_MACHINE "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${MUI_PRODUCT}"
; Remove the old version of ourself
ClearErrors
Delete $DELETE_EXE
IfErrors 0 +3
MessageBox MB_OK "File could NOT be deleted: $DELETE_EXE"
Goto +2
MessageBox MB_OK "File was successfully deleted: $DELETE_EXE"
; Remove ourself from $TEMP after reboot
Delete /REBOOTOK $EXEPATH
; ${If} ${RunningX64}
; ${EnableX64FSRedirection}
; ${EndIf}
SetDetailsPrint textonly
DetailPrint "Completed"
${Else}
; We're NOT running from $TEMP, copy to temp and respawn ourself
GetTempFileName $0
CopyFiles "$EXEPATH" "$0"
Exec '"$0" /RESPAWN=1 /DELETE_DIR="$EXEDIR" /DELETE_EXE="$EXEPATH"'
Quit
${EndIf}
SectionEnd
Function .onInit
; ${If} ${RunningX64}
; SetRegView 64
; ${DisableX64FSRedirection}
; ${EndIf}
FunctionEnd

Can i detect 32 or 64 bit OS without plugin somehow?

now i use the x64.nsh for this, but i can detect it without this plugin?
${If} ${RunningX64}
MessageBox MB_OK "running on 64 bit"
File /r ${64BIT_OPENVPN_INSTALL}
Execwait ${64BIT_OPENVPN_INSTALL}
${Else}
MessageBox MB_OK "running on 32 bit"
File /r ${32BIT_OPENVPN_INSTALL}
Execwait ${32BIT_OPENVPN_INSTALL}
${EndIf}
x64.nsh does not implies specific external plugin usage (apart the system plugin): it is just an included file that defines 3 macros based on kernel calls (i.e kernel32::GetCurrentProcess() and kernel32::IsWow64Process()) through the system plugin, that can be conveniently used with LogicLib.nsh
There are probably many ways to detect the native bitness by just looking at files and registry keys but there is always the risk that some 32-bit systems have somehow ended up with a SysWOW64 folder in %WinDir% etc.
The SetRegView test should be pretty safe but there is a small window where some other app could change the registry at just the wrong time giving you the wrong result.
The correct way to detect this is of course to call the IsWow64Process function and the x64.nsh header already does that for you.
!include LogicLib.nsh
Section
!if "${NSIS_PTR_SIZE}" > 4
DetailPrint "64-bit NSIS, this must be a 64-bit system"
!endif
${If} ${FileExists} "$WinDir\SysWOW64\kernel32.dll"
DetailPrint "Probably not a native 32-bit system"
${EndIf}
${If} ${FileExists} "$WinDir\SysNative\kernel32.dll"
DetailPrint "Probably a 32-bit app on a native 64-bit system (Vista+ only)"
${EndIf}
SetRegView 64
ReadRegStr $6 HKLM "Software\Microsoft\Windows\CurrentVersion" "ProgramFilesDir"
SetRegView lastused
SetRegView 32
ReadRegStr $3 HKLM "Software\Microsoft\Windows\CurrentVersion" "ProgramFilesDir"
SetRegView lastused
${If} $3 != $6
DetailPrint "Probably a 32-bit app on a native 64-bit system"
${EndIf}
; ReadEnvStr on ProgramW6432 or PROCESSOR_ARCHITEW6432 etc
SectionEnd

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.

Resources