Archive: Uninstall


Uninstall
  Hello

After the installation, it is possible that some folders are added in the Directory from the user where I installed my files. So i don't know all the files and folders which i've to delete with my uninstaller. Additionaly I unzipp some folder, so there ara a lot of files after the Installation.

I'm using the Modern UI. At the moment my uninstaller looks like:
RMDir /r $INSTDIR\Installation
RMDir /r $INSTDIR\play
RMDir /r $INSTDIR\myfolder

I know it is a bit dangerous. I searched a while and only found the "Uninstall only installed files" unlist.nsi. But this function only create a list during the installation. And i'm looking for creating a list during uninstallation, while some files and order are added after my installation.

Does it already exist a function which search every file and folder in the installed directory and delete each file?
Maybe I missed a simple plug-in or something similar?

Thanks and Regards,
Wili


Originally posted by wili
Does it already exist a function which search every file and folder in the installed directory and delete each file?
Think again please. "Make a list of all files and folders, and then delete the items on that list", is EXACTLY the same as "Delete all files and folders".

If you don't know what you want to delete, Delete /r is the only possible solution. This probably means that you need to change your application, so that you DO know what you want to delete.

Hi

Thanks for your answer.
Does theire exist a function which generate a list of all files and folders during the Uninstall Section?

Thanks and Regards,
Marco


What I would do is write a small program (it could be a silent NSIS executable) and run it at compile time using !system. It will simply write the NSIS File instructions to one file (which you !include in your install section(s)) and the NSIS Delete instructions to another file (which you !include in your uninstall section(s)). If you write the executable in NSIS, you can use Locate (look in the manual). Job done.

Stu


Thanks a lot for your hints.

I'll change my uninstaller Ideas.
Last Question, is it possible to figure when unistaller.exe is launched, on with path it was stored?

The $EXEPATH, etc. shows the path where it is executed.
But I'm looking to read out, where the uninstaller was store. Is theire a simple way?

Thanks and regards,
Marco


