Откат и .bak¶
Есть несколько слоёв защиты от испорченного update.rpf.
Слой 1: .preinstall snapshot¶
В самом начале install pipeline, до Smart Rebuild, делаем копию текущего update.rpf:
await Task.Run(() => File.Copy(updateRpf, rollbackPath, overwrite: true));
// rollbackPath = "<gta>/update/update.rpf.preinstall"
Это точный snapshot того что было до. Если на следующих шагах что-то крашит (Smart Rebuild упал на корруптном donor file, ArchiveFix отвалился, диск кончился) — у нас есть точная копия.
TryRollbackAsync:
private async Task TryRollbackAsync(string updateRpf, string rollbackPath)
{
try
{
if (File.Exists(rollbackPath))
{
await Task.Run(() => File.Copy(rollbackPath, updateRpf, overwrite: true));
Debug.WriteLine("[redux.install] rolled back to preinstall snapshot");
}
}
finally
{
try { if (File.Exists(rollbackPath)) File.Delete(rollbackPath); } catch { }
}
}
После успешного install rollback файл удаляется. Если процесс убит между snapshot и delete — .preinstall остаётся, его нужно вычистить вручную или при следующем install.
Слой 2: .bak внутри Smart Rebuild¶
В самом InjectSmartRebuild есть второй уровень защиты — atomic swap через три File.Move:
if (File.Exists(backupPath)) File.Delete(backupPath);
File.Move(sourceArchivePath, backupPath); // update.rpf → update.rpf.bak
File.Move(tempPath, sourceArchivePath); // update.rpf.hnt_temp → update.rpf
File.Delete(backupPath); // удаляем .bak
Между шагом 1 и 2 есть окно в несколько миллисекунд где update.rpf отсутствует физически — он переименован в .bak. Если процесс убит именно в этот момент (SIGKILL, BSOD, отключение света) — у юзера в <gta>/update/ лежит только update.rpf.bak.
GTA при запуске откажется грузить (нет файла) — но это легко исправимо если мы найдём этот висячий .bak.
Catch-блок в InjectSmartRebuild пытается откатить:
catch (Exception ex)
{
SetError($"Ошибка Rebuild: {ex.GetType().Name}: {ex.Message}");
try
{
if (File.Exists(tempPath)) File.Delete(tempPath);
if (File.Exists(backupPath) && !File.Exists(sourceArchivePath))
File.Move(backupPath, sourceArchivePath);
}
catch (Exception cleanupEx) { /* ... */ }
return false;
}
Но это работает только если процесс прошёл через catch. Если убили SIGKILL — catch не вызывается.
Слой 3: OrphanBackupRecovery на старте лаунчера¶
Чтобы покрыть SIGKILL-сценарий, на каждый запуск Miami Graphics сканируем GTA-папки на висячие .bak:
public static class OrphanBackupRecovery
{
public static void Run()
{
var locator = new HunterGraphics.Core.System.HardwareLocator();
var gtaPath = locator.FindGtaPath();
if (string.IsNullOrWhiteSpace(gtaPath) || !Directory.Exists(gtaPath))
return;
var probeDirs = new[]
{
Path.Combine(gtaPath, "update"),
Path.Combine(gtaPath, "update", "x64", "dlcpacks"),
Path.Combine(gtaPath, "x64", "audio", "sfx"),
Path.Combine(gtaPath, "mods", "update"),
Path.Combine(gtaPath, "mods", "update", "x64", "dlcpacks"),
Path.Combine(gtaPath, "mods", "x64", "audio", "sfx"),
};
int restored = 0, removed = 0;
foreach (var dir in probeDirs.Where(Directory.Exists))
{
foreach (var bakPath in Directory.EnumerateFiles(dir, "*.bak", SearchOption.TopDirectoryOnly))
{
var origPath = bakPath.Substring(0, bakPath.Length - 4); // отрезаем ".bak"
if (!File.Exists(origPath))
{
// Только .bak, .rpf нет → восстанавливаем
File.Move(bakPath, origPath);
restored++;
}
else
{
// .rpf И .bak оба → .bak это residual после crash на шаге 3
File.Delete(bakPath);
removed++;
}
}
// Также чистим .hnt_temp файлы — это temporary rebuild output
foreach (var tmpPath in Directory.EnumerateFiles(dir, "*.hnt_temp", SearchOption.TopDirectoryOnly))
File.Delete(tmpPath);
}
}
}
Вызывается из App.OnStartup:
protected override void OnStartup(StartupEventArgs e)
{
try { HunterGraphics.Shell.Services.MiamiPathMigration.EnsureMiamiLayout(); } catch {}
try { HunterGraphics.Shell.Services.OrphanBackupRecovery.Run(); } catch {}
// ... остальной startup ...
}
Это идемпотентно: запускается каждый startup, best-effort, log-only. Не валит лаунчер если что-то странное в GTA-папке.
Сценарии что покрыто¶
| Сценарий | Слой 1 (preinstall) | Слой 2 (catch) | Слой 3 (orphan-bak) |
|---|---|---|---|
| Smart Rebuild упал с exception | ✅ восстанавливает | ✅ откатывает .bak |
— |
| ArchiveFix вернул error | ✅ восстанавливает | — | — |
| Процесс убит между File.Move шагами 1 и 2 | — | — | ✅ восстанавливает .bak на старте |
| Процесс убит между File.Move 2 и 3 | — | — | ✅ удаляет residual .bak |
| BSOD во время Smart Rebuild | — | — | ✅ удаляет .hnt_temp |
| Power-loss до atomic swap | — | — | ✅ удаляет .hnt_temp (update.rpf не тронут) |
Power-loss после .preinstall создан но update.rpf целый |
manual cleanup | — | — |
Слой 1 пишется один раз перед началом, слой 2 только при обычном exception flow, слой 3 спасает катастрофические сценарии.
Если все три слоя не сработали (диск умер, гта-папка переименована) — финальный fallback это BackupService.RestoreFromCleanAsync. Юзер кликнет «Восстановить чистый» в UI, мы возьмём reference из локального кеша или CDN.
Что было до этих слоёв¶
В первых релизах был только слой 2 (.bak внутри SmartRebuild). Юзеры с нестабильным компом (особенно ноутбуки в энергосберегающем режиме) изредка ловили "GTA files corrupted" после установки мода. Потому что Windows может усыпить процесс или kill'нуть его за высокое потребление CPU.
Слой 3 (OrphanBackupRecovery) добавили после одного публичного инцидента — у RP-сервера случился наплыв юзеров, и сразу 8 человек написали что после установки redux'а GTA не запускается. Полчаса разбирались, оказалось у каждого .bak без .rpf в <gta>/update/. Восстановили вручную, на следующий день написали слой 3.
Слой 1 (.preinstall snapshot) — добавили ещё раньше, как только начали поддерживать customize (где сам install pipeline может упасть на этапе apply кастомного компонента, не на Smart Rebuild).