Loupe

Mise à jour automatique d'une application UWP sideloadée sur un Raspberry PI à l'aide d'une background task

Lorsque l'on a une application UWP sideloadée sur Windows 10 IOT Core, il peut être intéressant de la mettre à jour de temps en temps :) Or si l'on ne passe pas par le Store il n'y a pas de solution simple. Ce que je vais proposer ici est de mettre en place une application en tâche de fond qui va s'occuper de le faire pour nous.

Le fonctionnement en théorie

L'idée est d'utiliser les mêmes API que le Store et notamment la classe PackageManager au sein d'une application Background IOT. Ces applications tournent en tâche de fond tant que le Raspberry PI est allumé. De plus, si elles crashent, elles seront relancées automatiquement.

La classe PackageManager va nous permettre 3 choses primordiales à partir d'un package appx et de son identité :

  1. Vérifier si un package est installé sur le périphérique,
  2. Installer un package,
  3. Mettre à jour un package.

Pour pouvoir utiliser cette API il faut déclarer une capacité spécifique au sein de votre manifest : packageManagement. Cette dernière faisant partie des capacités jugées dangereuses, il faut penser à ajouter à la main le namespace XML leur correspondant dans le manifest et l'utiliser comme ceci :

<Package ...
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
 IgnorableNamespaces="uap mp iot rescap">

  <Capabilities>
    <Capability Name="internetClient" />
    <rescap:Capability Name="packageManagement" />
  </Capabilities>

</Package>

Créer une App Background

Pour créer une application Background, il faut penser à installer les templates IOT dédiés à Visual Studio 2017 disponibles dans la galerie Visual Studio. Une fois cette opération faite, vous pouvez créer un projet en utilisant ce template :2018-03-09.png 

Notre code correspondra alors à l'implémentation d'une IBackgroundTask ne devant jamais se terminer. Pour cela on va déclencher une boucle dans la méthode Run après avoir demandé un deferral que l'on ne complétera qu'en cas de demande d'annulation de la BackgroundTask par l'OS.

public sealed class StartupTask : IBackgroundTask
{
  public void Run(IBackgroundTaskInstance taskInstance)
  {
    StartUpdateAvailableLoopAsync(taskInstance);
  }

  private async Task StartUpdateAvailableLoopAsync
    (IBackgroundTaskInstance taskInstance)
  {
    var deferal = taskInstance.GetDeferral();
    var canceller = new CancellationTokenSource();
    taskInstance.Canceled += (_, __) => canceller.Cancel();
    var token = canceller.Token;

    while (!token.IsCancellationRequested)
    {
      //faire ici le travail de vérification de nouvelle version

      // check toutes les 10 secondes
      await Task.Delay(1000 * 10, token);
    }

    deferal.Complete();
  }
}

Le problème que l'on va ensuite rencontrer est qu'il est impossible de débugger cette tâche de fond directement sur notre PC de développement. Pour répondre à cette problématique, on va utiliser une astuce toute simple : je déplace le code correspondant à la boucle dans une classe AppUpdaterLogic à part que j'ajoute "en tant que lien" à un nouveau projet UWP classique. Je pourrais alors debugger ce projet de façon classique.

2018-03-09 (1).png

Le code final de ma background Task ressemble donc à cela : 

var deferal = taskInstance.GetDeferral();
var canceller = new CancellationTokenSource();
taskInstance.Canceled += (_, __) => canceller.Cancel();

await AppUpdaterLogic.UpdaterLoopAsync(canceller.Token);

deferal.Complete();

 

Détecter un nouveau package

La logique sera spécifique à votre contexte mais ici j'ai choisi d'exposer une potentielle mise à jour sous la forme d'un package appx ayant toujours le même nom sur un serveur de fichier. Pour développer rapidement, j'utilise le package npm http-server : il suffit de l'installer (npm install http-server -g) et de lancer la commande http-server dans le dossier que l'on veut exposer:
Capture.PNG

