Windows Store Apps Sideloadée et mises à jours automatisées

Ça y est, vous avez fini votre application d’entreprise Windows Store. Elle est pas encore parfaite mais cela s’arrangera au prochaine mise à jour.

Les mises à jours ! Vous y avez pensé ? Mais comment être sûr que les utilisateurs utilisent bien la dernière version de votre programme ?

Je vous propose ici une solution qui a comme seule contrainte d’utiliser un service WCF, donc qui ne fonctionnera que pour une application connectée, mais après tout, comment vérifier l’existence d’une mise à jour en mode déconnecté ?

Les prérequis

Le fonctionnement de ce système nécessite d’avoir un minimum la main sur le serveur hébergeant le WCF, à savoir :

  • Pouvoir y copier des fichiers dans un dossier spécifique et qui sera partagé en lecture seule.
    • Ces fichiers seront : le package Appx en cours et un fichier Bat d’installation
  • Le service Web devra également avoir le droit de lire dans ce dossier.
  • Enfin, un autre site Web devra être créé – sur ce serveur ou un autre, peut importe – afin d’héberger le script de lancement de la mise à jour.

L’autre prérequis essentiel est que le package de l’application doit impérativement être signé.

L’approche

En partant sur une application basée sur un modèle MVVM la logique de fonctionnement est la suivante :

J’ai une Loading Page qui mimique le Splash Screen (assez classique comme principe) à laquelle est associée un ViewModel.

Lors de l’initialisation de ce ViewModel, une requête vers le service WCF va récupérer la version en cours du package disponible à l’installation et modifie une propriété booléenne indiquant si une mise à jour est disponible (en comparant la version retournée avec celle du package en cours d’exécution).

Le code behind de la vue vérifie cette propriété avant toute autre action et, si une mise à jour est nécessaire, invite l’utilisateur à la faire avant de continuer l’initialisation de l’application.

Si l’utilisateur décide de faire l’update, une Uri est appelée pour démarrer le processus de mise à jour via un batch et l’application est fermée.

Lorsqu’une mise à jour est disponible, un MessageDialog est affiché pour permettre à l’utilisateur de faire la mise à jour

La configuration du serveur

Pour l’exemple, nous imaginerons la configuration suivante :

    • un service WCF disponible à l’adresse https://contoso/SuperApp/SuperWCF.svc
    • Sur ce même serveur, un dossier D:\SuperApp\Package qui contiendra le package en cours (partagé en tant que \\contoso\SuperAppPackage\)
    • Un autre site web https://contoso/SuperAppUpdate qui contiendra un fichier Bat au contenu suivant
  1: start "Updating SuperApp" /WAIT /B \\contoso\SuperAppPackage\add-ModernApplication.bat
  2: exit

et le fichier Web.config

  1: <?xml version="1.0" encoding="UTF-8"?>
  2: <configuration>
  3:     <system.webServer>
  4:         <staticContent>
  5:             <mimeMap fileExtension=".bat" mimeType="application/octet-stream" />
  6:         </staticContent>
  7:     </system.webServer>
  8: </configuration>

Le code de l’application

LoadingPage.Xaml.cs

  1: public sealed partial class LoadingPage : LayoutAwarePage
  2: {
  3:     public LoadingPageViewModel ViewModel
  4:     {
  5:         get { return this.DataContext as LoadingPageViewModel; }
  6:         set { this.DataContext = value; }
  7:     }
  8:  
  9:     public LoadingPage()
  10:     {
  11:         this.InitializeComponent();
  12:  
  13:         //Initialisation du ViewModel
  14:         var viewModel = new LoadingPageViewModel(ViewService.Service);
  15:         this.ViewModel = viewModel;
  16:  
  17:         //Verification de la disponibilité d'une mise à jour
  18:         var isOutDatedTask = CheckOutdatedVersion();
  19:         isOutDatedTask.Wait();
  20:         var isOutDated = isOutDatedTask.Result;
  21:     }
  22:  
  23:     private async Task<bool> CheckOutdatedVersion()
  24:     {
  25:         if (this.ViewModel.IsOutdated)
  26:         {
  27:             bool? requestUpdate = null;
  28:             var message = new MessageDialog(@"Super App n'est pas à jour. Voulez-vous effectuer la mise à jour ?", "Super App")
  29:                 {
  30:                     Commands =
  31:                 {
  32:                     new UICommand(@"Mettre à jour", cmd => requestUpdate = true),
  33:                     new UICommand(@"Quitter Super App", cmd => requestUpdate = false)
  34:                 },
  35:                     DefaultCommandIndex = 0,
  36:                     CancelCommandIndex = 1
  37:                 };
  38:  
  39:             await message.ShowAsync();
  40:  
  41:             if (!requestUpdate.HasValue || !requestUpdate.Value)
  42:                 Application.Current.Exit();
  43:             else
  44:             {
  45:                 await Windows.System.Launcher.LaunchUriAsync(new Uri(new Uri(@"https://contoso/SuperAppUpdate/"), "Update.bat"));
  46:                 Application.Current.Exit();
  47:             }
  48:             return true;
  49:         }
  50:         return false;
  51:     }
  52: }

