Making an MSI that doesn’t need a UAC/LUA prompt


The goal


I think that most things don’t need to require a UAC prompt to install – just install it for that user.  Why not make the MSI so it doesn’t prompt and your users get a smoother experience?  (Also, I feel much better installing a program that doesn’t require elevation to install – at a minimum I know it’s not disabling my anti-malware software.)  Ideally, with that same package you could optionally install per-machine (which requires elevation).  Here’s some information on how to make it happen…


Background


I was recently asked to make an MSI for an extremely minimal replacement of the Run As menu item that was removed in Vista (by calling runas.exe).  Although I doubt we would ever ship something like that I decided to make an installation that didn’t elevate, or that could elevate and install per-machine.  It was an interesting experience, although I didn’t entirely get the behavior I was going for – there are some limitations that make it impossible to do with a MSI in Windows Vista, but still possible in a single download.  (More on that later).


The technical details


All MSIs elevate on Windows Vista by default.  However there is a flag that you can set that will tell MSI that you don’t need to elevate.  The 3rd bit of the word count summary property can be set by using the msiinfo tool (comes with the platform SDK).  Since I also used an embedded cab, that means my word count summary property should be set to 10 (8 | 2 == 10).



msiinfo RunAsNewUser.msi -w 10


You need to have the feature include both the per-user and per-machine information if you’re trying to put both in one package like I did (not a best practice – more on that later). 

    <Feature Id=RunAsNewUser Title=Run as New User Level=1>
<
ComponentRef Id=pm.runasnewuser.cmd
/>
<
ComponentRef Id=pm.runasnewuser.reg.exe
/>
<
ComponentRef Id=pm.runasnewuser.reg.msc
/>
<
ComponentRef Id=pu.runasnewuser.cmd
/>
<
ComponentRef Id=pu.runasnewuser.reg.exe
/>
<
ComponentRef Id=pu.runasnewuser.reg.msc
/>
</
Feature
>

