Skip to content

Commit

Permalink
Initial attempt at ensuring half-way upgrading unity version does not…
Browse files Browse the repository at this point in the history
… result in broken install
  • Loading branch information
drojf committed Jul 15, 2023
1 parent 185d602 commit a39aac5
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 22 deletions.
70 changes: 49 additions & 21 deletions higurashiInstaller.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,16 @@ def listInvalidUIFiles(folder):

return invalidUIFileList

def backupFileIfNotExist(sourcePath, backupPath):
#type: (str, str) -> None
"""
Try to backup a file in a transactional way so you can't get a half-copied file.
If the file already is backed up to the backupPath, it won't be overwritten
"""
if path.exists(sourcePath) and not path.exists(backupPath):
shutil.copy(sourcePath, backupPath + '.temp')
os.rename(backupPath + '.temp', backupPath)

class Installer:
def getDataDirectory(self, installPath):
if common.Globals.IS_MAC:
Expand Down Expand Up @@ -170,31 +180,41 @@ def __init__(self, fullInstallConfiguration, extractDirectlyToGameDirectory, mod

self.downloaderAndExtractor.printPreview()

def backupUI(self):
"""
Backs up the `sharedassets0.assets` file
Try to do this in a transactional way so you can't get a half-copied .backup file.
This is important since the .backup file is needed to determine which ui file to use on future updates
The file is not moved directly in case the installer is halted before the new UI file can be placed, resulting
in an install completely missing a sharedassets0.assets UI file.
"""
try:
uiPath = path.join(self.dataDirectory, "sharedassets0.assets")

def getBackupPath(self, relativePath):
# partialManualInstall is not really supported on MacOS, so just assume output folder is HigurashiEpX_Data
if self.forcedExtractDirectory is not None:
backupPath = path.join(self.forcedExtractDirectory, self.info.subModConfig.dataName, "sharedassets0.assets.backup")
return path.join(self.forcedExtractDirectory, self.info.subModConfig.dataName, relativePath + '.backup')
else:
backupPath = path.join(self.dataDirectory, "sharedassets0.assets.backup")
return path.join(self.dataDirectory, relativePath + '.backup')

if path.exists(uiPath) and not path.exists(backupPath):
shutil.copy(uiPath, backupPath + '.temp')
os.rename(backupPath + '.temp', backupPath)
def tryBackupFile(self, relativePath):
"""
Tries to backup a file relative to the dataDirectory of the game, unless a backup already exists.
"""
try:
sourcePath = path.join(self.dataDirectory, relativePath)
backupPath = self.getBackupPath(relativePath)
backupFileIfNotExist(sourcePath, backupPath)
except Exception as e:
print('Error: Failed to backup sharedassets0.assets file: {} (need backup for future installs!)'.format(e))
print('Error: Failed to backup {} file: {}'.format(relativePath, e))
raise e


def backupFiles(self):
"""
Backs up various files necessary for the installer to operate
Usually this is to prevent the installer having issues if it fails or is stopped half-way
"""
# Backs up the `sharedassets0.assets` file
# Try to do this in a transactional way so you can't get a half-copied .backup file.
# This is important since the .backup file is needed to determine which ui file to use on future updates
# The file is not moved directly in case the installer is halted before the new UI file can be placed, resulting
# in an install completely missing a sharedassets0.assets UI file.
self.tryBackupFile('sharedassets0.assets')
# Backs up the `resources.assets` file
# The backup (resources.assets.backup) will be deleted on a successful install
self.tryBackupFile('resources.assets')

def clearCompiledScripts(self):
compiledScriptsPattern = path.join(self.assetsDir, "CompiledUpdateScripts/*.mg")

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

# Remove the resources.assets.backup file if install succeeds
resourcesBackupPath = self.getBackupPath('resources.assets')
try:
if os.path.exists(resourcesBackupPath):
forceRemove(resourcesBackupPath)
except Exception as e:
print("Warning: Failed to remove `{}`. Updating the mod may not work correctly unless this file is deleted.".format(resourcesBackupPath))

def saveFileVersionInfoStarted(self):
self.fileVersionManager.saveVersionInstallStarted()

Expand All @@ -453,7 +481,7 @@ def main(fullInstallConfiguration):

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

modOptionParser = installConfiguration.ModOptionParser(fullInstallConfiguration)
skipDownload = modOptionParser.downloadManually
Expand All @@ -478,7 +506,7 @@ def main(fullInstallConfiguration):
installer.download()
installer.saveFileVersionInfoStarted()
if not isVoiceOnly:
installer.backupUI()
installer.backupFiles()
installer.cleanOld()
print("Extracting...")
installer.extractFiles()
Expand All @@ -497,7 +525,7 @@ def main(fullInstallConfiguration):
installer.extractFiles()
commandLineParser.printSeventhModStatusUpdate(85, "Moving files into place...")
if not isVoiceOnly:
installer.backupUI()
installer.backupFiles()
installer.cleanOld()
installer.moveFilesIntoPlace()
commandLineParser.printSeventhModStatusUpdate(97, "Cleaning up...")
Expand Down
16 changes: 15 additions & 1 deletion installConfiguration.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,21 @@ def getUnityVersion(datadir, verbosePrinting=True):
- The `HigurashiEp0X_Data/resources.assets` bundle failed to open (raises error from open() call or read() call)
- The Unity version was too old (raises OldUnityException)
"""
assetsbundlePath = os.path.join(datadir, "resources.assets")

# In certain cases, we upgrade the Unity version of the game (for example, from 5.6.7f1 to 2017.2.5)
#
# This involves overwriting the resources.assets file (which we usually only ever read the Unity version file) and various other system files
#
# 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
# the installer would think the unity version is already upgraded, and not finish applying the upgrade when you re-run the installer
# (or if the resources.assets is only partially overwritten)
#
# For this reason, we make a temporary version of the 'original' resources.assets file as 'resources.assets.backup' when the install starts,
# When the upgrade finishes successfully, we delete this temporary file to signify that the upgrade is complete.
assetsbundlePath = os.path.join(datadir, "resources.assets.backup")
if not os.path.exists(assetsbundlePath):
assetsbundlePath = os.path.join(datadir, "resources.assets")

if not os.path.exists(assetsbundlePath):
raise MissingAssetsBundleException(assetsbundlePath)

Expand Down

0 comments on commit a39aac5

Please sign in to comment.