Wednesday, 23 February 2011

Running a ClickOnce Application as Administrator

I've been using ClickOnce to deploy our Windows apps for about 12 months now, and it's been an absolutely painless way to handle installation and application updates.
One of the reasons it's so painless is that ClickOnce doesn't require admin privileges to install apps on a machine. However this also means that you cannot launch a ClickOnce app with admin privileges.
Until recently this hasn't been a problem for us, but due to some obscure requirement that 64-bit Windows requires admin privileges to access a 32-bit ODBC data source, this shortfall has reared its ugly head.
The official Microsoft stance is that you cannot use ClickOnce if admin privilges are required, and instead install using Windows Installer or similar. However this really didn't appeal because I'd need implement a new way of handling application updates and have to deal with a whole lot of change.
I came across this article by Charles Engelke, which discusses how he used a ClickOnce app to launch a secondary app as Administrator. This sounded really cool, so I wondered if it would be possible for a ClickOnce app to re-launch itself as administrator. It turns out you can.
Here's how we did it:
Our ClickOnce app is WPF, so it's entry point is via the Application_Startup method App.xaml - however you probably already know the entry point for your app. We added the following:
private bool IsRunAsAdministrator()
{
var wi = WindowsIdentity.GetCurrent();
var wp = new WindowsPrincipal(wi);

return wp.IsInRole(WindowsBuiltInRole.Administrator);
}

private void Application_Startup(object sender, StartupEventArgs e)
{
if (!IsRunAsAdministrator())
{
    // It is not possible to launch a ClickOnce app as administrator directly, so instead we launch the
    // app as administrator in a new process.
    var processInfo = new ProcessStartInfo(Assembly.GetExecutingAssembly().CodeBase);

    // The following properties run the new process as administrator
    processInfo.UseShellExecute = true;
    processInfo.Verb = "runas";
        
    // Start the new process
    try
    {
        Process.Start(processInfo);
    }
    catch (Exception)
    {
        // The user did not allow the application to run as administrator
        MessageBox.Show("Sorry, this application must be run as Administrator.");
    }

    // Shut down the current process
    Application.Current.Shutdown();
}
else
{
    // We are running as administrator
        
    // Do normal startup stuff...
}
}
The idea here is that if the current process is not running as administrator, then launch a new process as administrator, then shut down the current process. The new process will realise it's running as administrator and function as normal.
The method we're using to run as administrator is to set the following properties or ProcessStartInfo:
    // The following properties run the new process as administrator
    processInfo.UseShellExecute = true;
    processInfo.Verb = "runas";
I have a feeling this is not best practice (I believe the "correct" way is to embed a manifest in your application with a requestedExecutionLevel element) - but this way was much easier since the process will only ever be launched from within ClickOnce.

5 comments:

Tom said...

Hi Anthony,
Greetings from Florida.

Thanks for the post. Saved me a lot of time.

Mark said...

Thanks for the post. But the new instance launched this way will result in ApplicationDeployment.IsNetworkDeployed to be false and you cannot do updates. How were you able to check for updates?

Thanks in advance.
Mark

Anthony said...

Hi Mark. Yes you are correct, after posting this article I also came across this problem.

I haven't yet had a chance to find a solution, and I've just been telling our clients to uninstall & reinstall to get an update - shocking I know!!

Anthony.

Mark said...

Hi Anthony!

Thanks for the quick reply!. I found a solution and would be happy to share with you. It looks like I can either have my app running under admin role or under network-deployed mode but not both!

So here is what I did, kind of kludge but works:

First thing I do is to check for admin identity, if not then I relaunch the app using your method with some argument telling the next instance why it is launched ( and IsFirstRun flag) that gives me a new instance with admin rights but not network-deployed.

In this new instance, based on the argument from first instance I lunch the click once version of the app like this:

string path = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
path += "\\YouApp.appref-ms";
Process.Start(path, someArgs);

Now this last instance has both admin rights and network deployed feature.

For my app, I needed to know whether it's been launched for the first time using the ApplicationDeployment.CurrentDeployment.IsFirstRun property as I need to download onetime files. Keep in mind that flag is only set to true the very first time the app is run. I need to pass that to the last instance so I can download the rest of the installation accordingly.

Thanks and good luck!

Mark.

Anthony said...

Thanks Mark, looks like a great solution - awesome work. I'll try it out as soon as I get a chance...