Your standard MSI that installs to the system directories doesn’t face the (fairly simple) issues involved.  Here’s the per-machine version of my components (you need two versions of the components in order to put them in different directories(*). [I build all my MSIs using Wix – it gives me fine-grained control for making really solid MSIs and it tends to match the way I want to think about MSIs.]

      <!– We need a per-machine version as well, with seperate components –>
<
Directory Id=ProgramFilesFolder Name=AdTools
>
<
Directory Id=pm.Microsoft Name=Microsoft
>
<
Directory Id=pm.Runas Name=RunAs
>
<
Component Id=pm.runasnewuser.cmd Guid=9D448C8B-AF67-423B-9622-D9720770B61E
>
<
File Id=pm.runasnewuser.cmd Name=runasnewuser.cmd DiskId=1 Source=runasnewuser.cmd KeyPath=yes
/>
<
Condition>ALLUSERS</Condition
>
</
Component
>
<
Component Id=pm.runasnewuser.reg.exe Guid=98A7DE3E-EF1D-434F-80CB-2F878CD0E9F5
>
<
RegistryKey Id=pm.runasnewuser.reg.exe Key=SystemFileAssociations\.exe\shell\Run as new user…\command Root=HKCR Action=createAndRemoveOnUninstall
>
<
RegistryValue Id=pm.runasnewuserCommand.exe Type=expandable Value=“[#pm.runasnewuser.cmd]” “%1” %* KeyPath=yes
/>
</
RegistryKey
>
<
Condition>ALLUSERS</Condition
>
</
Component
>
<
Component Id=pm.runasnewuser.reg.msc Guid=20D758CC-2774-4532-BD6D-E7C378761C90
>
<
RegistryKey Id=pm.runasnewuser.reg.msc Key=SystemFileAssociations\.msc\shell\Run as new user…\command Root=HKCR Action=createAndRemoveOnUninstall
>
<
RegistryValue Id=pm.runasnewuserCommand.msc Type=expandable Value=“[#pm.runasnewuser.cmd]” “%1” %* KeyPath=yes
/>
</
RegistryKey
>
<
Condition>ALLUSERS</Condition
>
</
Component
>
</
Directory
>
</
Directory
>
</
Directory
>

Notice that I made these components conditional on ALLUSERS.  We’ll make the per-user version conditional on NOT (ALLUSERS).  This is pretty basic stuff, and is more or less simply specifying what goes where without much specialized stuff.  You can do a per-user setup that looks quite similar by putting it somewhere that isn’t off the user’s profile.  I decided to put mine in the user’s profile which means a little extra baggage.


So we’re basically done right?  Well, not really, no.  We’ve disabled the elevation, but now you need to make your installation not require elevation.  Things like installing to Program Files require admin rights, so we’ll need to install to somewhere else.  I’m not really sure where that should be – probably %UserProfile%\ProgramFiles or something like that.  In this example, I installed to LocalAppData.

      <Directory Id=LocalAppDataFolder Name=AdTools>
<
Directory Id=pu.Microsoft Name=Microsoft
>
<
Directory Id=pu.Runas Name=RunAs
>
<
Component Id=pu.runasnewuser.cmd Guid=06A57A74-7639-4A96-A1FF-6C434ED50CEF
>
<
File Id=pu.runasnewuser.cmd Name=runasnewuser.cmd DiskId=1 Source=runasnewuser.cmd
/>
<
RegistryValue Id=pu.runasnewuser.cmd.keypath Root=HKCU Key=Software\Microsoft\RunAs\KeyPaths Type=string Value=RunAsNewUser.cmd KeyPath=yes
/>
<
RemoveFolder Id=pu.Runas Directory=pu.Runas On=uninstall
/>
<
RemoveFolder Id=pu.Microsoft Directory=pu.Microsoft On=uninstall
/>
<
Condition>NOT (ALLUSERS)</Condition
>
</
Component
>
<
Component Id=pu.runasnewuser.reg.exe Guid=C0DB0776-441A-4AB9-871A-5AF1F326FA0A
>
<
RegistryKey Id=pu.runasnewuser.reg.exe Key=SystemFileAssociations\.exe\shell\Run as new user…\command Root=HKCR Action=createAndRemoveOnUninstall
>
<
RegistryValue Id=pu.runasnewuserCommand.exe Type=expandable Value=“[#pu.runasnewuser.cmd]” “%1” %*
/>
</
RegistryKey
>
<
RegistryValue Id=pu.runasnewuser.reg.keypath Root=HKCU Key=Software\Microsoft\RunAs\KeyPaths Type=string Value=RunAsNewUser.reg KeyPath=yes
/>
<
Condition>NOT (ALLUSERS)</Condition
>
</
Component
>
<
Component Id=pu.runasnewuser.reg.msc Guid=C5B94A3B-7D87-4FC0-AC28-111B86679251
>
<
RegistryKey Id=pu.runasnewuser.reg.msc Key=SystemFileAssociations\.msc\shell\Run as new user…\command Root=HKCR Action=createAndRemoveOnUninstall
>
<
RegistryValue Id=pu.runasnewuserCommand.msc Type=expandable Value=“[#pu.runasnewuser.cmd]” “%1” %*
/>
</
RegistryKey
>
<
RegistryValue Id=pu.runasnewuser.reg.msc.keypath Root=HKCU Key=Software\Microsoft\RunAs\KeyPaths Type=string Value=RunAsNewUser.reg.msc KeyPath=yes
/>
<
Condition>NOT (ALLUSERS)</Condition
>
</
Component
>
</
Directory
>
</
Directory
>
</
Directory
>

The extra baggage is the HKCU keypath RegistryValue, and the RemoveFolder entries.  Nothing crazy, but a little extra to be aware of.  These are necessary because of things like roaming profiles.


The experience


 You can install it and it goes through without any elevation.  The installation is per-user by default.  (Yay! Per-user works and has a smokin’ experience, and supports all kinds of scenarios.)


If you want to install it per-machine, you need to launch it via this command:



msiexec /i RunAsNewUser.msi ALLUSERS=1


This is obviously not ideal – but there’s a hidden hiccup.  I thought that it would prompt for elevation if you specified ALLUSERS=1 – it does not.  You get this error message:


You do not have sufficient privileges to complete this installation for all usrs of the machine.


It’s not the end of the world.  If you run the same command from an already elevated process (e.g. an elevated command prompt) the installation goes through just fine (as the error message implies).  It’s a bit of a shame, but I’ve confirmed with Carolyn that you can’t build a single package that will elevate sometimes and sometimes not (because the flag is in the MSI summary information it can’t be changed in-flight).


The best practice for building a per-user (without elevation) and per-machine app (with elevation)


Because of the limitations noted above, here’s what is the best practice for building a per-user and per-machine app:  Have a bootstrapper exe, and two separate MSIs (one per-user package and one per-machine package).  If these are embedded in the bootstrapper exe as resources that are extracted at install, then you can a single download that installs either as per-machine or per-user.


Why?  Well, not many folks are trying to do this at the moment.  Of course, more will as Vista is on more and more computers – ultimately I think this is a huge step forward for keeping your computer free of malware.  I think it’s worth saying twice: I feel much better installing a program that doesn’t require elevation to install – at a minimum I know it’s not disabling my anti-malware software.


Other Resources


http://blogs.msdn.com/windows_installer_team/ – Get it straight from the horse’s mouth


http://blogs.msdn.com/astebner/archive/2006/12/13/some-useful-things-i-have-learned-about-windows-installer-and-uac.aspx – Good intro article that doesn’t bury you in information like Roberts’ (great) blog entries


http://blogs.msdn.com/rflaming/archive/2006/10/01/uac-in-msi-notes-answers-to-questions-in-comments-from-earlier-blog-posts.aspx – Summary of Robert’s huge series about MSI and UAC


http://blogs.msdn.com/rflaming/archive/2006/09/30/778690.aspx – Robert gives his take on the question: Should I write my installer as a Standard User install? If yes, how?



* I imagine that you don’t really need two versions of the components because I imagine that you could change the directory location using a custom action that set it, but I just found things a lot simpler if I used two different components

Comments (6)

  1. windowsmarketplace says:

    James, why do you use msiinfo instead of the Package/@InstallPrivileges attribute to set the elevation flags in the MSI?

    Also, if you look at a <a href="http://wix.sourceforge.net/clickthrough.html">ClickThrough</a&gt; in the WiX toolset, you can see a much more elegant way of switching from per-machine to per-user without duplicating all of your authoring.

  2. @windowsmarketplace

    InstallPrivileges certainly works – I was using a few different versions of Wix and some of them (some of the 3.0 stuff, IIRC) didn’t support it.  msiinfo is just something I used because it was always supported.

    ClickThrough – ignorance, really.  I tried to look at it and give it a run, but I really don’t understand how it all connects together and the limitations.  Maybe you’d be interested in doing lunch?

  3. As I previously described in this blog post , Windows Installer will prompt users for elevation by default

  4. Pankaj says:

    James,

    I am currently writing a WiX installer that needs to do both single-user and all-user installs, and I followed the scheme you described above.

    However, I find that this doubled the size of the installer, because it is now carrying around two compressed copies of the same executable.

    Is there any way to get around that?

    Also, is LocalAppDataDir the canonical place for a single-user install?

    Thanks,

    Pankaj

  5. @Pankaj

    Well, a few comments first – one point of this blog entry is to talk about the issues with a single-package per-machine/per-user setup.  You should follow the best practice unless you’re happy with the issues that I mentioned here.

    If those don’t bother your scenario, then you can do the work (mentioned super briefly in the footnote) to change the installdir on the components and make the removedir/HKCU keypath stuff be conditional on ALLUSERS, or you can simply use the latest v3 version of wix (it has a feature that reuses compression of duplicate files – the file will have 2 entries in your cab, but only take up space once).

    I don’t think that there is a canonical place for a single-user (and LUA-free) install yet.

    HTH,

    James

Skip to main content