In the uninstaller, $INSTDIR contains the directory of uninstall.exe. (Note: This is also why it's so dangerous to do RmDir /r $INSTDIR, because someone might have moved uninstall.exe to Program Files!)

You can store the actual installation directory in an ini file or the registry, during installation. (You can also use that to check if uninstall.exe has been moved, before you start the uninstallation.)


Thanks a lot.
So i'll check if the uninstaller.exe is still at the same folder (with filexists...).

Regards,
Marco


What would you want to do with FileExists? o_O

ReadRegStr $0 HKLM YourRegKey YourRegString
${If} $0 != $INSTDIR
MessageBox MB_OK "Warning here"
${EndIf}


I have realised the danger of RMDir /r "$INSTDIR", even if it is a really small possibility that a user move the Uninstall.exe to another directory...

so I thought about a solution for my script and just replace the $INSTDIR with value from the registry. My questions: Why does this not work? (the installdir is untouched after uninstall is finished - nothing is deleted! but the key is there and is been showed "MessageBox MB_OK "echo - $0")
and: is there another universal solution?

example:

Section-End

WriteUninstaller "$INSTDIR\Uninstall.exe"
>WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\myprogram" "UninstallString" '"$INSTDIR\Uninstall.exe"'
>WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\myprogram" "DisplayIcon" '"$INSTDIR\myprogram.exe"'
>WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\myprogram" "InstallPath" '"$INSTDIR"'
>SectionEnd


Section "Uninstall"
>.
>ReadRegStr $0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\myprogram" "InstallPath"
>RMDir /r $0
>.
>SectionEnd
>
bye

If you checked the value of $0 you'd know. You've put quotes around the value in the registry. Also your script is just as bad as using $INSTDIR. If someone deletes that registry value, RMDIR will be given an empty string, i.e. the root and delete EVERYTHING. Please either avoid RMDir completely or do at least some validation on the path you are giving it.

Stu


hi

-but RMDir /r "$INSTDIR" have also quotes and it works,... the $0 includes the "" so i removed them in the script -> RMDir /r $0

The error flag will be set and $x will be set to an empty string ("") if the string is not present.
RMDir /r "" -> doesn NOT delete ROOT or anything else. I tested it right now on the VM. =)

I think, the best way will be to check that the last dir is ..."/MyProgram" to make sure the User didnt use "C:\" or "C:\Program Files" as installation directory!? but how to do it?

btw. That some one deletes the Reg entry is very unlikely, IMHO

--------------
I have testet your Example: http://nsis.sourceforge.net/Uninstal...nstalled_files
But it does not work. I guess it is because of * or *.*
This is what i use in my script:


        Section "Main Files" test32


SectionIn RO
SetOutPath $INSTDIR
File/r "${BIN}\*"


${If} ${RunningX64}
File /r "${BIN64}\*.*"
${EndIf}


${If} ${
IsWin2000}
File /r "${BINWin2k}\*.*"
${EndIf}
SectionEnd
>

when i try to use your script, the compiller breaks with:
->$UninstLog
Usage: File [/nonfatal] [/a] ([/r] [/x filespec [...]] filespec [...] |
/oname=outfile one_file_only)
Error in macro File on macroline 3
Error in script "stdin" on line 270 -- aborting creation process
I implement it exactly as you described.
The line of error:
${File} /r "${BIN}\*"

bye

Originally posted by Smurge
btw. That some one deletes the Reg entry is very unlikely, IMHO
The regentry not existing is actually far more likely than a user moving uninstall.exe. And the consequences if it happens are far far worse. So summing up, your method is extremely dangerous. (Example: User installs to D:\Software\YourApp, then formats C: and reinstalls windows, then runs your uninstaller. As you can see, this could happen very easily.)


Here's what I do:

  ;Check validity of the $INSTDIR variable

${If} $ALLUSERS == 1
ReadRegStr$2 HKLM "Software\${REGKEY_GROUP}\${REGKEY_NAME}" "InstallDir"
${Else}
ReadRegStr $2 HKCU "Software\${REGKEY_GROUP}\${REGKEY_NAME}" "InstallDir"
${EndIf}
${If} $
2 != $INSTDIR
;Something is wrong! Fall back to checking for filenames.
${IfNot} ${FileExists} "$INSTDIR\MyApp.exe"
${OrIfNot} ${FileExists} "$INSTDIR\MyOtherFile.dat"
${OrIfNot} ${FileExists} "$INSTDIR\Etcetera.etc"
MessageBox MB_YESNO|MB_ICONEXCLAMATION "WARNING: Setup could not verify that this uninstaller is being run from its proper \
directory. If you have moved Uninstall.exe to a directory other than the actual installation folder, it is \
impossible to guarantee that the uninstallation will be executed properly. If you proceed, it is theoretically \
possible that setup will delete files that should not be deleted. Are you sure you wish to continue with \
the uninstallation?"IDYES +2
quit;quit installer.
${EndIf}
${EndIf}
This is still not waterproof safe. But it's pretty safe as long as the filenames in my software (MyApp.exe and the others) are very specific and unlikely to be used in other software.

And together with the above code, I NEVER use RmDir /r, no matter how badly I want to. I only delete the files I know I should delete. Nothing else. It's just a matter of courtesy to your users that you don't take frivolous risks with their computers. Even if it means leaving some files behind after uninstall, there's just no excuse to risk destroying someone's OS.

This doesn't cover the case when the user installed the app in C:\ ... :D
hmm, so ideally the installer should delete 1 file at the time, and delete directory's only if the dir is empty?

Thanks for the example. I will use it in my script. But what i don't understand is the "$ALLUSERS == 1" thing.
I didn't find this Variable in the manual. Is this automatically detected when "SetShellVarContext all" is set previously? or do i need a "!include ..." to use this?

bye


-but RMDir /r "$INSTDIR" have also quotes and it works,... the $0 includes the "" so i removed them in the script -> RMDir /r $0
That is the same as doing RMDir /r '"some_path"'. RMDir does not accept quotes around its path.
RMDir /r "" -> doesn NOT delete ROOT or anything else. I tested it right now on the VM. =)
Looks like code was added quite a while ago to prevent this. My bad.
I think, the best way will be to check that the last dir is ..."/MyProgram" to make sure the User didnt use "C:\" or "C:\Program Files" as installation directory!? but how to do it?
Use StrCpy $0 $path "" -X to get the last X characters of the path.
when i try to use your script, the compiller breaks with:

I implement it exactly as you described.
The line of error:
${File} /r "${BIN}\*"
You cannot use the recursive switch as the macro requires you to explicitly tell it each file name/path. If you need to use these macros but with recursion then you need to write a program that generates the NSIS script (i.e. the individual ${File} instructions for each file).

Stu

Ignore that $ALLUSERS bit. Uninstall registry is always under HKLM.

Stu


That is the same as doing RMDir /r '"some_path"'. RMDir does not accept quotes around its path.
I just assumed that the Path have to be in "". I dint know that it needed ' to function properly. All examples i read, had "" with RMDir! so what is the problem with it?

The manual don't show any necessary format:
http://nsis.sourceforge.net/Docs/Chapter4.html#4.9.1.8

--------
Originally posted by Afrow UK
If you need to use these macros but with recursion then you need to write a program that generates the NSIS script (i.e. the individual ${File} instructions for each file).
This is too complex for me. I don't have any professional programming skills.

Cant i just use the ${File} ..\* for the app and any sub directory? will this delete/uninstall those sub dirs as well?

------

