App Elevation Samples – Part 3

Picture5

If you haven’t read part 1 and part 2 of this series yet, please start there first. I have covered the “hello world” version of a packaged app that requires elevation at launch time and showed how a packaged app can dynamically elevate itself. In this post we’ll get to the fun part: using the ‘allowElevation’ capability from a UWP app to execute code with elevated privileges on Windows 10 desktop with the 1809 update.

TL;DR

I already have the 1809 update and the Windows SDK 17763 (or later) installed. Just show me the code on GitHub …

https://github.com/StefanWickDev/AllowElevation-Samples/tree/master/Elevated%20Extension%20UWP

Step-by-Step Tutorial

1. Create a new Universal Windows App

Make sure you run Windows 10 with the 1809 update and have the Windows SDK 17763 (or later) installed. Now create a new C# Universal Windows application in Visual Studio 2017 (can use any supported programming language as well, e.g. C++, JS, …):

FileNewProject2

Be sure to set both “Minimum” and “Target” version to 17763 or higher as build number 17763 maps to the 1809 update for Windows 10. The ‘allowElevation’ capability we will be using has been introduced in 1809 and is not available in earlier versions of Windows 10.

setVersions

Now we need to add a ‘fullTrustProcess’ desktop extension to our UWP that will contain the code we want to run with elevated privileges. For a complete tutorial on UWP desktop extension read up here on all the details. In this post I will quickly move through the steps of setting up the desktop extension for our new UWP app:

2. Add the desktop extension project

For this example we want the desktop extension process to be headless (i.e. a background process without UI). It can have a UI as well if that’s what you need for your scenario. So let’s add a C# desktop console application (of course this could be C++, VB, etc. as well) and call it “Elevated Extension”:

addConsole2We don’t want a console box to pop up for our elevated background process, so we need to configure this project to run headless. For that go into the project settings and change the Output Type from “Console Application” to “Windows Application” (I know, not very intuitive …)

outputType

5. Packaging everything together

Next we need to add a “Windows Application Packaging Project” (look for it in the “Windows Universal” category). Name it “Package” and make it the startup project,  so your solution setup looks like in the screenshot below. In order to package both the UWP and the desktop code in one app package, add both as Application references to the Packaging project:

addref3

6. Configuring the appx manifest

Now to connect the two projects we need to declare the “fullTrustProcess” desktop extension in the package manifest. For this open the Package.appxmanifest file in the Packaging project (“Package”) in XML editing mode and add the extension within the <Application> node. While we are in this file we are also adding the “appExecutionAlias” extension that we learned about in part 2, as we will need it to launch our extension with elevated privileges. Last but not least, you will also need to add namespace definitions for “uap3” and “desktop” in the root <Package> element per below snippet.

<Package
  ...
  xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
  xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
  IgnorableNamespaces="uap3 desktop ...">
  ...

  <Extensions>

    <desktop:Extension Category="windows.fullTrustProcess"
                       Executable="Elevated Extension\Elevated Extension.exe">
      <desktop:FullTrustProcess>
        <desktop:ParameterGroup GroupId="User" Parameters="/user" />
        <desktop:ParameterGroup GroupId="Admin" Parameters="/admin" />
      </desktop:FullTrustProcess>
    </desktop:Extension>

    <uap3:Extension Category="windows.appExecutionAlias"
                    Executable="Elevated Extension\Elevated Extension.exe"
                    EntryPoint="Windows.FullTrustApplication">
      <uap3:AppExecutionAlias>
        <desktop:ExecutionAlias Alias="ElevatedExtension.exe" />
      </uap3:AppExecutionAlias>
    </uap3:Extension>

  </Extensions> 
  ...
</Package>

Note that the the two parameter groups I am defining for the desktop extension are not mandatory. I am doing it here only for this sample to later demonstrate how we can launch the extension with either user privileges or admin privileges.

7. Adding the launcher code

Now that solution and manifest are set up and configured correctly, we are ready to add the code to launch the extension from the UWP – as either elevated or regular process. First lets add some XAML to MainPage.xaml:

<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
    <Button Content="Run Backgroundprocess as User"
            Click="ButtonUser_Click"
            HorizontalAlignment="Stretch"
            FontSize="32" Margin="3"/>
    <Button Content="Run Backgroundprocess as Admin"
            Click="ButtonAdmin_Click"
            HorizontalAlignment="Stretch"
            FontSize="32" Margin="3"/>
</StackPanel>

Before we can add the code-behind for the button click handlers, we need to add a reference to the UWP desktop extension SDK, because that’s where the FullTrustProcessLauncher API is defined. This API, btw, is not new in the 1809 update. It has been around since the Creators Update (SDK build 14393), but it’s recommended you reference the Desktop extension SDK that matches your target version, i.e. 17763 in our case.

addDeskExte

Next we add the two button click handlers in MainPage.xaml.cs. The key piece is the FullTrustProcessLauncher API. We are passing in the parametergroup to indicate to the extension whether we need to run it as user or with admin privileges. The processing of this parameter is application logic as we will see in the next step. Wrapping this call into an “IsApiContractPresent” is an important practice to ensure our universal app can run without crashing also on non-desktop devices that don’t support the FullTrustProcessLauncher API:

