Skip to content

Commit 63097ab

Browse files
committed
Ensure more transactional unity version upgrade
1 parent 185d602 commit 63097ab

2 files changed

Lines changed: 77 additions & 22 deletions

File tree

‎higurashiInstaller.py‎

Lines changed: 62 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,24 @@ def listInvalidUIFiles(folder):
9090

9191
return invalidUIFileList
9292

93+
def copyFileIfSourceExistsAndDestDoesNot(sourcePath, destinationPath):
94+
#type: (str, str) -> None
95+
"""
96+
Try to copy + move a file in a transactional way so you can't get a half-copied file.
97+
The copy will only be performed if the source path exists, and the destination path doesn't already exist.
98+
If the copy is not performed a warning message will be printed.
99+
"""
100+
if not path.exists(sourcePath):
101+
print("WARNING: Not copying {} -> {} as source does not exist".format(sourcePath, destinationPath))
102+
return
103+
104+
if path.exists(destinationPath):
105+
print("WARNING: Not copying {} -> {} as destination already exists".format(sourcePath, destinationPath))
106+
return
107+
108+
shutil.copy(sourcePath, destinationPath + '.temp')
109+
os.rename(destinationPath + '.temp', destinationPath)
110+
93111
class Installer:
94112
def getDataDirectory(self, installPath):
95113
if common.Globals.IS_MAC:
@@ -170,31 +188,41 @@ def __init__(self, fullInstallConfiguration, extractDirectlyToGameDirectory, mod
170188

171189
self.downloaderAndExtractor.printPreview()
172190

173-
def backupUI(self):
174-
"""
175-
Backs up the `sharedassets0.assets` file
176-
Try to do this in a transactional way so you can't get a half-copied .backup file.
177-
This is important since the .backup file is needed to determine which ui file to use on future updates
178-
179-
The file is not moved directly in case the installer is halted before the new UI file can be placed, resulting
180-
in an install completely missing a sharedassets0.assets UI file.
181-
"""
182-
try:
183-
uiPath = path.join(self.dataDirectory, "sharedassets0.assets")
184-
191+
def getBackupPath(self, relativePath):
185192
# partialManualInstall is not really supported on MacOS, so just assume output folder is HigurashiEpX_Data
186193
if self.forcedExtractDirectory is not None:
187-
backupPath = path.join(self.forcedExtractDirectory, self.info.subModConfig.dataName, "sharedassets0.assets.backup")
194+
return path.join(self.forcedExtractDirectory, self.info.subModConfig.dataName, relativePath + '.backup')
188195
else:
189-
backupPath = path.join(self.dataDirectory, "sharedassets0.assets.backup")
196+
return path.join(self.dataDirectory, relativePath + '.backup')
190197

191-
if path.exists(uiPath) and not path.exists(backupPath):
192-
shutil.copy(uiPath, backupPath + '.temp')
193-
os.rename(backupPath + '.temp', backupPath)
198+
def tryBackupFile(self, relativePath):
199+
"""
200+
Tries to backup a file relative to the dataDirectory of the game, unless a backup already exists.
201+
"""
202+
try:
203+
sourcePath = path.join(self.dataDirectory, relativePath)
204+
backupPath = self.getBackupPath(relativePath)
205+
copyFileIfSourceExistsAndDestDoesNot(sourcePath, backupPath)
194206
except Exception as e:
195-
print('Error: Failed to backup sharedassets0.assets file: {} (need backup for future installs!)'.format(e))
207+
print('Error: Failed to backup {} file: {}'.format(relativePath, e))
196208
raise e
197209

210+
211+
def backupFiles(self):
212+
"""
213+
Backs up various files necessary for the installer to operate
214+
Usually this is to prevent the installer having issues if it fails or is stopped half-way
215+
"""
216+
# Backs up the `sharedassets0.assets` file
217+
# Try to do this in a transactional way so you can't get a half-copied .backup file.
218+
# This is important since the .backup file is needed to determine which ui file to use on future updates
219+
# The file is not moved directly in case the installer is halted before the new UI file can be placed, resulting
220+
# in an install completely missing a sharedassets0.assets UI file.
221+
self.tryBackupFile('sharedassets0.assets')
222+
# Backs up the `resources.assets` file
223+
# The backup (resources.assets.backup) will be deleted on a successful install
224+
self.tryBackupFile('resources.assets')
225+
198226
def clearCompiledScripts(self):
199227
compiledScriptsPattern = path.join(self.assetsDir, "CompiledUpdateScripts/*.mg")
200228

@@ -439,6 +467,16 @@ def cleanup(self, cleanExtractionDirectory, cleanDownloadDirectory=True):
439467
# Removes the quarantine attribute from the game (which could cause it to get launched read-only, breaking the script compiler)
440468
subprocess.call(["xattr", "-d", "com.apple.quarantine", self.directory])
441469

470+
def removeResourcesAssetsBackup(self):
471+
# Remove the resources.assets.backup file if install succeeds
472+
# This must be done immediately after extracting all files successfully (and before any languageSpecificAssets are applied)
473+
resourcesBackupPath = self.getBackupPath('resources.assets')
474+
try:
475+
if os.path.exists(resourcesBackupPath):
476+
forceRemove(resourcesBackupPath)
477+
except Exception as e:
478+
print("Warning: Failed to remove `{}`. Updating the mod may not work correctly unless this file is deleted.".format(resourcesBackupPath))
479+
442480
def saveFileVersionInfoStarted(self):
443481
self.fileVersionManager.saveVersionInstallStarted()
444482

@@ -453,7 +491,7 @@ def main(fullInstallConfiguration):
453491

454492
isVoiceOnly = fullInstallConfiguration.subModConfig.subModName == 'voice-only'
455493
if isVoiceOnly:
456-
print("Performing Voice-Only Install - backupUI() and cleanOld() will NOT be performed.")
494+
print("Performing Voice-Only Install - backupFiles() and cleanOld() will NOT be performed.")
457495

458496
modOptionParser = installConfiguration.ModOptionParser(fullInstallConfiguration)
459497
skipDownload = modOptionParser.downloadManually
@@ -465,6 +503,7 @@ def main(fullInstallConfiguration):
465503
installer = Installer(fullInstallConfiguration, extractDirectlyToGameDirectory=False, modOptionParser=modOptionParser, forcedExtractDirectory=extractDir)
466504
installer.download()
467505
installer.extractFiles()
506+
installer.removeResourcesAssetsBackup()
468507
if installer.optionParser.installSteamGrid:
469508
steamGridExtractor.extractSteamGrid(installer.downloadDir)
470509
installer.applyLanguagePatchFixesIfNecessary()
@@ -478,11 +517,12 @@ def main(fullInstallConfiguration):
478517
installer.download()
479518
installer.saveFileVersionInfoStarted()
480519
if not isVoiceOnly:
481-
installer.backupUI()
520+
installer.backupFiles()
482521
installer.cleanOld()
483522
print("Extracting...")
484523
installer.extractFiles()
485524
commandLineParser.printSeventhModStatusUpdate(97, "Cleaning up...")
525+
installer.removeResourcesAssetsBackup()
486526
if installer.optionParser.installSteamGrid:
487527
steamGridExtractor.extractSteamGrid(installer.downloadDir)
488528
installer.applyLanguagePatchFixesIfNecessary()
@@ -497,10 +537,11 @@ def main(fullInstallConfiguration):
497537
installer.extractFiles()
498538
commandLineParser.printSeventhModStatusUpdate(85, "Moving files into place...")
499539
if not isVoiceOnly:
500-
installer.backupUI()
540+
installer.backupFiles()
501541
installer.cleanOld()
502542
installer.moveFilesIntoPlace()
503543
commandLineParser.printSeventhModStatusUpdate(97, "Cleaning up...")
544+
installer.removeResourcesAssetsBackup()
504545
if installer.optionParser.installSteamGrid:
505546
steamGridExtractor.extractSteamGrid(installer.downloadDir)
506547
installer.applyLanguagePatchFixesIfNecessary()

‎installConfiguration.py‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,21 @@ def getUnityVersion(datadir, verbosePrinting=True):
2828
- The `HigurashiEp0X_Data/resources.assets` bundle failed to open (raises error from open() call or read() call)
2929
- The Unity version was too old (raises OldUnityException)
3030
"""
31-
assetsbundlePath = os.path.join(datadir, "resources.assets")
31+
32+
# In certain cases, we upgrade the Unity version of the game (for example, from 5.6.7f1 to 2017.2.5)
33+
#
34+
# This involves overwriting the resources.assets file (which we usually only ever read the Unity version file) and various other system files
35+
#
36+
# It is possible to have a half-upgraded install due to this, as if the install fails or is stopped after the resources.assets is overwritten
37+
# the installer would think the unity version is already upgraded, and not finish applying the upgrade when you re-run the installer
38+
# (or if the resources.assets is only partially overwritten)
39+
#
40+
# For this reason, we make a temporary version of the 'original' resources.assets file as 'resources.assets.backup' when the install starts,
41+
# When the upgrade finishes successfully, we delete this temporary file to signify that the upgrade is complete.
42+
assetsbundlePath = os.path.join(datadir, "resources.assets.backup")
43+
if not os.path.exists(assetsbundlePath):
44+
assetsbundlePath = os.path.join(datadir, "resources.assets")
45+
3246
if not os.path.exists(assetsbundlePath):
3347
raise MissingAssetsBundleException(assetsbundlePath)
3448

0 commit comments

Comments
 (0)