private void PatchWorkerDoWork(object sender, DoWorkEventArgs e) { Logger?.Debug("Patch request"); // guard against multiple tasks running at the same time if (!TryEnterTask()) { return; } Logger?.Info("Patch started"); var packageDirectory = Utility.GetTempDirectory(); try { // disable UI Dispatcher.Invoke(() => { Patch.IsEnabled = false; TargetVersion.IsEnabled = false; }); var version = e.Argument as Version; Package package = PackageRepository.FindPackage(_detectedSnapshot.Version, version); Logger?.Info("Package found for source version {0} to target version {1}", package.SourceSnapshot.Version, package.TargetSnapshot.Version); // download package contents locally Logger?.Info("Attempting to download package contents from {0}", Config.ContentRepoUri); package.Save(Path.GetDirectoryName(Config.ContentRepoUri), packageDirectory, WorkerOnProgressChanged, "Downloading package"); // apply package Logger?.Info("Applying package"); package.Apply(Path.Combine(packageDirectory, package.Id + "\\"), _contentPath, Config.ValidateBeforePackageApply, Config.ValidateAfterPackageApply, WorkerOnProgressChanged, "Applying package"); // update detected version _detectedSnapshot = package.TargetSnapshot; Logger?.Info("Patch completed"); MessageBox.Show("Patch applied successfully!", "Done", MessageBoxButton.OK, MessageBoxImage.Information); } catch { MessageBox.Show("Patch failed! Make sure the base game files haven't been modified in any way. If unsure, please run the updater against a fresh copy.", "Hold up!", MessageBoxButton.OK, MessageBoxImage.Error); } finally { // allow other tasks to run ExitTask(); // cleanup the temp package directory Directory.Delete(packageDirectory, true); } }
//TODO: will need to disable multi-threaded processing of actions during application if specified //[JsonProperty(Required = Required.Default)] //public bool PreserveActionOrder { get; set; } // TODO: when order is not important, perform multi-threaded execution in order of biggest to smallest /// <summary> /// Applies package actions in-order against the target directory using the contents of the package directory. /// </summary> /// <param name="packageDirectory">The package contents directory.</param> /// <param name="targetDirectory">The target base directory.</param> /// <param name="validateBefore">Indicates whether or not the target directory should be validated before package application.</param> /// <param name="validateAfter">Indicates whether or not the target directory should be validated after package application.</param> /// <param name="progressHandler">The optional handler to report progress to.</param> /// <param name="progressStatus">The optional progress status description.</param> public void Apply(Path packageDirectory, Path targetDirectory, bool validateBefore = true, bool validateAfter = true, ProgressChangedEventHandler progressHandler = null, string progressStatus = null) { // validate source content if (validateBefore) { Logger?.Info("Validating package source content"); } if (validateBefore && !new Snapshot(targetDirectory, progressHandler, "Validating source content").Contains(SourceSnapshot)) { throw new DataException("Directory contents do not match the source snapshot."); } // create temporary directory to contain target backup in case of rollback Path backupDirectory = Utility.GetTempDirectory(); int processedCount = 0; // TODO: verify backup/rollback logic try { const string backupStepName = "Performing backup"; progressHandler?.Invoke(this, new ProgressChangedEventArgs(0, backupStepName)); // backup content to be modified, skipping NoAction types foreach (var action in Actions.Where(a => !(a is NoAction))) { Path targetPath = Path.Combine(targetDirectory, action.TargetPath); if (File.Exists(targetPath)) { Path backupPath = Path.Combine(backupDirectory, action.TargetPath); Directory.CreateDirectory(Path.GetDirectoryName(backupPath)); File.Copy(targetPath, backupPath, true); } // propagate current progress upstream processedCount++; int progressPercentage = (int)(processedCount / (float)Actions.Count * 100); progressHandler?.Invoke(this, new ProgressChangedEventArgs(progressPercentage, backupStepName)); } progressHandler?.Invoke(this, new ProgressChangedEventArgs(0, progressStatus)); // apply the actions in-order processedCount = 0; foreach (var action in Actions) { action.Run(new ActionContext(targetDirectory, packageDirectory)); // propagate current progress upstream processedCount++; int progressPercentage = (int)(processedCount / (float)Actions.Count * 100); progressHandler?.Invoke(this, new ProgressChangedEventArgs(progressPercentage, progressStatus)); } // TODO: selective file validation fed from list of changed files after patch application // validate modified content if (validateAfter) { Logger?.Info("Validating package output content"); } if (validateAfter && !new Snapshot(targetDirectory, progressHandler, "Validating output").Contains(TargetSnapshot)) { throw new DataException("Directory contents do not match the target snapshot."); } } catch (Exception ex) { Logger?.Error(ex, "Performing rollback."); const string rollbackStepName = "Performing rollback"; progressHandler?.Invoke(this, new ProgressChangedEventArgs(0, rollbackStepName)); processedCount = 0; // if failure is detected, delete any existing targets and restore any backups foreach (var action in Actions.Where(a => !(a is NoAction))) { // log any failures encountered during the rollback, keep chugging through regardless try { File.Delete(Path.Combine(targetDirectory, action.TargetPath)); Path backupPath = Path.Combine(backupDirectory, action.TargetPath); if (File.Exists(backupPath)) { File.Copy(backupPath, Path.Combine(targetDirectory, action.TargetPath), true); } } catch (Exception e) { Logger?.Warn(e, "Unknown package restore failure."); } // propagate current progress upstream processedCount++; int progressPercentage = (int)(processedCount / (float)Actions.Count * 100); progressHandler?.Invoke(this, new ProgressChangedEventArgs(progressPercentage, rollbackStepName)); } // re-throw the original exception throw; } finally { Directory.Delete(backupDirectory, true); } // TODO: remove empty directories or leave that up to the package actions? }
private void UpdateWorker_DoWork(object sender, DoWorkEventArgs e) { Logger?.Debug("Update request"); // guard against multiple tasks running at the same time if (!TryEnterTask()) { return; } try { // attempt to open the updater repo config _updateWorker.ReportProgress(0, "Checking for updates"); Logger?.Info("Downloading updater repository configuration from {0}", Config.UpdaterRepoUri); var repo = Repository.Deserialize(new ReadOnlyFile(Config.UpdaterRepoUri).ReadAllText()); // check for the newest version available var updateSnapshot = repo.Snapshots.OrderByDescending(t => t.Version).FirstOrDefault(); if (updateSnapshot == null) { Logger?.Info("No updates available"); return; } // get the current version var binPath = Assembly.GetExecutingAssembly().Location; var binDir = System.IO.Path.GetDirectoryName(binPath) + "\\"; var currentVersion = new Version(FileVersionInfo.GetVersionInfo(binPath).FileVersion); // abort if no updates available if (updateSnapshot.Version <= currentVersion) { Logger?.Info("Already updated"); return; } if (!Config.UpdaterAutoUpdate) { // give user the choice to accept or decline MessageBoxResult choice = MessageBox.Show( $"Update {updateSnapshot.Version} is available. Would you like to apply it?", "Update Available", MessageBoxButton.YesNo, MessageBoxImage.Information); // abort if declined if (choice != MessageBoxResult.Yes) { Logger?.Debug("Update declined"); return; } } const string backupExtension = ".backup"; string remoteFileDirectory = Path.Combine(Path.GetDirectoryName(Config.UpdaterRepoUri) + "/", updateSnapshot.Version.ToString()); try { // apply updates foreach (var file in updateSnapshot.Files) { Logger?.Info("Updating {0}", file); // build local and remote file paths string remoteFilePath = Path.Combine(remoteFileDirectory + "/", file.Path); string localFilePath = Path.Combine(binDir, file.Path); // rename local file with backup extension if it exists, deleting any that already exist if (File.Exists(localFilePath)) { string backupPath = localFilePath + backupExtension; File.Delete(backupPath); File.Move(localFilePath, backupPath); } // download remote file new ReadOnlyFile(remoteFilePath).Copy(localFilePath); // TODO: validate downloaded content Logger?.Info("Finished updating {0}", file); } Logger?.Info("Update completed, restarting application"); // start a new instance of the updated application and exit the existing // TODO: start cmd with timer to delete backup files Process.Start(Assembly.GetExecutingAssembly().Location); Dispatcher.Invoke(() => { Application.Current.Shutdown(); }); } catch { foreach (var file in Directory.GetFiles(binDir, "*" + backupExtension, SearchOption.AllDirectories)) { string originalPath = file.Substring(0, file.Length - backupExtension.Length); File.Delete(originalPath); File.Move(file, originalPath); } throw; } } finally { // allow other tasks to run ExitTask(); } }