private async void ButtonUser_Click(object sender, RoutedEventArgs e)
{
  if (ApiInformation.IsApiContractPresent(
      "Windows.ApplicationModel.FullTrustAppContract", 1, 0))
  {
    await
      FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync("User");
  }
}

private async void ButtonAdmin_Click(object sender, RoutedEventArgs e)
{
  if (ApiInformation.IsApiContractPresent(
      "Windows.ApplicationModel.FullTrustAppContract", 1, 0))
  {
    await
      FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync("Admin");
  }
}

The final piece of code we need to add now goes into Program.cs of the “Elevated Extension” project. This is the code that handles the “/user” vs “/admin” parameter. If the latter has been specified we want to do the same as in part 2, i.e. relaunch the process as admin using the AppExecutionAlias. In case you are wondering why the strings we are handling here are different from the strings being passed to the launch API (“/user vs “User”), take a look at the ParameterGroup entries we added to the manifest. It maps a friendly name (“User”) to what could be in some case a fairly convoluted long string of parameters (“/user” in our case).

static void Main(string[] args)
{
    if (args.Length > 2)
    {
        if (args[2] == "/admin")
        {
          string aliasPath =
      Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) +
      @"\microsoft\windowsapps\ElevatedExtension.exe";

          ProcessStartInfo info = new ProcessStartInfo();
          info.Verb = "runas";
          info.UseShellExecute = true;
          info.FileName = aliasPath ;
          Process.Start(info);
          return;
        }
    }

    AutoResetEvent areWeDone = new AutoResetEvent(false);
    // do some task, then exit
    areWeDone.WaitOne(10000); 
}

8. Run and test the solution

Now we are ready to run and test our solution, before hitting F5 make sure the “Package” project is set as startup project and you have added the “allowElevation” capability in its appx manifest:

  <Capabilities>
    <Capability Name="internetClient" />
    <rescap:Capability Name="runFullTrust" />
    <rescap:Capability Name="allowElevation" />
  </Capabilities>

When the UWP is up-and-running use the buttons to launch the extension either as user or as admin. Observe the result in TaskManager, remember that the extension has been coded to exit itself after 10sec in this example:

uwpElevated

This concludes my 3-part tutorial about the new ‘allowElevation’ capability. Please leave your feedback and questions as comments and I will do my best to address them in a timely fashion.

Advertisements

Global hotkey registration in UWP

screenshot_octo

As promised, I am starting now to share a number of concrete scenario samples where UWP apps can light up common desktop features using desktop extensions. In this post my UWP app will register for system-wide hotkeys and handle them – even when the app is suspended.

TL;DR;

architecture

Just show me the code on github …
https://github.com/StefanWickDev/ExtensionGallery/tree/master/GlobalHotkey

Let me run the demo app from the Microsoft Store …
https://www.microsoft.com/store/apps/9NJW3LBC2Z3Q

 

Implementation

If you have followed my tutorial for desktop extensions you will find this one pretty straight forward, here are the detailed steps to implement this feature from scratch:

1. Project Setup

We are using the same project setup as explained in part 1 of the tutorial, with a UWP project, a C# console project and a Packaging project to tie it all together:

solution

Since we don’t want the console project to actually pop up a console window (or any window for that matter), we need to switch the output type from Console to Windows Application (I know, not very intuitive …):

outputtype

2. Declaring & Launching the Desktop Extension

Now we can declare the Desktop Extension in the Package.appxmanifest file. Be sure to declare this in the Packaging project, not the UWP project! While we are at it, let’s also declare the AppService. We will need it later to communicate the WM_HOTKEY event back to the UWP process.

<Extensions>
 <desktop:Extension Category="windows.fullTrustProcess"
 Executable="HotkeyWindow\HotkeyWindow.exe" />
 <uap:Extension Category="windows.appService">
 <uap:AppService Name="HotkeyConnection" />
 </uap:Extension>
</Extensions>

Now with this in place, we can launch the Desktop Extension from our UWP app. Before we launch it, we will store our ProcessId in the local settings, so the extension can pick it up and track the lifetime of the UWP. This will help us to clean up and shutdown when the UWP process goes away.

// MainPage.xaml.cs
if (ApiInformation.IsApiContractPresent("Windows.ApplicationModel.FullTrustAppContract", 1, 0))
{
 Process process = Process.GetCurrentProcess();
 ApplicationData.Current.LocalSettings.Values["processID"] = process.Id;

 await FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync();
}

When the extension launches, it will get to work right away and register the hot keys for our application. Before it can do so though, it needs to create a (headless) window in order to handle the incoming WM_HOTKEY messages, whenever the hot keys get pressed:

// HotkeyWindow.cs
public HotkeyAppContext()
{
 int processId = (int)ApplicationData.Current.LocalSettings.Values["processId"];
 process = Process.GetProcessById(processId);
 process.EnableRaisingEvents = true;
 process.Exited += HotkeyAppContext_Exited;
 hotkeyWindow = new HotKeyWindow();
 hotkeyWindow.HotkeyPressed += new HotKeyWindow.HotkeyDelegate(hotkeys_HotkeyPressed);
 hotkeyWindow.RegisterCombo(1001, Modifiers.Alt, Keys.S); // Alt-S = stingray
 hotkeyWindow.RegisterCombo(1002, Modifiers.Alt, Keys.O); // Alt-O = octopus
}

 

3. Communicating the WM_HOTKEY event

Once we get a HotkeyPressed event (i.e. when we process WM_HOTKEY in our headless window), we create an AppServiceConnection back to the UWP process and send the ID of the hotkey:

// HotkeyWindow.cs
private async void hotkeys_HotkeyPressed(int ID)
{
 // send the key ID to the UWP
 ValueSet hotkeyPressed = new ValueSet();
 hotkeyPressed.Add("ID", ID);

 AppServiceConnection connection = new AppServiceConnection();
 connection.PackageFamilyName = Package.Current.Id.FamilyName;
 connection.AppServiceName = "HotkeyConnection";
 AppServiceConnectionStatus status = await connection.OpenAsync();
 connection.ServiceClosed += Connection_ServiceClosed;
 AppServiceResponse response = await connection.SendMessageAsync(hotkeyPressed);
}

Now on the UWP side we handle this request and kick-off off the respective animal animation:

// MainPage.xaml.cs
private async void AppServiceConnection_RequestReceived(
                     AppServiceConnection sender,
                     AppServiceRequestReceivedEventArgs args)
{
 AppServiceDeferral messageDeferral = args.GetDeferral();
 int id = (int)args.Request.Message["ID"];
 switch(id)
 {
  case 1001://stingray
   await this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
   {
    stingrayMove.Begin();
   });
   break;
   ...

 

4. NativeWindow Implementation

The underlying Win32 magic is implemented in the HotkeyWindow class, derived from NativeWindow:

// HotkeyWindow.cs
public class HotKeyWindow : NativeWindow
{
 private const int WM_HOTKEY = 0x0312;
 private const int WM_DESTROY = 0x0002;

 [DllImport("user32.dll")]
 public static extern bool RegisterHotKey(IntPtr hWnd, int id, int fsModifiers, int vlc);
 [DllImport("user32.dll")]
 public static extern bool UnregisterHotKey(IntPtr hWnd, int id);

 private List<Int32> IDs = new List<int>();
 public delegate void HotkeyDelegate(int ID);
 public event HotkeyDelegate HotkeyPressed;

 // creates a headless Window to register for and handle WM_HOTKEY
 public HotKeyWindow()
 {
  this.CreateHandle(new CreateParams());
  Application.ApplicationExit += new EventHandler(Application_ApplicationExit);
 }

 public void RegisterCombo(Int32 ID, Modifiers fsModifiers, Keys vlc)
 {
  if (RegisterHotKey(this.Handle, ID, (int)fsModifiers, (int)vlc))
  {
   IDs.Add(ID);
  }
 }

 private void Application_ApplicationExit(object sender, EventArgs e)
 {
  this.DestroyHandle();
 }

 protected override void WndProc(ref Message m)
 {
  switch (m.Msg)
  {
   case WM_HOTKEY: //raise the HotkeyPressed event
    HotkeyPressed?.Invoke(m.WParam.ToInt32());
    break;

   case WM_DESTROY: //unregister all hot keys
    foreach (int ID in IDs)
    {
     UnregisterHotKey(this.Handle, ID);
    }
    break;
   }
   base.WndProc(ref m);
  }
 }

5. Lifetime Considerations

There are a couple of interesting app lifetime/lifecycle considerations and options in this sample that I want to touch on:

a. Bringing the UWP back to the foreground

When the hotkey is processed in the sample, the UWP is brought to the foreground, so you can enjoy the animation. This is optional and for other scenarios you may chose to leave it in the background. At any rate, we accomplish this by calling AppListEntry.LaunchAsync() on ourselves. This API when called on a running app has the desired effect to bring the app into the foreground, even when the UWP process was suspended.

b. Lifecycle of the Desktop Extension

In this sample the extension’s lifecycle is coupled with the UWP app’s lifecycle. This makes sense here since we want the hotkeys to only be registered while the UWP is running (or suspended), not beyond that. That’s why we are tracking the UWP process and will unregister the hotkeys and shutdown the extension when the UWP goes away.

However, the extension lifetime is in your control and for a different scenario you may choose to keep the extension process alive even after the UWP process goes away.

c. Lifetime of the AppServiceConnection

On every WM_HOTKEY message we open an AppServiceConnection to the UWP, which we will then close once it has been processed. We could also choose to keep the connection around for the lifetime of the process, but this would have the unwanted effect that the UWP would never suspend when minimized and therefore use more system resources than it should.

 

Credits

C# Code for the headless window to handle WM_HOTKEY inspired my this Stackoverflow answer: https://stackoverflow.com/questions/17133656/global-hotkeys-in-windowless-net-app/17136050

Tile icon for the Store sample provided by https://creativeninjas.com/