Compare commits

...
8 Commits
Author SHA1 Message Date
nicolas 89570947aa Merge pull request 'Dev/3.4.0' (#142) from dev/3.4.0 into master
Reviewed-on: #142
Reviewed-by: Nicolas Deville <nicolas.deville@lanisys.fr>
2026-11-10 13:49:24 +01:00
nicolas 14b2167218 Launcher 3.4.0 : mises à jour prioritaires
- Version 3.3.1 → 3.4.0
- Notes de version (notes-launcher.txt, RELEASE-3.4.0.md)
- README §7 : section « Version prioritaire », sécurité, publication
2026-11-10 13:35:25 +01:00
nicolas 2e4d2ee76e UiSnapshot : scène launcher-update-priority 2026-11-10 13:34:37 +01:00
nicolas 7d9032fe20 Publication : options --priority et --clear-priority
- LauncherPublisher publish : --priority (confirmation o/N), --clear-priority,
  --yes ; priorité reprise du launcher-update.json précédent s'il est signé
- Ligne « Priorité » dans publish et verify
- publish-launcher-update.bat : paramètres --priority, --clear-priority,
  --help (shift /1 pour garder %~dp0)
2026-11-10 13:34:13 +01:00
nicolas ccd6b1a430 Interface : pastille « Prioritaire » dans la fenêtre de mise à jour du launcher
Sous-titre dédié précisant que le réglage du joueur ne change pas.
2026-11-10 13:33:02 +01:00
nicolas e4a21c6f5d Launcher : version prioritaire installée même si la MAJ auto est désactivée
- Recherche au démarrage même option désactivée ; seule une version
  prioritaire est alors installée, self-update.json jamais modifié
- Erreur de téléchargement d'une version prioritaire : retentée au
  démarrage suivant
- Version prioritaire non installée : avertissement dans le bandeau et
  statut orange dans les Paramètres du launcher
2026-11-10 13:32:42 +01:00
nicolas 1a7f45c38b Tests : versions prioritaires du launcher (8 tests, 12 cas)
Signature, compatibilité sans priorité, launchers forcés ou non,
validation, reprise / retrait par l'outil de publication,
self-update.json sans champ de priorité.
2026-11-10 13:32:05 +01:00
nicolas b48d4222db Core : version prioritaire du launcher dans launcher-update.json (signée)
- LauncherRelease.PriorityVersion : dernière version prioritaire publiée,
  dans le payload signé (ignorée par les launchers < 3.4.0)
- IsPriorityRelease, IsPriorityFor(), TryGetPriorityVersion()
- ResolvePriorityVersion() : marquer, reprendre ou retirer la priorité
  d'une publication à l'autre
- Validate() refuse une version prioritaire illisible ou supérieure à la
  version publiée
- LauncherUpdateCheck.IsPriority
2026-11-10 13:31:36 +01:00
15 changed files with 478 additions and 79 deletions

No files matched your search

+21 -1
View File
@@ -429,7 +429,7 @@ Le launcher (un seul `GameLauncher.exe`) vérifie au démarrage s'il existe une
lui-même, la télécharge, l'installe et **redémarre tout seul**. Réglable dans **Paramètres du launcher** (roue crantée) :
interrupteur « Mettre à jour le launcher automatiquement » (commun à tous les jeux) et bouton
**« Vérifier les mises à jour »** (puis « Installer la x.y.z »). Option désactivée : un bandeau signale
simplement qu'une nouvelle version existe.
simplement qu'une nouvelle version existe — sauf pour une **version prioritaire** (ci-dessous), installée quand même.
### Sécurité
@@ -441,6 +441,7 @@ simplement qu'une nouvelle version existe.
| Pas de rétrogradation | Seule une version **strictement supérieure** est installée ; le produit (`launcherId`) doit correspondre. |
| Tailles bornées | `launcher-update.json` ≤ 256 Ko ; un serveur qui envoie plus que la taille annoncée est coupé (vaut aussi pour les fichiers du jeu). |
| Retour arrière automatique | Si la nouvelle version ne démarre pas dans les 60 s (ou n'est pas la version annoncée), elle est arrêtée et l'ancienne remise en place. Cette version n'est plus proposée automatiquement (bouton manuel uniquement). |
| Version prioritaire signée | L'indication « prioritaire » fait partie du contenu signé : un serveur compromis ne peut ni imposer une mise à jour, ni retirer la priorité d'un correctif. Elle ne permet pas d'installer autre chose qu'une version signée, strictement supérieure. |
### Déroulement
@@ -463,6 +464,22 @@ La mise à jour est refusée tant qu'un jeu est lancé ou qu'un téléchargement
est dans un dossier protégé (Program Files), il ne peut pas se remplacer : le message l'explique.
Depuis Visual Studio (F5), le bouton vérifie mais n'installe pas : seule la version publiée se remplace.
### Version prioritaire (correctif critique)
Une version publiée avec **`--priority`** est installée au démarrage **même chez les joueurs qui ont
désactivé** « Mettre à jour le launcher automatiquement ». Exemple : une faille critique est corrigée en 3.4.0.
| Point | Comportement |
|---|---|
| Réglage du joueur | **Jamais modifié** : `autoUpdate` reste à `false` dans `self-update.json`. Effet ponctuel : une fois le launcher à jour, les versions normales suivantes ne sont plus installées automatiquement chez lui. |
| Versions suivantes | La priorité est **reprise** par les publications suivantes (`priorityVersion` dans `launcher-update.json`) : si la 3.5.0 (normale) sort après le correctif 3.4.0, un launcher encore en 3.3.x passe directement en 3.5.0 ; un launcher déjà en 3.4.0 n'est pas concerné. `--clear-priority` arrête la reprise (priorité publiée par erreur). |
| Fenêtre de mise à jour | Pastille **« Prioritaire »** et texte « Mise à jour importante… votre réglage ne change pas ». « Plus tard » reste possible : nouvelle tentative au démarrage suivant, bandeau d'avertissement en attendant. |
| Erreur de téléchargement | Retentée au démarrage suivant (une version normale n'est plus retentée automatiquement). |
| La nouvelle version ne démarre pas | Retour arrière, puis plus de tentative automatique (sinon 60 s d'attente à chaque lancement) : bandeau d'avertissement et statut orange dans les Paramètres du launcher, installation par le bouton. |
| Dossier protégé (Program Files) | Pas d'installation possible sans droits administrateur : bandeau d'avertissement. |
| Option désactivée | Le launcher interroge désormais le serveur au démarrage (`launcher-update.json`, 1 à 2 Ko, 6 s au plus) pour savoir si une version prioritaire existe. |
| Launchers antérieurs à 3.4.0 | Ignorent l'indication : ils ne suivent que leur propre réglage. La priorité ne vaut que pour les joueurs déjà en 3.4.0 ou plus. |
### Mise en place (une seule fois)
1. **Clés** : double-cliquez `tools\keygen-launcher.bat`. Choisissez une phrase de passe (12 caractères
@@ -490,6 +507,9 @@ Depuis Visual Studio (F5), le bouton vérifie mais n'installe pas : seule la ver
signé par votre clé. Les launchers antérieurs à 1.1.3 reçoivent toujours le titre et la liste « à plat ».
3. Double-cliquez `tools\publish-launcher-update.bat` : compilation, signature (phrase de passe demandée),
puis envoi WinSCP — l'exe d'abord, `launcher-update.json` **en dernier**.
**Correctif critique** : depuis une invite de commandes, `tools\publish-launcher-update.bat --priority`
(confirmation demandée avant la signature). `--clear-priority` ne reprend pas la priorité déjà publiée.
Le résumé affiché indique la priorité (« normale », « PRIORITAIRE » ou « version prioritaire x.y.z reprise »).
Vérification à tout moment :
`dotnet run --project tools\LauncherPublisher -- verify --manifest https://…/launcher/launcher-update.json --public-key src\GameLauncher\Security\update-signing.pub`
@@ -50,9 +50,58 @@ public sealed partial class LauncherRelease
/// </summary>
public List<ChangelogEntry> Changelog { get; set; } = new();
/// <summary>
/// Version PRIORITAIRE la plus récente publiée jusqu'à celle-ci incluse (ex. "3.4.0"), ou null s'il n'y en a pas.
/// Un launcher plus ancien que cette version installe la mise à jour au démarrage MÊME SI le joueur a désactivé
/// la mise à jour automatique (correctif critique). Le réglage du joueur (self-update.json) n'est jamais modifié :
/// une fois le launcher à jour, il n'est plus concerné (effet ponctuel).
/// Reprise d'une publication à l'autre par LauncherPublisher (voir <see cref="ResolvePriorityVersion"/>) : une
/// version normale publiée après un correctif critique l'impose toujours aux launchers restés trop anciens.
/// Fait partie du contenu SIGNÉ : un serveur compromis ne peut ni l'ajouter, ni la retirer, ni la modifier.
/// Ignorée par les launchers antérieurs à 3.4.0 (propriété inconnue).
/// </summary>
public string? PriorityVersion { get; set; }
[JsonIgnore]
public Version ParsedVersion => VersionUtil.Parse(Version);
/// <summary>Cette version a elle-même été publiée comme prioritaire (option --priority).</summary>
[JsonIgnore]
public bool IsPriorityRelease => TryGetPriorityVersion(out var priority) && priority == ParsedVersion;
/// <summary>
/// Vrai si un launcher en version <paramref name="currentVersion"/> doit installer cette mise à jour même avec la
/// mise à jour automatique désactivée : il est plus ancien que la dernière version prioritaire publiée.
/// </summary>
public bool IsPriorityFor(Version currentVersion) =>
TryGetPriorityVersion(out var priority) && priority > currentVersion;
/// <summary>Version prioritaire lisible (null ou illisible : faux).</summary>
public bool TryGetPriorityVersion(out Version priority)
{
priority = new Version(0, 0, 0);
return !string.IsNullOrWhiteSpace(PriorityVersion) && VersionUtil.TryParse(PriorityVersion, out priority);
}
/// <summary>
/// Version prioritaire à inscrire dans une nouvelle publication (LauncherPublisher) :
/// <list type="bullet">
/// <item><paramref name="markPriority"/> : la version publiée elle-même ;</item>
/// <item><paramref name="clearPriority"/> : aucune (priorité publiée par erreur, par exemple) ;</item>
/// <item>sinon : celle de la publication précédente (<paramref name="previous"/>, dont la signature a été
/// VÉRIFIÉE par l'appelant), si elle n'est pas supérieure à la version publiée.</item>
/// </list>
/// </summary>
public static string? ResolvePriorityVersion(Version published, bool markPriority, bool clearPriority, LauncherRelease? previous)
{
if (markPriority && clearPriority)
throw new ArgumentException("Une publication ne peut pas à la fois être prioritaire et retirer la priorité.");
if (markPriority) return VersionUtil.Display(published);
if (clearPriority || previous is null || !previous.TryGetPriorityVersion(out var carried) || carried > published)
return null;
return VersionUtil.Display(carried);
}
public void Validate()
{
if (Format != 1)
@@ -67,6 +116,12 @@ public sealed partial class LauncherRelease
throw new UpdateSecurityException("Description de mise à jour invalide : taille incorrecte.");
if (!Sha256Regex().IsMatch(Sha256 ?? ""))
throw new UpdateSecurityException("Description de mise à jour invalide : empreinte SHA-256 incorrecte.");
if (string.IsNullOrWhiteSpace(PriorityVersion)) PriorityVersion = null;
else if (!VersionUtil.TryParse(PriorityVersion, out var priority))
throw new UpdateSecurityException($"Description de mise à jour invalide : version prioritaire « {PriorityVersion} » illisible.");
else if (priority > ParsedVersion)
throw new UpdateSecurityException(
$"Description de mise à jour invalide : version prioritaire {PriorityVersion} supérieure à la version publiée ({Version}).");
Notes ??= new List<string>();
// Textes affichés tels quels dans l'interface : mêmes bornes et nettoyage que les notes du jeu.
Changelog = ChangelogLimits.Normalize(Changelog);
@@ -10,6 +10,12 @@ public sealed record LauncherUpdateCheck(Version CurrentVersion, LauncherRelease
public bool IsAvailable => Release is not null && DownloadUri is not null;
public string CurrentDisplay => VersionUtil.Display(CurrentVersion);
public string AvailableDisplay => Release is null ? "" : VersionUtil.Display(Release.ParsedVersion);
/// <summary>
/// Mise à jour PRIORITAIRE pour ce launcher (plus ancien que la dernière version prioritaire publiée) :
/// installée au démarrage même si la mise à jour automatique est désactivée. Information signée.
/// </summary>
public bool IsPriority => IsAvailable && Release!.IsPriorityFor(CurrentVersion);
}
/// <summary>
+9 -2
View File
@@ -899,7 +899,8 @@ public partial class App : Application, ILauncherRestartHost
/// <summary>
/// Nouvelle version non installée automatiquement (option désactivée, échec précédent,
/// dossier protégé) : simple information dans le bandeau.
/// dossier protégé, « Plus tard ») : simple information dans le bandeau, en avertissement pour une
/// version prioritaire.
/// </summary>
private async Task NotifyLauncherUpdateAsync()
{
@@ -907,7 +908,13 @@ public partial class App : Application, ILauncherRestartHost
{
await Task.Delay(TimeSpan.FromSeconds(4));
var check = _updates!.SkippedUpdate ?? await _updates.CheckAsync();
if (check.IsAvailable)
if (check.IsPriority)
// Correctif critique non installé au démarrage (« Plus tard », échec précédent, dossier protégé…)
_launcher?.SelectedGame?.ShowNotice("Mise à jour importante du launcher",
$"La version {check.AvailableDisplay} corrige un problème important et n'est pas encore installée : " +
"Paramètres du launcher (roue crantée) › Vérifier les mises à jour.",
DialogKind.Warning);
else if (check.IsAvailable)
_launcher?.SelectedGame?.ShowNotice("Nouvelle version du launcher",
$"La version {check.AvailableDisplay} est disponible : Paramètres du launcher (roue crantée) › Vérifier les mises à jour.",
DialogKind.Info);
+1 -1
View File
@@ -17,7 +17,7 @@
(1.0.0 → 1.0.1 → 1.1.0…). Les launchers des joueurs ne se mettent à jour que vers
une version STRICTEMENT supérieure à la leur.
-->
<Version>3.3.1</Version>
<Version>3.4.0</Version>
<Authors>Lanisys</Authors>
<Company>LanisysGames</Company>
<Copyright>© LanisysGames</Copyright>
@@ -128,12 +128,15 @@ public sealed class LauncherUpdateService : ILauncherUpdateService
/// <summary>
/// Au démarrage, avant l'affichage de la fenêtre principale : recherche rapide et, si une
/// nouvelle version existe et que la mise à jour automatique est activée, installation.
/// Mise à jour automatique désactivée : la recherche a lieu quand même, et seule une version
/// PRIORITAIRE (correctif critique, information signée) est installée. Le réglage du joueur
/// (self-update.json) n'est jamais modifié : il reste désactivé pour les versions suivantes.
/// Renvoie vrai si le launcher redémarre (l'appelant doit alors s'arrêter là).
/// </summary>
public async Task<bool> RunStartupCheckAsync()
{
ExecutableSwapper.CleanupLeftovers(TargetPath, CurrentVersionValue);
if (_client is null || !AutoUpdate || !IsSingleFileBuild) return false;
if (_client is null || !IsSingleFileBuild) return false;
LauncherUpdateCheck check;
try
@@ -148,6 +151,18 @@ public sealed class LauncherUpdateService : ILauncherUpdateService
}
if (!check.IsAvailable) return false;
if (!AutoUpdate && !check.IsPriority)
{
// Option désactivée, version normale : simple information dans le bandeau (sans seconde recherche).
SkippedUpdate = check;
return false;
}
if (check.IsPriority)
Log.Warn($"Launcher {check.AvailableDisplay} : mise à jour PRIORITAIRE (correctif publié en {check.Release!.PriorityVersion})" +
(AutoUpdate ? "." : ", installée bien que la mise à jour automatique soit désactivée (réglage conservé)."));
// Échec précédent : même prioritaire, pas de nouvelle tentative automatique (une version qui ne démarre pas
// sur ce PC imposerait 60 s d'attente et un retour arrière à chaque lancement). Bandeau + bouton manuel.
if (string.Equals(_settings.FailedVersion, check.AvailableDisplay, StringComparison.OrdinalIgnoreCase))
{
Log.Warn($"Launcher {check.AvailableDisplay} : installation automatique ignorée (échec précédent). " +
@@ -167,7 +182,10 @@ public sealed class LauncherUpdateService : ILauncherUpdateService
SkippedUpdate = check;
return false;
}
return await InstallAsync(check, automatic: true);
if (await InstallAsync(check, automatic: true)) return true;
// Version prioritaire non installée (« Plus tard », erreur) : rappel dans le bandeau une fois la fenêtre affichée.
if (check.IsPriority) SkippedUpdate = check;
return false;
}
// ------------------------------------------------------------------ Installation
@@ -265,10 +283,17 @@ public sealed class LauncherUpdateService : ILauncherUpdateService
Log.Error("Mise à jour du launcher impossible", ex);
// Au démarrage, une erreur persistante (fichier absent du serveur, antivirus…) ne doit
// pas réapparaître à chaque lancement : cette version ne sera plus installée automatiquement.
if (automatic) RememberFailure(update);
vm.SetError(FriendlyMessage(ex) + (automatic
? "\n\nVous pourrez réessayer depuis Paramètres du launcher › Vérifier les mises à jour."
: ""));
// Exception : une version PRIORITAIRE (correctif critique) est retentée au prochain démarrage
// (erreur de téléchargement le plus souvent passagère). Seul un échec de démarrage de la nouvelle
// version (plus haut, après retour arrière) l'écarte des installations automatiques.
var retryAtNextStart = automatic && update.IsPriority;
if (automatic && !retryAtNextStart) RememberFailure(update);
vm.SetError(FriendlyMessage(ex) + (retryAtNextStart
? "\n\nCette mise à jour est importante : elle sera retentée au prochain démarrage du launcher " +
"(ou dès maintenant depuis Paramètres du launcher › Vérifier les mises à jour)."
: automatic
? "\n\nVous pourrez réessayer depuis Paramètres du launcher › Vérifier les mises à jour."
: ""));
}
// Erreur : la fenêtre reste ouverte jusqu'à ce que l'utilisateur la ferme.
@@ -116,9 +116,19 @@ public sealed class LauncherSettingsViewModel : SettingsViewModelBase
if (check.IsAvailable)
{
SetAvailable(check);
LauncherStatusKind = DialogKind.Info;
var title = string.IsNullOrWhiteSpace(check.Release?.Title) ? "" : $" — {check.Release!.Title}";
LauncherStatus = $"Nouvelle version {check.AvailableDisplay} disponible{title} (actuelle : {check.CurrentDisplay}).";
if (check.IsPriority)
{
// Correctif critique non installé au démarrage (annulé, échec, dossier protégé…)
LauncherStatusKind = DialogKind.Warning;
LauncherStatus = $"Mise à jour importante {check.AvailableDisplay} disponible{title} (actuelle : {check.CurrentDisplay}). " +
"Installez-la dès que possible.";
}
else
{
LauncherStatusKind = DialogKind.Info;
LauncherStatus = $"Nouvelle version {check.AvailableDisplay} disponible{title} (actuelle : {check.CurrentDisplay}).";
}
}
else
{
@@ -21,13 +21,19 @@ public sealed class LauncherUpdateViewModel : ObservableObject
public LauncherUpdateViewModel(LauncherUpdateCheck update, bool automatic)
{
Automatic = automatic;
IsPriority = update.IsPriority;
FromVersion = update.CurrentDisplay;
ToVersion = update.AvailableDisplay;
// Notes de toutes les versions publiées depuis la version installée (format enrichi)
Changelog = update.Release?.GetChangelogSince(update.CurrentVersion) ?? Array.Empty<ChangelogEntry>();
Subtitle = automatic
? "Une nouvelle version est disponible. Elle s'installe automatiquement, le launcher redémarrera tout seul."
: "Installation de la nouvelle version. Le launcher redémarrera tout seul.";
Subtitle = (automatic, IsPriority) switch
{
(true, true) => "Mise à jour importante : elle s'installe automatiquement, même si la mise à jour automatique " +
"du launcher est désactivée (votre réglage ne change pas). Le launcher redémarrera tout seul.",
(true, false) => "Une nouvelle version est disponible. Elle s'installe automatiquement, le launcher redémarrera tout seul.",
(false, true) => "Installation d'une mise à jour importante. Le launcher redémarrera tout seul.",
_ => "Installation de la nouvelle version. Le launcher redémarrera tout seul."
};
Progress = new Progress<OperationProgress>(p =>
{
@@ -44,6 +50,9 @@ public sealed class LauncherUpdateViewModel : ObservableObject
/// <summary>Recherche au démarrage (vrai) ou demandée dans les Paramètres du launcher (faux).</summary>
public bool Automatic { get; }
/// <summary>Mise à jour prioritaire (correctif critique, information signée) : pastille « Prioritaire ».</summary>
public bool IsPriority { get; }
public string FromVersion { get; }
public string ToVersion { get; }
public string Subtitle { get; }
@@ -76,6 +76,16 @@
<TextBlock Text="{Binding ToVersion}" FontSize="13" FontWeight="Bold" VerticalAlignment="Center" />
</StackPanel>
</Border>
<!-- Mise à jour prioritaire (correctif critique) : installée même si l'option automatique est désactivée -->
<Border Style="{StaticResource VersionChipStyle}" Margin="10,0,0,0" BorderBrush="{DynamicResource WarningBrush}"
ToolTip="Correctif important : installé même si la mise à jour automatique du launcher est désactivée. Votre réglage ne change pas."
Visibility="{Binding IsPriority, Converter={StaticResource BoolToVisibility}}">
<StackPanel Orientation="Horizontal">
<Path Data="{StaticResource Icon.Alert}" Fill="{DynamicResource WarningBrush}"
Width="14" Height="14" Stretch="Uniform" VerticalAlignment="Center" Margin="0,0,7,0" />
<TextBlock Text="Prioritaire" FontSize="12.5" FontWeight="SemiBold" VerticalAlignment="Center" />
</StackPanel>
</Border>
</StackPanel>
<!-- Nouveautés : même présentation que les notes du jeu (Themes/Changelog.xaml) -->
@@ -0,0 +1,139 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using GameLauncher.Core.SelfUpdate;
using GameLauncher.Core.Utils;
namespace GameLauncher.Tests.Signing;
/// <summary>
/// Version PRIORITAIRE du launcher (correctif critique) : installée au démarrage même si le joueur a désactivé la mise
/// à jour automatique. Information signée, reprise d'une publication à l'autre, sans effet sur les launchers à jour.
/// </summary>
public class LauncherPriorityTests
{
private static LauncherRelease Release(string version, string? priority = null) => new()
{
Product = "GameLauncher",
Version = version,
File = $"GameLauncher-{version}.exe",
Size = 1234,
Sha256 = new string('a', 64),
PublishedAt = DateTimeOffset.UnixEpoch,
PriorityVersion = priority
};
private static (string Envelope, UpdateSignature.TrustedKey Key) Sign(LauncherRelease release, ECDsa key)
{
var payload = JsonSerializer.SerializeToUtf8Bytes(release, JsonDefaults.Options);
var envelope = UpdateSignature.CreateEnvelope(payload, key);
var spki = key.ExportSubjectPublicKeyInfo();
return (envelope, new UpdateSignature.TrustedKey(UpdateSignature.ComputeKeyId(spki), spki));
}
private static LauncherUpdateCheck Check(string current, LauncherRelease? release) =>
new(VersionUtil.Parse(current), release, release is null ? null : new Uri("https://jeux.exemple.test/launcher/" + release.File));
[Fact]
public void The_priority_version_is_part_of_the_signed_content()
{
using var key = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var (envelope, trusted) = Sign(Release("3.4.0", priority: "3.4.0"), key);
var read = UpdateSignature.ReadRelease(envelope, new[] { trusted });
Assert.Equal("3.4.0", read.PriorityVersion);
Assert.True(read.IsPriorityRelease);
// Serveur compromis : retirer (ou ajouter) la priorité dans le payload casse la signature.
var json = JsonSerializer.Deserialize<SignedEnvelope>(envelope, JsonDefaults.Options)!;
var tampered = Encoding.UTF8.GetString(Convert.FromBase64String(json.Payload)).Replace("\"3.4.0\"", "\"3.3.9\"");
json.Payload = Convert.ToBase64String(Encoding.UTF8.GetBytes(tampered));
var forged = JsonSerializer.Serialize(json, JsonDefaults.Options);
Assert.Throws<UpdateSecurityException>(() => UpdateSignature.ReadRelease(forged, new[] { trusted }));
}
[Fact]
public void A_release_without_priority_stays_compatible_with_older_launchers()
{
using var key = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var release = Release("3.4.0");
var payload = Encoding.UTF8.GetString(JsonSerializer.SerializeToUtf8Bytes(release, JsonDefaults.Options));
Assert.DoesNotContain("priorityVersion", payload, StringComparison.OrdinalIgnoreCase); // champ absent : rien ne change
var (envelope, trusted) = Sign(release, key);
var read = UpdateSignature.ReadRelease(envelope, new[] { trusted });
Assert.Null(read.PriorityVersion);
Assert.False(read.IsPriorityRelease);
Assert.False(Check("3.3.1", read).IsPriority);
Assert.True(Check("3.3.1", read).IsAvailable);
}
[Theory]
[InlineData("3.3.1", true)] // plus ancien que le correctif : mise à jour imposée
[InlineData("3.3.9", true)]
[InlineData("3.4.0", false)] // correctif déjà installé : réglage du joueur respecté pour 3.5.0
[InlineData("3.4.2", false)]
public void Only_launchers_older_than_the_priority_version_are_forced(string current, bool forced)
{
var release = Release("3.5.0", priority: "3.4.0"); // 3.4.0 prioritaire, puis 3.5.0 normale
Assert.Equal(forced, release.IsPriorityFor(VersionUtil.Parse(current)));
Assert.Equal(forced, Check(current, release).IsPriority);
Assert.False(release.IsPriorityRelease);
}
[Fact]
public void No_update_available_means_no_priority()
{
Assert.False(Check("3.4.0", null).IsPriority);
Assert.False(new LauncherUpdateCheck(VersionUtil.Parse("3.3.0"), Release("3.4.0", "3.4.0"), null).IsPriority);
}
[Theory]
[InlineData("3.6.0")] // supérieure à la version publiée
[InlineData("pas-une-version")]
public void An_inconsistent_priority_version_is_refused(string priority)
{
var release = Release("3.5.0", priority);
Assert.Throws<UpdateSecurityException>(release.Validate);
}
[Fact]
public void A_blank_priority_version_means_none()
{
var release = Release("3.5.0", " ");
release.Validate();
Assert.Null(release.PriorityVersion);
}
[Fact]
public void The_publisher_marks_carries_over_or_clears_the_priority()
{
var v350 = VersionUtil.Parse("3.5.0");
var previousPriority = Release("3.4.0", priority: "3.4.0");
var previousNormal = Release("3.4.0");
// --priority : la version publiée elle-même
Assert.Equal("3.5.0", LauncherRelease.ResolvePriorityVersion(v350, markPriority: true, clearPriority: false, previousNormal));
// version normale après un correctif critique : priorité reprise (les launchers < 3.4.0 restent forcés)
Assert.Equal("3.4.0", LauncherRelease.ResolvePriorityVersion(v350, false, false, previousPriority));
// --clear-priority : plus aucune version imposée
Assert.Null(LauncherRelease.ResolvePriorityVersion(v350, false, true, previousPriority));
// rien à reprendre
Assert.Null(LauncherRelease.ResolvePriorityVersion(v350, false, false, previousNormal));
Assert.Null(LauncherRelease.ResolvePriorityVersion(v350, false, false, null));
// priorité précédente supérieure à la version publiée (--force vers une version inférieure) : non reprise
Assert.Null(LauncherRelease.ResolvePriorityVersion(VersionUtil.Parse("3.3.5"), false, false, previousPriority));
// options contradictoires
Assert.Throws<ArgumentException>(() => LauncherRelease.ResolvePriorityVersion(v350, true, true, null));
}
[Fact]
public void The_player_setting_file_has_no_priority_field()
{
// Effet ponctuel : la priorité vient UNIQUEMENT de la description signée, jamais de self-update.json.
var settings = new SelfUpdateSettings { AutoUpdate = false };
var json = JsonSerializer.Serialize(settings, JsonDefaults.Options);
Assert.DoesNotContain("priority", json, StringComparison.OrdinalIgnoreCase);
Assert.False(JsonSerializer.Deserialize<SelfUpdateSettings>(json, JsonDefaults.Options)!.AutoUpdate);
}
}
+68 -11
View File
@@ -20,6 +20,7 @@ namespace LauncherPublisher;
/// --config src\GameLauncher\launcher.json --output "D:\Publish\launcher"
/// [--version 1.2.0] [--notes notes-launcher.txt]
/// [--public-key src\GameLauncher\Security\update-signing.pub]
/// [--priority | --clear-priority] [--yes]
/// LauncherPublisher verify --manifest "D:\Publish\launcher\launcher-update.json"
/// --public-key src\GameLauncher\Security\update-signing.pub [--exe fichier.exe]
///
@@ -49,6 +50,11 @@ public static class Program
default: return Usage();
}
}
catch (OperationCanceledException ex)
{
Warn(ex.Message);
return 1;
}
catch (Exception ex)
{
Error($"Erreur : {ex.Message}");
@@ -136,6 +142,22 @@ public static class Program
@"Augmentez <Version> dans src\GameLauncher\GameLauncher.csproj, recompilez le launcher, puis relancez. (--force pour passer outre)");
}
// Version PRIORITAIRE (correctif critique) : installée au démarrage même chez les joueurs qui ont désactivé la
// mise à jour automatique (leur réglage n'est pas modifié). Confirmation demandée : rien n'est encore écrit.
var markPriority = o.Has("--priority");
var clearPriority = o.Has("--clear-priority");
if (markPriority && clearPriority)
throw new ArgumentException("--priority et --clear-priority sont incompatibles.");
if (markPriority && !o.Has("--yes"))
{
Warn($"Version PRIORITAIRE : le launcher {display} sera installé au démarrage MÊME chez les joueurs qui ont désactivé " +
"la mise à jour automatique (leur réglage n'est pas modifié). À réserver aux correctifs critiques (faille de sécurité…).");
Console.Write("Publier cette version comme prioritaire ? (o/N) ");
var answer = Console.ReadLine()?.Trim().ToLowerInvariant();
if (answer is not ("o" or "oui"))
throw new OperationCanceledException("Abandon : rien n'a été publié.");
}
using var key = LoadPrivateKey(keyPath);
var keyId = UpdateSignature.ComputeKeyId(key.ExportSubjectPublicKeyInfo());
@@ -172,10 +194,20 @@ public static class Program
ChangelogParser.Parse(await File.ReadAllLinesAsync(notesFile), entry);
}
// Historique : notes des versions déjà publiées (launcher-update.json précédent, signé par cette clé)
var history = ReadHistory(previousJson, key, keyId, display);
// launcher-update.json précédent, signé par cette clé : historique des notes et version prioritaire repris
var previousRelease = ReadPreviousRelease(previousJson, key, keyId);
var history = ReadHistory(previousRelease, display);
var changelog = ChangelogLimits.Normalize(new[] { entry }.Concat(history));
// Version prioritaire : celle-ci (--priority), aucune (--clear-priority), sinon celle déjà publiée
var priorityVersion = LauncherRelease.ResolvePriorityVersion(version, markPriority, clearPriority, previousRelease);
if (previousRelease is null && previousJson is not null && !markPriority && !clearPriority &&
TryReadUnverified(previousJson)?.PriorityVersion is { Length: > 0 } unverifiedPriority)
Warn($"Version prioritaire {unverifiedPriority} du launcher-update.json précédent NON reprise (signature non vérifiée) : " +
"relancez avec --priority si cette version doit rester imposée.");
if (clearPriority && previousRelease?.PriorityVersion is { Length: > 0 } removedPriority)
Warn($"Version prioritaire {removedPriority} retirée : seuls les joueurs ayant activé la mise à jour automatique recevront cette version.");
var release = new LauncherRelease
{
Product = product,
@@ -187,7 +219,8 @@ public static class Program
// Format historique, lu par les launchers antérieurs à 1.1.3
Title = entry.Title,
Notes = entry.Changes.ToList(),
Changelog = changelog
Changelog = changelog,
PriorityVersion = priorityVersion
};
release.Validate();
@@ -219,6 +252,7 @@ public static class Program
? " Notes : aucune (--notes non fourni ou fichier vide)"
: $" Notes : « {current.Title ?? "sans titre"} », {current.Sections.Count} section(s), " +
$"{current.Sections.Sum(x => x.Items.Count)} élément(s) ; historique : {release.Changelog.Count - 1} version(s)");
Console.WriteLine($" Priorité : {DescribePriority(release)}");
Console.WriteLine($" Produit : {product}");
Console.WriteLine($" Clé : {keyId}");
Console.WriteLine($" SHA-256 : {sha256}");
@@ -245,6 +279,7 @@ public static class Program
Console.WriteLine($" Version : {release.Version} ({release.PublishedAt:yyyy-MM-dd HH:mm})");
Console.WriteLine($" Fichier : {release.File} ({Format.Bytes(release.Size)})");
Console.WriteLine($" SHA-256 : {release.Sha256}");
Console.WriteLine($" Priorité: {DescribePriority(release)}");
var exe = o.Get("--exe");
if (exe is null) return true;
@@ -295,25 +330,30 @@ public static class Program
}
/// <summary>
/// Notes des versions déjà publiées, reprises du launcher-update.json précédent — uniquement
/// si sa signature est valide pour la clé utilisée (un fichier modifié à la main est ignoré).
/// launcher-update.json précédent — uniquement si sa signature est valide pour la clé utilisée (un fichier
/// modifié à la main est ignoré) : son historique des notes et sa version prioritaire sont repris.
/// </summary>
private static IReadOnlyList<ChangelogEntry> ReadHistory(string? previousJson, ECDsa key, string keyId, string version)
private static LauncherRelease? ReadPreviousRelease(string? previousJson, ECDsa key, string keyId)
{
if (previousJson is null) return Array.Empty<ChangelogEntry>();
LauncherRelease previous;
if (previousJson is null) return null;
try
{
previous = UpdateSignature.ReadRelease(previousJson, new[]
return UpdateSignature.ReadRelease(previousJson, new[]
{
new UpdateSignature.TrustedKey(keyId, key.ExportSubjectPublicKeyInfo())
});
}
catch (UpdateSecurityException ex)
{
Warn($"Historique des notes ignoré ({ex.Message}).");
return Array.Empty<ChangelogEntry>();
Warn($"launcher-update.json précédent ignoré ({ex.Message}) : historique des notes et version prioritaire non repris.");
return null;
}
}
/// <summary>Notes des versions déjà publiées (publication précédente vérifiée), sans la version en cours.</summary>
private static IReadOnlyList<ChangelogEntry> ReadHistory(LauncherRelease? previous, string version)
{
if (previous is null) return Array.Empty<ChangelogEntry>();
// Version publiée avant le format enrichi : son titre et sa liste deviennent une entrée
var entries = previous.Changelog.Count > 0
@@ -322,6 +362,16 @@ public static class Program
return entries.Where(e => !string.Equals(e.Version, version, StringComparison.OrdinalIgnoreCase)).ToList();
}
/// <summary>Ligne « Priorité » affichée après publish et verify.</summary>
private static string DescribePriority(LauncherRelease release)
{
if (!release.TryGetPriorityVersion(out var priority)) return "normale (installée selon le réglage de chaque joueur)";
var display = VersionUtil.Display(priority);
return release.IsPriorityRelease
? "PRIORITAIRE — installée au démarrage même si la mise à jour automatique est désactivée (réglage du joueur conservé)"
: $"normale, version prioritaire {display} reprise — les launchers antérieurs à {display} se mettent à jour quel que soit leur réglage";
}
private static LauncherRelease? TryReadUnverified(string envelopeJson)
{
try
@@ -509,7 +559,14 @@ public static class Program
(--config <launcher.json> | --product <launcherId>)
[--version x.y.z] [--notes <notes.txt>] [--title <texte>]
[--public-key <update-signing.pub>] [--force]
[--priority | --clear-priority] [--yes]
Signe la nouvelle version et prépare le dossier à envoyer sur le serveur.
--priority : version PRIORITAIRE (correctif critique), installée au démarrage même
chez les joueurs qui ont désactivé la mise à jour automatique (leur
réglage n'est pas modifié). Confirmation demandée, sauf --yes.
Reprise par les publications suivantes : un launcher plus ancien que
cette version se met à jour quoi qu'il arrive.
--clear-priority : ne reprend pas la version prioritaire déjà publiée.
verify --manifest <launcher-update.json | https://…> --public-key <update-signing.pub> [--exe <fichier>]
Vérifie la signature (et l'exécutable).
+2 -1
View File
@@ -36,7 +36,8 @@ Les positions du rapport sont en unités WPF (multiplier par `--scale` pour les
`main-maintenance-plain` sans précision, jeu non installé) ; `main-multi` : plusieurs jeux (exemple
`launcher.multi-jeux.example.json`).
* **Fenêtres** : paramètres du jeu et du launcher, notes de version agrandies, choix du dossier
d'installation, mise à jour du launcher (téléchargement, version vérifiée, erreur), boîtes de dialogue
d'installation, mise à jour du launcher (téléchargement, version vérifiée, version prioritaire
`launcher-update-priority`, erreur), boîtes de dialogue
(information, succès, question, avertissement, erreur, action irréversible avec option), menu de la zone
de notification.
* **Fenêtre « Aide »** (`feedback-*`, bouton de la barre de titre) : choix du type de demande, avis noté,
+16
View File
@@ -137,6 +137,7 @@ internal sealed class PlayerScenes : ISceneSet
yield return new SceneInfo("install-dialog" + suffix, "Installation : choix du dossier");
yield return new SceneInfo("launcher-update" + suffix, "Mise à jour du launcher · téléchargement");
yield return new SceneInfo("launcher-update-verified" + suffix, "Mise à jour du launcher · version vérifiée");
yield return new SceneInfo("launcher-update-priority" + suffix, "Mise à jour du launcher · version prioritaire");
yield return new SceneInfo("launcher-update-error" + suffix, "Mise à jour du launcher · erreur");
foreach (var (id, title, _) in Dialogs) yield return new SceneInfo(id + suffix, title);
yield return new SceneInfo("tray-menu" + suffix, "Menu de la zone de notification");
@@ -290,6 +291,21 @@ internal sealed class PlayerScenes : ISceneSet
return Task.FromResult<Window>(new LauncherUpdateWindow { DataContext = viewModel });
}, CloseUpdateWindowAsync);
// Version prioritaire (correctif critique) : pastille « Prioritaire » et texte dédié
var priorityRelease = SampleData.CreateLauncherRelease(_fixture.CurrentVersion);
priorityRelease.PriorityVersion = priorityRelease.Version;
var priorityCheck = new LauncherUpdateCheck(_fixture.CurrentVersion, priorityRelease,
new Uri("https://jeux.exemple.test/launcher/GameLauncher.exe"));
await capture.ShootAsync("launcher-update-priority" + suffix, "Mise à jour du launcher · version prioritaire", () =>
{
_fixture.ApplyTheme(theme);
var viewModel = new LauncherUpdateViewModel(priorityCheck, automatic: true);
viewModel.Progress.Report(new OperationProgress("Téléchargement de la nouvelle version",
$"{Format.Bytes(25_800_000)} / {Format.Bytes(priorityRelease.Size)} • {Format.Speed(6_300_000)} • " +
$"{Format.Duration(TimeSpan.FromSeconds(2))} restantes", 67.5));
return Task.FromResult<Window>(new LauncherUpdateWindow { DataContext = viewModel });
}, CloseUpdateWindowAsync);
await capture.ShootAsync("launcher-update-error" + suffix, "Mise à jour du launcher · erreur", () =>
{
_fixture.ApplyTheme(theme);
+38 -52
View File
@@ -1,9 +1,9 @@
# Launcher 3.3.1
# Launcher 3.4.0
LAUNCHER - NOTES DE MISE À JOUR
Correctif du mode maintenance - Mise à jour du 10 novembre 2026
==================================================================
Mises à jour prioritaires du launcher - Mise à jour du 10 novembre 2026
=========================================================================
Un launcher resté ouvert pendant la re-signature du trousseau ne voyait pas la maintenance activée ensuite : chaque relecture (toutes les 2 minutes) la refusait jusqu'au redémarrage. Il prend désormais le nouveau trousseau en compte tout seul, et le relit aussi régulièrement quand il reste ouvert longtemps.
Une version du launcher peut désormais être publiée comme prioritaire, pour un correctif critique (faille de sécurité…) : elle s'installe au démarrage même si vous avez désactivé la mise à jour automatique du launcher. Votre réglage n'est pas modifié : les versions normales suivantes ne s'installeront toujours pas toutes seules.
Aucune action sur le serveur. Le launcher se met à jour automatiquement.
@@ -11,58 +11,40 @@ Aucune action sur le serveur. Le launcher se met à jour automatiquement.
------------------------------------------------------------------
EN BREF
------------------------------------------------------------------
- CORRIGÉ : mode maintenance activé après la re-signature du
trousseau non vu par un launcher déjà ouvert (redémarrage
nécessaire en 3.3.0).
- AMÉLIORÉ : trousseau retéléchargé quand un contenu est signé par
une clé connue mais pas encore autorisée pour ce contenu.
- SÉCURITÉ : trousseau relu au plus toutes les 30 minutes : un
appareil retiré n'est plus accepté par un launcher laissé ouvert.
- QUALITÉ : 3 nouveaux tests automatisés (370 au total).
- NOUVEAU : version prioritaire, installée au démarrage même avec
la mise à jour automatique du launcher désactivée.
- NOUVEAU : pastille « Prioritaire » dans la fenêtre de mise à
jour, avertissement dans le bandeau et les paramètres tant
qu'elle n'est pas installée.
- SÉCURITÉ : l'indication « prioritaire » est signée, comme le
reste de la mise à jour.
- QUALITÉ : nouveaux tests automatisés.
------------------------------------------------------------------
CORRECTIF
VERSION PRIORITAIRE
------------------------------------------------------------------
- Cause : le launcher gardait en mémoire le trousseau lu au
démarrage. Il ne le retéléchargeait que pour une clé inconnue,
pas pour une clé connue qui vient de recevoir une autorisation
(« mode maintenance » ajoutée à l'étape 3 decies).
- Effet en 3.3.0 : « la clé … n'est pas autorisée à signer ce
contenu » dans le journal à chaque relecture, maintenance
ignorée jusqu'au redémarrage.
- Désormais : nouveau téléchargement du trousseau, au plus une
fois par minute ; la maintenance s'affiche à la relecture
suivante (2 minutes au plus).
- Concerne aussi les formulaires « Aide », le catalogue et les
versions des jeux signés par un appareil qui vient d'être
autorisé.
------------------------------------------------------------------
VÉRIFICATIONS PÉRIODIQUES DU LAUNCHER
------------------------------------------------------------------
- Mode maintenance : au démarrage (4 s au plus), puis toutes les
2 minutes (1 minute pendant une maintenance), et aussitôt après
une publication depuis le menu de ce PC.
- Versions des jeux (manifest) : au démarrage, puis toutes les
30 minutes (updateCheckIntervalMinutes).
- Trousseau : au démarrage ; après un échec réseau (1 minute) ;
clé inconnue ou non autorisée (1 minute au plus) ; au plus
toutes les 30 minutes (nouveau).
- Catalogue : au démarrage seulement.
- Mise à jour du launcher : au démarrage, et bouton « Rechercher »
des paramètres.
- Formulaires « Aide » : à l'ouverture de la fenêtre « Aide ».
- Votre réglage « Mettre à jour le launcher automatiquement »
n'est jamais modifié : effet ponctuel, le temps d'installer le
correctif.
- « Plus tard » reste possible : nouvelle tentative au prochain
démarrage du launcher.
- Une erreur de téléchargement est retentée au démarrage suivant.
- Si la nouvelle version ne démarre pas, l'ancienne est remise en
place et le bouton « Vérifier les mises à jour » permet de
réessayer.
- Mise à jour automatique désactivée : le launcher vérifie
désormais au démarrage si une version prioritaire existe.
------------------------------------------------------------------
SÉCURITÉ
------------------------------------------------------------------
- Chaque trousseau retéléchargé passe par les mêmes contrôles :
signature de la clé racine, numéro jamais en baisse, minimum
keyringMinSequence ; un ancien trousseau resservi est refusé.
- Serveur injoignable : dernier trousseau vérifié conservé.
- L'indication « prioritaire » fait partie du contenu signé : un
serveur compromis ne peut ni imposer une mise à jour, ni
retirer la priorité d'un correctif.
- Seule une version signée et strictement supérieure peut être
installée, comme avant.
------------------------------------------------------------------
@@ -74,11 +56,15 @@ SERVEUR
------------------------------------------------------------------
DÉVELOPPEMENT
------------------------------------------------------------------
- Core : ContentTrustService (KeyChangeRetryDelay, KeyringMaxAge,
KeyMayBeNewer).
- Tests : ContentTrustTests (2), MaintenanceServiceTests (1).
- Documentation : README, docs/ADMIN-ARCHITECTURE.md,
server/deploy/LISEZMOI.md (étapes 3 quinquies et 3 decies).
- publish-launcher-update.bat --priority (version prioritaire,
confirmation demandée) et --clear-priority (ne reprend pas la
priorité déjà publiée).
- LauncherPublisher : options --priority, --clear-priority, --yes ;
ligne « Priorité » dans publish et verify.
- Core : LauncherRelease.PriorityVersion (reprise d'une
publication à l'autre), LauncherUpdateCheck.IsPriority.
- UiSnapshot : scène launcher-update-priority.
- Tests : LauncherPriorityTests.
La sécurité des mises à jour du launcher ne change pas : la signature numérique et l'empreinte SHA-256 de chaque nouvelle version sont vérifiées avant toute installation.
+58
View File
@@ -7,8 +7,41 @@ REM 3. (optionnel) l'envoie sur le VPS avec WinSCP
REM
REM Avant de lancer : augmentez <Version> dans src\GameLauncher\GameLauncher.csproj
REM et ecrivez les nouveautes dans tools\notes-launcher.txt.
REM
REM Parametres (facultatifs, depuis une invite de commandes) :
REM publish-launcher-update.bat version normale (double-clic)
REM publish-launcher-update.bat --priority version PRIORITAIRE (correctif critique) :
REM installee au demarrage MEME chez les joueurs qui ont desactive la mise a jour
REM automatique du launcher. Leur reglage (self-update.json) n'est pas modifie.
REM Reprise par les publications suivantes : un launcher plus ancien que cette
REM version se met a jour quoi qu'il arrive. Confirmation demandee.
REM publish-launcher-update.bat --clear-priority ne reprend pas la version prioritaire
REM deja publiee (priorite publiee par erreur).
REM ---------------------------------------------------------------------------
setlocal
set PRIORITY=
:args
if "%~1"=="" goto :argsdone
if /i "%~1"=="--priority" goto :opt_priority
if /i "%~1"=="--clear-priority" goto :opt_clear
if /i "%~1"=="--help" goto :usage
if "%~1"=="/?" goto :usage
echo Parametre inconnu : %~1
goto :usage
:opt_priority
if defined PRIORITY goto :conflict
set PRIORITY=--priority
REM shift /1 : %0 (donc %~dp0) reste inchange
shift /1
goto :args
:opt_clear
if defined PRIORITY goto :conflict
set PRIORITY=--clear-priority
shift /1
goto :args
:argsdone
set ROOT=%~dp0..
set KEY=%USERPROFILE%\.launcher-keys\launcher-signing.key
set OUTPUT=C:\Users\solac\Documents\Game Laucher\GameLauncher_V2\Publish\Updates
@@ -16,6 +49,18 @@ REM Script WinSCP (laisser vide pour envoyer les fichiers a la main) :
set WINSCP_SCRIPT=C:\Users\solac\Documents\Game Laucher\GameLauncher_V2\Publish\publier-launcher-update.txt
set WINSCP="C:\Program Files (x86)\WinSCP\WinSCP.com"
if /i "%PRIORITY%"=="--priority" (
echo ***************************************************************************
echo Version PRIORITAIRE : installee au demarrage meme chez les joueurs qui ont
echo desactive la mise a jour automatique du launcher. Leur reglage ne change pas.
echo ***************************************************************************
echo.
)
if /i "%PRIORITY%"=="--clear-priority" (
echo Version normale : la version prioritaire deja publiee n'est PAS reprise.
echo.
)
echo === 1/3 Compilation du launcher ===
dotnet publish "%ROOT%\src\GameLauncher\GameLauncher.csproj" -c Release -p:PublishProfile=win-x64
if errorlevel 1 goto :error
@@ -28,6 +73,7 @@ dotnet run --project "%ROOT%\tools\LauncherPublisher\LauncherPublisher.csproj" -
--config "%ROOT%\src\GameLauncher\launcher.json" ^
--public-key "%ROOT%\src\GameLauncher\Security\update-signing.pub" ^
--notes "%~dp0notes-launcher.txt" ^
%PRIORITY% ^
--output "%OUTPUT%"
if errorlevel 1 goto :error
@@ -52,3 +98,15 @@ echo.
echo La publication a echoue.
pause
exit /b 1
:conflict
echo --priority et --clear-priority sont incompatibles.
exit /b 1
:usage
echo.
echo Utilisation : publish-launcher-update.bat [--priority ^| --clear-priority]
echo --priority version prioritaire (correctif critique) : installee meme si le
echo joueur a desactive la mise a jour automatique (reglage conserve)
echo --clear-priority ne reprend pas la version prioritaire deja publiee
exit /b 1