Ainsi dans ma boucle de travail, je vais regarder ponctuellement (toutes les 10 secondes dans le code ci-dessous) si j'ai un nouveau fichier. Pour savoir s'il s'agit d'une update, j'utilise le header Date que je stocke dans les paramètres de l'application (cette logique est cachée derrière une propriété LastSeenPackageDate dont je mets le code dans un gist ici). J'utilise bien sûr l'HttpClient Windows pour effectuer ce travail de requêtage et je n'ai besoin que de lire les Headers dans un premier temps :

internal static async Task 
   UpdaterLoopAsync(CancellationToken token)
{
  while (!token.IsCancellationRequested)
  {
    var nonce = Guid.NewGuid().ToString("N");
    bool needToInstallPackage = false;
    string packagePath = null;
    DateTimeOffset? tempLastSeenPackage = null;

    using (var httpClient = new HttpClient())
    using (var answer = await httpClient.GetAsync(
      new Uri($"http://host/PackageToDeploy.appx?temp={nonce}"),
      HttpCompletionOption.ResponseHeadersRead))
    {
      if (LastSeenPackageDate != answer.Headers.Date)
      {
        tempLastSeenPackage = answer.Headers.Date;
        needToInstallPackage = true;
        packagePath = await DownloadPackageAsync(answer)
          .ConfigureAwait(false);
      }
    }

    if (needToInstallPackage)
    {
      await InstallPackageAsync(packagePath).ConfigureAwait(false);
      LastSeenPackageDate = tempLastSeenPackage;
    }

    // check toutes les 10 secondes
    await Task.Delay(1000 * 10, token);
  }
}

Le téléchargement du package (la méthode DownloadPackageAsync) se fait aussi de manière très classique :

private static async Task<string>
    DownloadPackageAsync(HttpResponseMessage answer)
{
    var tempFile = await ApplicationData.Current.LocalCacheFolder
        .CreateFileAsync(LocalPackageFileName, 
           CreationCollisionOption.ReplaceExisting);
    using (var fileStream = await tempFile
               .OpenAsync(FileAccessMode.ReadWrite))
    {
        await answer.Content.WriteToStreamAsync(fileStream);
    }

    return tempFile.Path;
}

Installer / mettre à jour le package

 On va ensuite rentrer dans le vif du sujet, la méthode InstallPackageAsync va installer ou mettre à jour notre package téléchargé localement. 

Pour détecter si l'application est installée, je vais lister tous les packages de l'utilisateur et chercher si celle que je veux installer est présente. Contrairement à ce que dit la documentation Microsoft, la capacité PackageManagement déjà renseignée précédemment suffit pour utiliser cette API.

string fullNamePackage = "fullName";
var packageManager = new PackageManager();
var packages = packageManager.FindPackagesForUser(string.Empty).ToList();

var updateInsteadOfInstall = packages
    .Any(p => p.Id.FullName == fullNamePackage);

Une fois cette information déterminée, il suffit d'appeller les méthodes AddPackageAsync ou UpdatePackageAsync d'une instance de PackageManager. En cas d'installation, j'en profite pour lancer l'application via un protocole que je lui ai précédemment attribué.

if (updateInsteadOfInstall)
{
    var result = await packageManager.UpdatePackageAsync
        (packageUri, null, DeploymentOptions.ForceApplicationShutdown);
}
else
{
    var result = await packageManager.AddPackageAsync
        (packageUri, null, DeploymentOptions.ForceApplicationShutdown);

    // lances l'appli
    Launcher.LaunchUriAsync(new Uri("monApp://kikou"));
}

Déployer la tâche de fond

 Il ne nous reste plus qu'à déployer la tâche de fond sur le Raspberry PI. Pour cela on peut utiliser la manière classique depuis Visual Studio ou passer par le Device Portal du Raspberry. Une fois la tâche déployée, il faudra penser à aller la configurer pour se lancer automatiquement au démarrage. Le plus simple est de passer par le Device Portal mais il est possible de suivre la procédure détaillée dans la documentation.

Capture.PNG

 Happy coding :)

Photo de profil

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus