Self-update: UAC-aware, de-elevated relaunch, user-mode installer
Three coordinated changes so the auto-update flow works on every
existing install location.
LauncherSelfUpdater.cs
----------------------
- Probe the target directory with a write/delete of a temp file. If
it fails (e.g. install in Program Files without admin), set
UseShellExecute=true + Verb=runas on the Updater process so UAC
prompts the user once for elevation. If the directory is writable
(user-mode install or portable layout), skip elevation entirely.
- Switched from ProcessStartInfo.ArgumentList to a quoted Arguments
string because Verb=runas requires UseShellExecute=true, which
ignores ArgumentList. Paths get explicit quotes to survive spaces.
Updater Program.cs
------------------
After the file swap, the relaunch was a direct Process.Start(target).
With UAC elevation that propagates admin to the new launcher, then to
its child PROSERVE_UE_5_5.exe — undesirable. Replace with
`explorer.exe "<target>"`: explorer is always running in the user's
normal token, opens the exe via shell, the new process inherits the
non-elevated session. Standard de-elevation trick.
installer/PSLauncher.iss
------------------------
Switch from system-wide install (Program Files, requires admin) to
per-user (DefaultDirName={localappdata}\Programs\..., PrivilegesRequired=
lowest). Auto-update then never needs UAC at all on fresh installs.
Existing installs in Program Files keep working thanks to the runas
fallback above.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
BIN
PSLauncher-0.6.0.exe
Normal file
BIN
PSLauncher-0.6.0.exe
Normal file
Binary file not shown.
@@ -28,7 +28,9 @@ AppPublisher={#MyAppPublisher}
|
|||||||
AppPublisherURL={#MyAppURL}
|
AppPublisherURL={#MyAppURL}
|
||||||
AppSupportURL={#MyAppURL}
|
AppSupportURL={#MyAppURL}
|
||||||
AppUpdatesURL={#MyAppURL}
|
AppUpdatesURL={#MyAppURL}
|
||||||
DefaultDirName={autopf}\{#MyAppPublisher}\{#MyAppShortName}
|
; Installation user-mode (pas d'admin requis) : le launcher pourra se mettre à jour
|
||||||
|
; tout seul via PSLauncher.Updater.exe sans déclencher de UAC.
|
||||||
|
DefaultDirName={localappdata}\Programs\{#MyAppPublisher}\{#MyAppShortName}
|
||||||
DefaultGroupName={#MyAppName}
|
DefaultGroupName={#MyAppName}
|
||||||
DisableProgramGroupPage=yes
|
DisableProgramGroupPage=yes
|
||||||
OutputDir=output
|
OutputDir=output
|
||||||
@@ -37,8 +39,9 @@ SetupIconFile=
|
|||||||
Compression=lzma2/ultra
|
Compression=lzma2/ultra
|
||||||
SolidCompression=yes
|
SolidCompression=yes
|
||||||
WizardStyle=modern
|
WizardStyle=modern
|
||||||
PrivilegesRequired=admin
|
PrivilegesRequired=lowest
|
||||||
PrivilegesRequiredOverridesAllowed=dialog
|
PrivilegesRequiredOverridesAllowed=dialog
|
||||||
|
UsedUserAreasWarning=no
|
||||||
ArchitecturesAllowed=x64
|
ArchitecturesAllowed=x64
|
||||||
ArchitecturesInstallIn64BitMode=x64
|
ArchitecturesInstallIn64BitMode=x64
|
||||||
UninstallDisplayName={#MyAppName}
|
UninstallDisplayName={#MyAppName}
|
||||||
|
|||||||
BIN
installer/output/PSLauncher-Setup-0.5.0.exe
Normal file
BIN
installer/output/PSLauncher-Setup-0.5.0.exe
Normal file
Binary file not shown.
@@ -84,23 +84,45 @@ public sealed class LauncherSelfUpdater : ILauncherSelfUpdater
|
|||||||
File.Move(downloadedPath, newPath);
|
File.Move(downloadedPath, newPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Lance l'updater détaché
|
// 5. Lance l'updater détaché. Si le target est dans un dossier protégé
|
||||||
|
// (Program Files, etc.), on ne pourra pas le remplacer sans admin :
|
||||||
|
// on demande l'élévation via ShellExecute + Verb=runas (UAC popup).
|
||||||
var pid = Environment.ProcessId;
|
var pid = Environment.ProcessId;
|
||||||
|
var needsElevation = !CanWriteTo(targetExe);
|
||||||
|
|
||||||
var psi = new ProcessStartInfo
|
var psi = new ProcessStartInfo
|
||||||
{
|
{
|
||||||
FileName = updaterPath,
|
FileName = updaterPath,
|
||||||
UseShellExecute = false,
|
UseShellExecute = needsElevation, // requis pour Verb=runas
|
||||||
CreateNoWindow = true,
|
CreateNoWindow = !needsElevation,
|
||||||
|
Verb = needsElevation ? "runas" : "",
|
||||||
|
Arguments = string.Join(' ', new[]
|
||||||
|
{
|
||||||
|
$"--target=\"{targetExe}\"",
|
||||||
|
$"--source=\"{newPath}\"",
|
||||||
|
$"--pid={pid}",
|
||||||
|
"--launch",
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
psi.ArgumentList.Add($"--target={targetExe}");
|
|
||||||
psi.ArgumentList.Add($"--source={newPath}");
|
|
||||||
psi.ArgumentList.Add($"--pid={pid}");
|
|
||||||
psi.ArgumentList.Add("--launch");
|
|
||||||
|
|
||||||
_logger.LogInformation("Spawning updater {Updater} (target={Target}, source={Source}, pid={Pid})",
|
_logger.LogInformation("Spawning updater {Updater} (target={Target}, source={Source}, pid={Pid}, elevate={Elevate})",
|
||||||
updaterPath, targetExe, newPath, pid);
|
updaterPath, targetExe, newPath, pid, needsElevation);
|
||||||
SysProcess.Start(psi);
|
SysProcess.Start(psi);
|
||||||
|
|
||||||
return true; // l'appelant doit Application.Current.Shutdown() après
|
return true; // l'appelant doit Application.Current.Shutdown() après
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Test rapide : peut-on écrire à côté du fichier cible (= dans son dossier) ?</summary>
|
||||||
|
private static bool CanWriteTo(string filePath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var dir = Path.GetDirectoryName(filePath)!;
|
||||||
|
var probe = Path.Combine(dir, ".pslauncher-write-test-" + Guid.NewGuid().ToString("N").Substring(0, 8));
|
||||||
|
File.WriteAllText(probe, "");
|
||||||
|
File.Delete(probe);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,16 +82,21 @@ internal static class Program
|
|||||||
return 4;
|
return 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Relance
|
// 5. Relance.
|
||||||
|
// Si on a été lancé en admin (Verb=runas via UAC), un Process.Start direct
|
||||||
|
// propagerait l'élévation au launcher et donc à tous ses sous-processus
|
||||||
|
// (PROSERVE_UE_5_5.exe), ce qu'on ne veut pas. On passe par explorer.exe
|
||||||
|
// qui est non-élévé : il ouvre l'exe dans la session utilisateur normale.
|
||||||
if (opts.Launch)
|
if (opts.Launch)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Process.Start(new ProcessStartInfo
|
Process.Start(new ProcessStartInfo
|
||||||
{
|
{
|
||||||
FileName = opts.Target,
|
FileName = "explorer.exe",
|
||||||
UseShellExecute = true,
|
Arguments = "\"" + opts.Target + "\"",
|
||||||
WorkingDirectory = Path.GetDirectoryName(opts.Target)!,
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
Reference in New Issue
Block a user