/// <summary> /// Process and apply content patches. A server restart is scheduled /// if required for the content patch. /// </summary> /// <param name="ContentPatchPath">Supplies the configured content /// patch base path, from the database config table.</param> /// <param name="Database">Supplies the database object.</param> /// <param name="Script">Supplies the script object.</param> /// <param name="ConnectionString">Supplies the updater file store /// connection string.</param> /// <returns>True if a patch was applied and a reboot is required for /// it to take effect.</returns> public static bool ProcessContentPatches(string ContentPatchPath, ALFA.Database Database, ACR_ServerCommunicator Script, string ConnectionString) { bool ContentChanged = false; string Version = Database.ACR_GetHAKVersion(); List<ContentPatchFile> PatchFiles = new List<ContentPatchFile>(); ContentPatchPaths LocalPaths = new ContentPatchPaths() { HakPath = ALFA.SystemInfo.GetHakDirectory(), OverridePath = ALFA.SystemInfo.GetOverrideDirectory() + "ACR_ContentPatches" }; string RemoteContentPatchPath = String.Format("{0}{1}\\{2}", ALFA.SystemInfo.GetCentralVaultPath(), ContentPatchPath, Version); string FileStoreContentPatchPath = String.Format("{0}/{1}", ContentPatchPath, Version).Replace('\\', '/'); bool RecompileModule = false; bool SentNotification = false; Database.ACR_SQLQuery(String.Format( "SELECT `FileName`, `Location`, `Checksum`, `RecompileModule` FROM `content_patch_files` WHERE `HakVersion` = '{0}'", Database.ACR_SQLEncodeSpecialChars(Version))); while (Database.ACR_SQLFetch()) { ContentPatchFile PatchFile = new ContentPatchFile(); PatchFile.FileName = Database.ACR_SQLGetData(0); PatchFile.Location = Database.ACR_SQLGetData(1); PatchFile.Checksum = Database.ACR_SQLGetData(2); PatchFile.RecompileModule = ALFA.Database.ACR_ConvertDatabaseStringToBoolean(Database.ACR_SQLGetData(3)); if (PatchFile.Location != "override" && PatchFile.Location != "hak") { continue; } if (!ALFA.SystemInfo.IsSafeFileName(PatchFile.FileName)) continue; PatchFiles.Add(PatchFile); } if (!Directory.Exists(LocalPaths.OverridePath)) Directory.CreateDirectory(LocalPaths.OverridePath); // // Remove entries in the ACR patch override directory that are not // listed in the patch table. These may be patches for a previous // version, and are not applicable. // foreach (string DirFile in Directory.GetFiles(LocalPaths.OverridePath)) { ContentPatchFile FoundPatch = (from PF in PatchFiles where (PF.FileName.Equals(Path.GetFileName(DirFile), StringComparison.InvariantCultureIgnoreCase) && PF.Location == "override") select PF).FirstOrDefault(); if (FoundPatch == null) { Database.ACR_IncrementStatistic("CONTENT_PATCH_REMOVE_FILE"); Script.WriteTimestampedLogEntry(String.Format( "ModuleContentPatcher.ProcessContentPatches: Removing extraneous file {0} from {1}", Path.GetFileName(DirFile), LocalPaths.OverridePath)); File.Delete(DirFile); ContentChanged = true; } } // // Verify that the MD5 checksum of each of the content files in the // ACR patch override directory matches the database's expected // checksum. If not (or if the file in question didn't exist), // then copy the new version over from the central vault. // using (MD5CryptoServiceProvider MD5Csp = new MD5CryptoServiceProvider()) { foreach (ContentPatchFile PatchFile in PatchFiles) { bool Matched = false; bool Rename = false; FileUpdateMethod UpdateMethod; string LocalPath = PatchFile.GetLocalPath(LocalPaths); string RemotePath = String.Format("{0}\\{1}", RemoteContentPatchPath, PatchFile.FileName); string FileStorePath = String.Format("{0}/{1}", FileStoreContentPatchPath, PatchFile.FileName).Replace('\\', '/'); string LocalHashString = "<no such file>"; string TransferTempFilePath = LocalPath + ".patchxfer"; if (File.Exists(LocalPath)) { LocalHashString = GetFileChecksum(LocalPath, MD5Csp); Matched = (LocalHashString.ToString() == PatchFile.Checksum.ToLower()); } if (Matched) { Script.WriteTimestampedLogEntry(String.Format( "ModuleContentPatcher.ProcessContentPatches: Content patch file {0} is up to date (checksum {1}).", PatchFile.FileName, LocalHashString)); } else { if (File.Exists(TransferTempFilePath)) { try { File.Delete(TransferTempFilePath); } catch (Exception e) { Script.WriteTimestampedLogEntry(String.Format( "ModuleContentPatcher.ProcessContentPatches: Warning: Exception {0} removing transfer temp file {1} for transfer file {2}.", e, TransferTempFilePath, PatchFile.FileName)); } } Script.WriteTimestampedLogEntry(String.Format( "ModuleContentPatcher.ProcessContentPatches: Content patch file {0} needs to be updated (local checksum {1}, remote checksum {2}). Copying file...", PatchFile.FileName, LocalHashString, PatchFile.Checksum)); if (PatchFile.RecompileModule) { Script.WriteTimestampedLogEntry(String.Format( "ModuleContentPatcher.ProcessContentPatches: Content patch file {0} requires a module recompile, flagging module for recompilation.", PatchFile.FileName)); RecompileModule = true; } if (!SentNotification) { Script.SendInfrastructureIrcMessage(String.Format( "Server '{0}' is applying a content patch, and will restart shortly.", Script.GetName(Script.GetModule()))); SentNotification = true; } Script.SendInfrastructureDiagnosticIrcMessage(String.Format( "Server '{0}' is updating content file '{1}'.", Script.GetName(Script.GetModule()), PatchFile.FileName)); // // The file needs to be updated. Copy it over and // re-validate the checksum. If the checksum did not // match, log an error and delete the file. // try { try { // // Try first to download via the file store // provider. If that fails (e.g. the file // store is not supported), then fall back to // the traditional remote vault share transfer // mechanism. // try { DownloadContentPatchFromFileStore(FileStorePath, TransferTempFilePath, ConnectionString, Script); } catch (Exception e) { Script.WriteTimestampedLogEntry(String.Format("ModuleContentPatcher.ProcessContentPatches: Couldn't retrieve uncompressed file {0} from Azure, falling back to file share, due to exception: {1}", FileStorePath, e)); File.Copy(RemotePath, TransferTempFilePath, true); } } catch { throw; } UpdateMethod = PatchFile.UpdateMethod; // // If we are patching a hak, rename it away so that // the file can be written to. // switch (UpdateMethod) { case FileUpdateMethod.FileUpdateMethodRename: if (File.Exists(LocalPath)) { string OldFileName = LocalPath + ".old"; if (File.Exists(OldFileName)) File.Delete(OldFileName); File.Move(LocalPath, OldFileName); Rename = true; } break; case FileUpdateMethod.FileUpdateMethodDirectReplace: break; } Database.ACR_IncrementStatistic("CONTENT_PATCH_" + PatchFile.Location.ToUpper()); try { if (Rename) File.Move(TransferTempFilePath, LocalPath); else File.Copy(TransferTempFilePath, LocalPath, true); } catch { if (UpdateMethod == FileUpdateMethod.FileUpdateMethodRename) { string OldFileName = LocalPath + ".old"; try { if (File.Exists(LocalPath)) File.Delete(LocalPath); } catch { } File.Move(OldFileName, LocalPath); } else { File.Delete(LocalPath); } throw; } if (GetFileChecksum(LocalPath, MD5Csp) != PatchFile.Checksum.ToLower()) { Script.WriteTimestampedLogEntry(String.Format( "ModuleContentPatcher.ProcessContentPatches: Content patch file {0} was copied, but checksum did not match {1}! This may indicate a configuration error in the database, or file corruption in transit.", PatchFile.FileName, PatchFile.Checksum)); Script.SendInfrastructureDiagnosticIrcMessage(String.Format( "Server '{0}' had checksum mismatch for content patch file {1} after hotfix file deployment, rolling back file.", Script.GetName(Script.GetModule()), PatchFile.FileName)); if (UpdateMethod == FileUpdateMethod.FileUpdateMethodRename) { string OldFileName = LocalPath + ".old"; try { if (File.Exists(LocalPath)) File.Delete(LocalPath); } catch { } File.Move(OldFileName, LocalPath); } else { File.Delete(LocalPath); } Database.ACR_IncrementStatistic("CONTENT_PATCH_INCORRECT_CHECKSUM"); } else { ContentChanged = true; Database.ACR_IncrementStatistic("CONTENT_PATCH_UPDATED_FILE"); Script.WriteTimestampedLogEntry(String.Format( "ModuleContentPatcher.ProcessContentPatches: Successfully updated content patch file {0}.", PatchFile.FileName)); } } catch (Exception e) { Script.WriteTimestampedLogEntry(String.Format( "ModuleContentPatcher.ProcessContentPatches: Exception {0} updating content patch file {1}.", e, PatchFile.FileName)); Script.SendInfrastructureDiagnosticIrcMessage(String.Format( "Server '{0}' encountered exception deploying content patch file {1}, rolling back file.", Script.GetName(Script.GetModule()), PatchFile.FileName)); } } if (File.Exists(TransferTempFilePath)) { try { File.Delete(TransferTempFilePath); } catch (Exception e) { Script.WriteTimestampedLogEntry(String.Format( "ModuleContentPatcher.ProcessContentPatches: Warning: Exception {0} removing transfer temp file {1} for transfer file {2} after patching completed.", e, TransferTempFilePath, PatchFile.FileName)); } } } } // // Update autodownloader configuration, as necessary. // try { if (ProcessModuleDownloaderResourcesUpdates(Database, Script)) { Script.WriteTimestampedLogEntry("ModuleContentPatcher.ProcessContentPatches: Autodownloader configuration updated."); ContentChanged = true; } } catch (Exception e) { Script.WriteTimestampedLogEntry(String.Format( "ModuleContentPatcher.ProcessContentPatches: Warning: Exception {0} updating autodownloader configuration.", e)); Script.SendInfrastructureDiagnosticIrcMessage(String.Format( "Server '{0}' encountered exception updating autodownloader configuration.", Script.GetName(Script.GetModule()))); } if (ContentChanged) { if (RecompileModule) { Script.WriteTimestampedLogEntry("ModuleContentPatcher.ProcessContentPatches: A module recompile is required; recompiling module..."); CompileModuleScripts(Script, Database); } Database.ACR_IncrementStatistic("CONTENT_PATCH_REBOOT"); Script.SendInfrastructureDiagnosticIrcMessage(String.Format( "Server '{0}' restarting after content patch deployment (old HAK ACR version {1} build date {2}, old module ACR version {3}).", Script.GetName(Script.GetModule()), Database.ACR_GetHAKVersion(), Database.ACR_GetHAKBuildDate(), Database.ACR_GetVersion())); } return ContentChanged; }