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; } }