LoadingPaveViewModel.cs

  1: public class LoadingPageViewModel : BindableBase
  2: {
  3:     private bool _isOutdated;
  4:     public bool IsOutdated
  5:     {
  6:         get { return _isOutdated; }
  7:         set { SetProperty(ref _isOutdated, value); }
  8:     }
  9:  
  10:     public LoadingPageViewModel(IViewService viewService)
  11:         : base(viewService)
  12:     {
  13:         CheckVersion();
  14:     
  15:         if (IsOutdated)
  16:             return;
  17:     }
  18:  
  19:     private void CheckVersion()
  20:     {
  21:         try
  22:         {
  23:             //Appel du WCF pour récupérer la version du dernier package disponible
  24:             var lastVersion = WCFProxy.SuperWCFClient.GetCurrentVersion();
  25:             //Récupération de la version du package en cours d'éxecution
  26:             var packageVersion = Windows.ApplicationModel.Package.Current.Id.Version;
  27:             //Conversion du type PackageVersion en Version
  28:             var currentVersion =
  29:                 new Version(string.Format("{0}.{1}.{2}.{3}", packageVersion.Major, packageVersion.Minor,
  30:                                           packageVersion.Build, packageVersion.Revision));
  31:             //Comparaison des versions
  32:             IsOutdated = currentVersion.CompareTo(lastVersion) < 0;
  33:         }
  34:         catch (Exception)
  35:         {
  36:             //Si le WCF n'est pas accessible, on considère que la version est à jour
  37:             IsOutdated = false;
  38:         }
  39:     }
  40: }

SuperWCF.svc.cs

  1: public class SuperWCF
  2: {
  3:  
  4:     private const string SuperAppPackageFolder = @"D:\SuperApp\Package\";
  5:     private const string SuperAppPackageNameRegEx = @"SuperApp.*_(\d*[.]\d*[.]\d*[.]\d*).*";
  6:  
  7:     /// <summary>
  8:     /// Gets the current SuperApp package version.
  9:     /// </summary>
  10:     /// <returns></returns>
  11:     /// <exception cref="System.IO.FileNotFoundException">SuperApp Package not found</exception>
  12:     public Version GetCurrentVersion()
  13:     {
  14:         Version returnVersion;
  15:  
  16:         try
  17:         {
  18:             var folder = new DirectoryInfo(SuperAppPackageFolder);
  19:             var files = new List<FileInfo>(folder.GetFiles("*.appx"));
  20:             if (files.Count <= 0)
  21:                 throw new FileNotFoundException("Kymu Package not found");
  22:  
  23:             var packageFile = files.OrderByDescending(f => f.Name).First();
  24:             returnVersion = ExtractVersionNumberFromPackageName(packageFile.Name);
  25:         }
  26:         catch
  27:         {
  28:             returnVersion = new Version(1, 0, 0, 0);
  29:         }
  30:         return returnVersion;
  31:     }
  32:  
  33:     /// <summary>
  34:     /// Extracts the version number from the name of the SuperApp package.
  35:     /// </summary>
  36:     /// <param name="packageName">Name of the package.</param>
  37:     /// <returns></returns>
  38:     private static Version ExtractVersionNumberFromPackageName(string packageName)
  39:     {
  40:         var match = Regex.Match(packageName, SuperAppPackageNameRegEx, RegexOptions.IgnoreCase);
  41:         Version returnValue;
  42:         if (!match.Success || match.Groups.Count < 2 || !Version.TryParse(match.Groups[1].Value, out returnValue))
  43:             returnValue = new Version(1, 0, 0, 0);
  44:         return returnValue;
  45:     }
  46:  
  47: }

Le Batch d’installation

Voici le fichier batch qui réalise au l’installation et qui peut fonctionner depuis un partage réseau

