using System.Diagnostics;
namespace PSLauncher.Updater;
///
/// Mini exe qui remplace le launcher principal (qui ne peut pas s'auto-écraser pendant qu'il tourne).
///
/// Args attendus :
/// --target=<chemin_complet_de_PSLauncher.exe>
/// --source=<chemin_du_nouveau_PSLauncher.exe>
/// [--launch] : relance PSLauncher.exe une fois la copie faite
/// [--pid=<PID>] : attend que ce process se termine avant de copier
///
/// Workflow :
/// 1. Si --pid fourni, attend la sortie du process.
/// 2. Sinon, attend que le fichier target soit déverrouillé (poll 250 ms).
/// 3. Backup target → target.bak (au cas où).
/// 4. Copie source → target.
/// 5. Optionnellement relance target.
/// 6. Supprime source (le .new) puis target.bak après 30 s.
///
internal static class Program
{
[STAThread]
private static int Main(string[] args)
{
try
{
var opts = ParseArgs(args);
if (string.IsNullOrEmpty(opts.Target) || string.IsNullOrEmpty(opts.Source))
{
Console.Error.WriteLine("Usage: PSLauncher.Updater --target= --source= [--launch] [--pid=N]");
return 2;
}
// 1. Attendre la sortie du launcher principal
if (opts.Pid is int pid)
{
try
{
var proc = Process.GetProcessById(pid);
proc.WaitForExit(60_000);
}
catch { /* déjà mort, OK */ }
}
// 2. Attendre que le target soit déverrouillé
var deadline = DateTime.UtcNow.AddSeconds(60);
while (DateTime.UtcNow < deadline)
{
if (!File.Exists(opts.Target)) break;
try
{
using var fs = File.Open(opts.Target, FileMode.Open, FileAccess.ReadWrite, FileShare.None);
break;
}
catch (IOException)
{
Thread.Sleep(250);
}
}
// 3. Backup
var backup = opts.Target + ".bak";
try { if (File.Exists(backup)) File.Delete(backup); } catch { }
try { if (File.Exists(opts.Target)) File.Move(opts.Target, backup); } catch (Exception ex)
{
Console.Error.WriteLine($"Backup failed: {ex.Message}");
return 3;
}
// 4. Copy new file in place
try
{
File.Copy(opts.Source, opts.Target, overwrite: true);
}
catch (Exception ex)
{
Console.Error.WriteLine($"Copy failed: {ex.Message}");
// Tente la restauration
try { if (File.Exists(backup)) File.Move(backup, opts.Target, overwrite: true); } catch { }
return 4;
}
// 5. Relance
if (opts.Launch)
{
try
{
Process.Start(new ProcessStartInfo
{
FileName = opts.Target,
UseShellExecute = true,
WorkingDirectory = Path.GetDirectoryName(opts.Target)!,
});
}
catch (Exception ex)
{
Console.Error.WriteLine($"Relaunch failed: {ex.Message}");
}
}
// 6. Cleanup différé (le .new + .bak après 30s, en background détaché)
try { File.Delete(opts.Source); } catch { }
// .bak laissé sur place : si le nouveau exe plante au lancement, l'utilisateur
// peut renommer .bak → .exe pour récupérer l'ancien.
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Updater fatal: {ex}");
return 1;
}
}
private sealed class Options
{
public string? Target { get; set; }
public string? Source { get; set; }
public bool Launch { get; set; }
public int? Pid { get; set; }
}
private static Options ParseArgs(string[] args)
{
var o = new Options();
foreach (var a in args)
{
if (a.StartsWith("--target=", StringComparison.OrdinalIgnoreCase)) o.Target = a["--target=".Length..];
else if (a.StartsWith("--source=", StringComparison.OrdinalIgnoreCase)) o.Source = a["--source=".Length..];
else if (a.StartsWith("--pid=", StringComparison.OrdinalIgnoreCase)) o.Pid = int.TryParse(a["--pid=".Length..], out var p) ? p : null;
else if (a.Equals("--launch", StringComparison.OrdinalIgnoreCase)) o.Launch = true;
}
return o;
}
}