orchestration: target windows platform + prune stale files
Two fixes addressing review feedback on the previous avalonia-gui PR round.
1. Platform filter was using host OS (`IsWindows() ? "windows" : "linux"`),
which dropped all `platform=windows` manifest entries whenever the
launcher ran on Linux. Since the client is always a Windows PE binary
launched through Wine on Linux hosts, the target platform we apply is
always `"windows"` regardless of host. Before this, the orchestrator
silently skipped Metin2.exe and the python314 / openssl / mingw runtime
DLLs on Linux, leaving an unbootable install dir.
2. Added `PruneStaleFiles` that walks clientRoot after every successful
update (both the normal apply path and the already-up-to-date
short-circuit) and deletes any file not in the manifest. Without it,
switching from a dirty release to a leaner one — e.g. dropping legacy
.pck after a .m2p migration — left orphaned files on disk and inflated
the install dir.
The keep set is `manifest.files ∪ {manifest.launcher.path}`. Per the
manifest spec in `m2dev-client/docs/update-manifest.md`, the top-level
launcher entry is privileged and never listed in files; it must
survive prune so a correctly-authored manifest does not delete the
updater itself (caught in Jakub's review of the previous round).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -116,7 +116,12 @@ public sealed class UpdateOrchestrator
|
||||
// 3. diff
|
||||
progress.Report(new UpdateProgress { State = LauncherState.Diffing, Manifest = loaded });
|
||||
|
||||
var platform = OperatingSystem.IsWindows() ? "windows" : "linux";
|
||||
// The client is always a Windows PE binary launched through Wine on
|
||||
// Linux hosts, so the manifest "platform" we apply is always "windows"
|
||||
// regardless of host OS. Using host OS would drop Metin2.exe and the
|
||||
// python314 / openssl / mingw runtime DLLs whenever the launcher runs
|
||||
// on Linux, leaving an unbootable install dir.
|
||||
const string platform = "windows";
|
||||
var applicable = format.FilterApplicable(manifest, platform);
|
||||
|
||||
var needed = new List<(ManifestFile File, string FinalPath, string StagingPath)>();
|
||||
@@ -162,6 +167,8 @@ public sealed class UpdateOrchestrator
|
||||
try { format.OnApplied(_clientRoot, loaded, noopOutcome); }
|
||||
catch (Exception ex) { Log.Warn("format OnApplied (no-op path) failed: " + ex.Message); }
|
||||
|
||||
PruneStaleFiles(manifest);
|
||||
|
||||
progress.Report(new UpdateProgress { State = LauncherState.UpToDate, Percent = 100, Manifest = loaded });
|
||||
return new Result(true, LauncherState.UpToDate, loaded, format, noopOutcome.RuntimeKey);
|
||||
}
|
||||
@@ -260,8 +267,49 @@ public sealed class UpdateOrchestrator
|
||||
}
|
||||
}
|
||||
|
||||
PruneStaleFiles(manifest);
|
||||
|
||||
Log.Info($"update complete: {toApply.Count} files applied (format={format.Name})");
|
||||
progress.Report(new UpdateProgress { State = LauncherState.UpToDate, Percent = 100, Manifest = loaded });
|
||||
return new Result(true, LauncherState.UpToDate, loaded, format, outcome.RuntimeKey);
|
||||
}
|
||||
|
||||
// Walk clientRoot and delete any file not in the manifest. Skips the
|
||||
// .updates state dir. Called after both the normal apply path and the
|
||||
// already-up-to-date short-circuit so switching to a leaner release
|
||||
// does not leave orphaned files on disk.
|
||||
//
|
||||
// The keep set is files ∪ {manifest.launcher.path}. Per the manifest
|
||||
// spec (m2dev-client/docs/update-manifest.md), the top-level launcher
|
||||
// entry is privileged and is never listed in files; it must survive
|
||||
// prune so a correctly-authored manifest does not delete the updater.
|
||||
private void PruneStaleFiles(Manifest.Manifest manifest)
|
||||
{
|
||||
var keepRel = new HashSet<string>(
|
||||
manifest.Files.Select(f => f.Path.Replace('/', Path.DirectorySeparatorChar)),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
if (manifest.Launcher?.Path is { Length: > 0 } launcherPath)
|
||||
{
|
||||
keepRel.Add(launcherPath.Replace('/', Path.DirectorySeparatorChar));
|
||||
}
|
||||
int pruned = 0;
|
||||
try
|
||||
{
|
||||
var rootFull = Path.GetFullPath(_clientRoot);
|
||||
var stateDirFull = Path.GetFullPath(Path.Combine(_clientRoot, LauncherConfig.StateDirName));
|
||||
foreach (var path in Directory.EnumerateFiles(rootFull, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
if (path.StartsWith(stateDirFull, StringComparison.Ordinal)) continue;
|
||||
var rel = Path.GetRelativePath(rootFull, path);
|
||||
if (keepRel.Contains(rel)) continue;
|
||||
try { File.Delete(path); pruned++; }
|
||||
catch (Exception ex) { Log.Warn($"failed to prune {rel}: {ex.Message}"); }
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warn("prune walk failed: " + ex.Message);
|
||||
}
|
||||
if (pruned > 0) Log.Info($"pruned {pruned} stale files not in manifest");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user