Add-ModernApplication.bat

  1: @ECHO OFF
  2:  
  3: REM #######################################################################
  4: REM # Add-ModernApplication.bat
  5: REM # <summary>
  6: REM # Add-ModernApplication.bat is a powershell.exe script designed to install
  7: REM # the Microsoft Corporate Appx packages created by Visual Studio. 
  8: REM #
  9: REM # The target folder will contain the .appx application file,
  10: REM # the Add-ModernApplication.bat script, plus a .\Dependencies\ folder 
  11: REM # containing all the framework packages used by the application, if needed
  12: REM # When executed from a local directory, the Add-ModernApplication.bat script
  13: REM # simplifies installing the AppX package on a new computer by
  14: REM # automating the following functions.
  15: REM #
  16: REM #
  17: REM # 1. Checks for the Win8 or Win8.1 OS client.
  18: REM # 2. Checks for the client to be volume licensed
  19: REM # 3. Checks for the build to be at the minimum level
  20: REM # 4. If a server SKU, checks for the Desktop Experience Feature
  21: REM #
  22: REM # 4. Installs all the framework packages contained in
  23: REM # .\Dependencies
  24: REM #
  25: REM # 5. Installs the application package .appx file.
  26: REM # 
  27: REM # The Assumption is made that the client satisfies all the requirements 
  28: REM # for Enterprise side-loading of AppX packages and that the Package is signed
  29:  
  30: REM #######################################################################
  31:  
  32: set W8RTM=9200
  33: set W81RTM=9600
  34: set RP=8400
  35:  
  36: REM Check for Windows 8 OS and build
  37: for /f "delims=[] tokens=2" %%i in ('ver') do set MyVer=%%i
  38: for /f "tokens=2" %%i in ('echo %MyVer%') do set MyVer=%%i
  39: for /f "tokens=1,2,3 delims=." %%i in ('echo %MyVer%') do (
  40:     set OSMajorVer=%%i
  41:     set OSMinorVer=%%j
  42:     set OSBuild=%%k
  43: )
  44:  
  45: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "`n`nThe Script identified this machine as Windows Version "%OSMajorVer%"."%OSMinorVer%", Build Number " %OSBuild%"`n" -foregroundcolor "green" 
  46: REM echo The Script identified this machines as %OSMajorVer%.%OSMinorVer%, Build Number %OSBuild%
  47: REM # echo Found OS Minor Version: %OSMinorVer%
  48: REM # Found OS Build number: %OSBuild%
  49:  
  50:  
  51: REM Get SKU of client to test for Volume licensing
  52:  
  53: REM for /f "delims= tokens=1" %%i in ('%windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command "(get-wmiobject win32_operatingsystem).OperatingSystemSKU"') do set MySku=%%i
  54: for /f "delims= tokens=1" %%i in ('%windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command "(get-wmiobject win32_operatingsystem).OperatingSystemSKU"') do set MySku=%%i
  55:  
  56: If %OSMajorVer% EQU 6 if %OSMinorVer% EQU 2 goto Win8VLtest
  57: If %OSMajorVer% EQU 6 if %OSMinorVer% EQU 3 goto Win8VLtest
  58:  
  59: REM Version failed, not Windows8
  60: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "ERROR: Windows 8 Modern Applications are only supported on Windows 8." -foregroundcolor "red"
  61: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "Ending Program `n" -foregroundcolor "red"
  62: goto end
  63:  
  64:  
  65: :Win8VLtest
  66:  
  67: REM Checking for volume licensed media 
  68:  
  69:  
  70: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "`n`nThe SKU "%MySku%""
  71:  
  72: if %MySku% EQU 4 goto Win8RTMtest
  73: if %MySku% EQU 27 goto Win8RTMtest
  74: if %MySku% EQU 48 goto Win8RTMtest
  75: if %MySku% EQU 77 goto Win8RTMtest
  76: if %MySku% EQU 80 goto Win8RTMtest
  77: if %MySku% EQU 97 goto Win8RTMtest
  78: if %MySku% EQU 8 goto Win8BuildServer
  79: if %MySku% EQU 10 goto Win8BuildServer
  80: if %MySku% EQU 42 goto Win8BuildServer
  81: if %MySku% EQU 38 goto Win8BuildServer
  82:  
  83: REM not VL media, Failure
  84: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "ERROR: Sideloaded Windows 8 Modern applications may only be installed on Volume Licensed OS SKUs of Windows 8." -foregroundcolor "red" 
  85: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "Ending Program" -foregroundcolor "red" 
  86: goto end
  87:  
  88: :Win8BuildServer
  89:  
  90: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "This machine has been identifed as a Server OS."  -foregroundcolor "green" 
  91:  
  92: for /f "delims= tokens=1" %%i in  ('%windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command "(Get-WindowsFeature desktop-experience).Installstate"') do Set DesktopEx=%%i
  93:  
  94: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "This Server sku appears to have the Desktop Experience "%DesktopEx%", which is a requirement for side-loading of Modern Apps"  -foregroundcolor "green" 
  95: If %DesktopEx% EQU Installed goto Win8RTMtest
  96:  
  97: REM Server does not have Desktop-Experience implemented, Failure
  98: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "ERROR: This machine appears to be a valid server SKU, but you need to implement the Desktop-Experience feature, first." -foregroundcolor "red"
  99: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "Ending Program" -foregroundcolor "red" 
  100: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "Open up elevated Powershell and run" -foregroundcolor "red" 
  101: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "Import-Module ServerManager , then , Install-WindowsFeature Desktop-Experience" -foregroundcolor "red" 
  102: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "You will then need to reboot your system" -foregroundcolor "red" 
  103: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "Ending Program" -foregroundcolor "red" 
  104: goto end
  105:  
  106:  
  107: :Win8RTMtest
  108:  
  109: if %OSBuild% EQU %W8RTM% goto  Win8Pass
  110: if %OSBuild% EQU %W81RTM% goto  Win8Pass
  111: REM if %OSBuild% EQU %RP% goto Win8Pass
  112:  
  113:  
  114: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "ERROR: This program is only intended on Windows 8 RTM (6.2.9200) or Windows 8.1 RTM (6.2.9600)." -foregroundcolor "red"
  115: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "Ending Program `n" -foregroundcolor "red"
  116: goto end
  117:  
  118: :Win8Pass
  119:  
  120: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "This machine appears to satisfy the requirements needed to install this Modern application. `n Proceeding to install which may take a minute. Please wait... `n" -foregroundcolor "green"
  121: pushd %~dp0
  122: SET Test-Path=%~dp0
  123:  
  124: REM Proceed to add in the AppX package found in this same directory
  125: REM Only one AppX package should exist in this same directory
  126:  
  127: ECHO Using %windir%\System32\WindowsPowerShell\v1.0\powershell.exe Cmdlets to Install Appx Files
  128:  
  129:  
  130: ECHO  ^- Installing Dependency Packages...
  131: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Import-Module appx; if(Test-Path -Path .\Dependencies) { $DependencyFiles = get-childItem .\Dependencies ^| where{$_.extension -eq '.appx'} ^| foreach-object -process{$_.FullName}; if(!$DependencyFiles) {exit 1}; foreach($Dependency in $DependencyFiles){ Write-Output " Installing: $Dependency"; Add-AppxPackage $Dependency; if(!$?){exit{5}}} } else {exit 1}
  132:  
  133: set RETURN=%ERRORLEVEL%
  134: set RETURNDependency = %RETURN%
  135: IF "%RETURN%"=="1" ECHO      No Dependencies Found.
  136: IF "%RETURN%"=="5" (
  137:     %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host " ERROR: Dependent Package identified, but FAILED TO INSTALL!" -foregroundcolor "red"
  138:     goto CLEANUP
  139:     )
  140:         
  141:  
  142: ECHO  ^- Installing Windows 8 Modern Application...        
  143: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Import-Module appx; $PackageFile = get-childItem .\ ^| where{$_.extension -eq '.appx'} ^| foreach-object -process{$_.FullName}; if(!$PackageFile){exit 6}; if($PackageFile.Count -gt 1){exit 7}; Write-Output " Installing: $PackageFile"; Add-AppxPackage $PackageFile; if(!$?) {exit 8}
  144: set RETURN=%ERRORLEVEL%
  145: IF "%RETURN%"=="0" %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "`n SUCCESS! Find the tile for this application on the far right side of your Windows 8 Start Screen. `n" -foregroundcolor "green"
  146: IF "%RETURN%"=="6" %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host " ERROR: NO APPLICATION FOUND TO INSTALL! `n" -foregroundcolor "red"
  147: IF "%RETURN%"=="7" %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host " ERROR: MORE THAN ONE APPLICATION PACKAGE FOUND! 'n" -foregroundcolor "red"
  148: IF "%RETURN%"=="8" %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host " ERROR: FAILED TO INSTALL WINDOWS 8 APPLICATION! 'n" -foregroundcolor "red"
  149:    
  150: :CLEANUP
  151: popd
  152: :end
  153: pause

Voilà !

N’hésitez pas à me solliciter si vous avez la moindre question concernant ce modèle de mise à jour automatisée