by ignore you mean to skip the IF? like this:

    ReadRegStr$2 HKLM "Software\${REGKEY_GROUP}\${REGKEY_NAME}" "InstallDir"

${If} $2 != $INSTDIR
;Something is wrong! Fall back to checking for filenames.
${
IfNot} ${FileExists} "$INSTDIR\MyApp.exe"
${OrIfNot} ${FileExists} "$INSTDIR\MyOtherFile.dat"
${OrIfNot} ${FileExists} "$INSTDIR\Etcetera.etc"
MessageBox MB_YESNO|MB_ICONEXCLAMATION "WARNING: Setup could not verify that this uninstaller is being run from its proper \
directory. If you have moved Uninstall.exe to a directory other than the actual installation folder, it is \
impossible to guarantee that the uninstallation will be executed properly. If you proceed, it is theoretically \
possible that setup will delete files that should not be deleted. Are you sure you wish to continue with \
the uninstallation?"IDYES +2
quit;quit installer.
${EndIf}
${EndIf}

Cant i just use the ${File} ..\* for the app and any sub directory? will this delete/uninstall those sub dirs as well?
No you can't I'm afraid. If you really want to only uninstall installed files you will have to explicitly include File instructions for each file as a bare minimum. That script will generate a text file listing the files and folders to later delete during uninstall, saving you from having to write those Delete instructions yourself. In my opinion though, it is better to explicitly write the Delete instructions in the uninstaller too, rather than rely on an external text file. If the text file goes missing then you get problems, yet again.
by ignore you mean to skip the IF? like this:
That is correct.

Stu

Also remember the uninstall code needs to delete in reverse order to the order that the files were extracted/created. E.g.

Section Install
SetOutPath 1
File a
SetOutPath 1\2
File b
SetOutPath 1\2\3
File c
SectionEnd
Section Uninstall
Delete 1\2\3\c
RMDir 1\2\3
Delete 1\2\b
RMDir 1\2
Delete 1\a
RMDir 1
SectionEnd
Stu

Originally posted by Smurge
This doesn't cover the case when the user installed the app in C:\ ... :D
Actually I disallow root-directory installations, as well as installs to $PROGRAMFILES, $WINDIR etc, on my DIRECTORY page. But you're very right, I should add a check for these directories in the uninstaller as well, in case some joker moves uninstall.exe to one of those directories. :-)

Originally posted by Smurge
hmm, so ideally the installer should delete 1 file at the time, and delete directory's only if the dir is empty?
Yeah, that's the only safe way to do it.

Originally posted by Smurge
I just assumed that the Path have to be in "". I dint know that it needed ' to function properly. All examples i read, had "" with RMDir! so what is the problem with it?
The *parameter* of RmDir should be without quotes. The *command call* to RmDir may have quotes, because those quotes are for the compiler. If you do this: RmDir "$INSTDIR\something", then the compiler removes those quotes for you. But if you do RmDir $0, and $0 contains quotes itself, then the compiler cannot prevent the quote characters from ending up in the RmDir parameter. That's why you need to strip quote characters after reading a path string from registry (if there's a chance that the string contains quotes).

Originally posted by Afrow UK
Uninstall registry is always under HKLM.
Single-user installations' ARP info is stored in HKCU, actually.

Single-user installations' ARP info is stored in HKCU, actually.
That's interesting. There isn't even an Uninstall key under HKCU but if I create one and add an entry to does work. I wonder why that isn't used more often.

Edit: And for all this time I've been avoiding add/remove programs and using an Uninstall short-cut only lol. Thanks :)

Stu

so back to my question: what do i need to use the "$ALLUSERS == 1"
Your example is working in my installer, when i move the deinstaller to another location the MassageBox pops out.
There is only a tiny flaw... the compiler shows a warning:

1 warning:
unknown variable/constant "ALLUSERS" detected, ignoring (macro:_==:1)
-------
btw. the '"$INSTDIR\myprogram.exe"' only happened because i copy-pasted this line from another script. I removed the quotes from all WriteRegStr and now its working perfectly!

thanks

MSG was just giving you an idea to use that variable to indicate an all users / current user install. Doesn't mean you need to use it.

Stu


I have modified the example a little bit. This is what i use:

Section-End

>.
>WriteUninstaller "$INSTDIR\Uninstall.exe"
>WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\myprogram" "InstallPath" '$INSTDIR'
>.
>SectionEnd


Section "Uninstall"
>.
; # Check validity of the $INSTDIR variable. Compares the uninstall.exe location with the RegKey value.
>ReadRegStr $2 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\myprogram" "InstallPath"
>IfErrors 0 +2
ReadRegStr$2 HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\myprogram" "InstallPath"
>${If} $2 != $INSTDIR
; # Something is wrong! Fall back to checking for filenames.
${IfNot} ${FileExists} "$INSTDIR\MyProgram.exe"
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "WARNING: Setup could not verify that this uninstaller is being run from its proper \
directory. If you have moved Uninstall.exe to a directory other than the actual installation folder, it is \
impossible to guarantee that the uninstallation will be executed properly. If you proceed, it is theoretically \
possible that setup will delete files that should not be deleted. Are you sure you wish to continue with \
the uninstallation?$\r$\r-OK to continue and uninstall the application from:$\r$\n '$2' $\r$\n-CANCEL to end"IDOK +2
quit; # quit installer.
${EndIf}
${EndIf}
.
>RMDir /r $2
>.
>SectionEnd
>

I think, together with the Path saved in the RegKey and the user feedback, it is a good compromise.

I would add a ClearErrors above ReadRegStr just in case you later add code which may set the error flag. Also, should that not be an OR (you are implying an AND by using two If statements. I.e. it should be If $2 != $INSTDIR OR !FileExists $INSTDIR\MyProgram.exe). With your code, if someone moves the uninstall executable to another folder with a MyProgram.exe in it (unlikely I know), then the uninstall will continue.

Stu


Stu: That depends on what you decide is more important. If my regentry is there and the value matches $instdir, then I don't check which files are there. This is in order to support rollback in case of installation failure (the regkey is created first thing during installation). But yeah, Or might be the better option generally speaking.


Thanks for the tip, i extended the script with the ClearErrors, changed it to an OR case and added a check for the RegKey existence in "un.onInit". But i dont get the result i expected.

1. Question: Why is ${Usedinstpath} defined, even if it is "jumped" by a GOTO? (in the un.onInit part)
2. Question: Why does the Uninstaller not delete the directory that is defined in $2 and ${Usedinstpath}? (the App Directory remains untouched after successful uninstallation)



onInit

; # Check existence of the RegKey. If not there (in case of an error or windows reinstallation) then try to to use the current Installer location as path
ClearErrors
ReadRegStr$1 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Hybrid" "InstallPath"
IfErrors 0 +2
ReadRegStr$1 HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Hybrid" "InstallPath"
IfErrors "" goout
; # Ask to use path or else abort
!ifndef $1
MessageBox MB_YESNO
|MB_ICONQUESTION "Cant determine the install path from registry. Shall the installer use the current location of the installer?$\r$\n Installer path? -> '$INSTDIR'" IDYES definepath
quit
definepath:
!define Usedinstpath "$INSTDIR"
!endif
>goout:

;INFO
MessageBox MB_OK "1-s1-$1$\r$\ninstdir-$INSTDIR$\r$\nuserinstpath-${Usedinstpath}"

>FunctionEnd


Section "Uninstall"

SetShellVarContext all
; # Check validity of the $INSTDIR variable. Compares the uninstall.exe location with the RegKey value.
ClearErrors
ReadRegStr$2 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Hybrid" "InstallPath"
IfErrors 0 +2
ReadRegStr$2 HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Hybrid" "InstallPath"
;INFO
MessageBox MB_OK "2-s2-$2$\r$\ninstdir-$INSTDIR$\r$\nuserinstpath-${Usedinstpath}"

${If} $2 != $INSTDIR
GOTO cantfinddirwarning
${EndIf}
${IfNot} ${FileExists} "$INSTDIR\Hybrid.exe"
GOTO cantfinddirwarning
${EndIf}
GOTO startuninstall

cantfinddirwarning:
; # Something is wrong! Fall back to checking for filenames.
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "WARNING: Setup could not verify that this uninstaller is being run from its proper \
directory. If you have moved Uninstall.exe to a directory other than the actual installation folder, it is \
impossible to guarantee that the uninstallation will be executed properly. If you proceed, it is theoretically \
possible that setup will delete files that should not be deleted. Are you sure you wish to continue with \
the uninstallation?$\r$\r-OK to continue and uninstall the application from:$\r$\n '$2${Usedinstpath}' $\r$\n-CANCEL to end"IDOK +2
quit; # quit installer.


startuninstall:
!ifdef $2
RMDir/r $2
!endif
!ifdef ${Usedinstpath}
RMDir /r ${Usedinstpath}
!endif
thx

Originally posted by Smurge
1. Question: Why is ${Usedinstpath} defined, even if it is "jumped" by a GOTO? (in the un.onInit part)
Because !defines are executed at compiletime, whereas gotos are runtime commands. You'll need to learn the difference between compiletime and runtime. Google may have some explanations.

Originally posted by Smurge
2. Question: Why does the Uninstaller not delete the directory that is defined in $2 and ${Usedinstpath}?
Because "$2" is not a define, so "!ifdef $2" is always false and the compiler completely skips the RmDir command. You need to use ${If} $2 != "" . Once again, you're confusing compiletime with runtime.