private OperationLogMode mode; // The current mode /// <summary> /// Opens or creates a file operation log file. /// </summary> /// <param name="path">The path to the log file.</param> /// <param name="transactionID">The log's transaction <see cref="Guid" />.</param> /// <remarks> /// New logs will be created in <see cref="OperationLogMode.Undo" /> mode. /// </remarks> public FileOperationLog(string path, Guid transactionID) { this.path = Path.GetFullPath(path); this.file = new EnhancedFileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite); if (file.Length == 0) { // Initialize a new file file.WriteInt32(Magic); file.WriteInt32(0); file.WriteInt32(0); file.WriteInt32((int)OperationLogMode.Undo); file.WriteBytesNoLen(transactionID.ToByteArray()); file.Flush(); } else { // Open an existing file. try { if (file.ReadInt32() != Magic || // Magic number file.ReadInt32() != 0) { // Format Versopn throw new Exception(); } file.ReadInt32(); // Reserved switch (file.ReadInt32()) // Mode { case (int)OperationLogMode.Undo: mode = OperationLogMode.Undo; break; case (int)OperationLogMode.Redo: mode = OperationLogMode.Redo; break; default: throw new Exception(); } this.transactionID = new Guid(file.ReadBytes(16)); if (transactionID != this.transactionID) { throw new Exception(); } } catch { throw new TransactionException(CorruptMsg); } } }
/// <summary> /// Reads the metadata from a message file. /// </summary> /// <param name="msgID">The message ID.</param> /// <param name="path">Fully qualified path to the message file.</param> /// <returns>The <see cref="QueuedMsgInfo" />.</returns> internal QueuedMsgInfo ReadMessageMetadata(Guid msgID, string path) { QueuedMsgInfo msgInfo; int cbBody; byte[] md5Hash; long savePos; using (var fsMsg = new EnhancedFileStream(path, FileMode.Open, FileAccess.ReadWrite)) { try { // Read the message file header if (fsMsg.ReadInt32() != MsgMagic) // Magic Number { throw new Exception(); } if (fsMsg.ReadInt32() != 0) // Format Version { throw new Exception(); } fsMsg.ReadInt32(); // Reserved // Verify that the MD5 hash saved in then file matches the // hash computed for the remainder of the file as it exists // right now. md5Hash = fsMsg.ReadBytes(MD5Hasher.DigestSize); savePos = fsMsg.Position; if (!Helper.ArrayEquals(md5Hash, MD5Hasher.Compute(fsMsg, fsMsg.Length - fsMsg.Position))) { throw new FormatException(string.Format("Message file [{0}] is corrupt. MD5 digests do not match.", path)); } fsMsg.Position = savePos; // Skip over the message body data cbBody = fsMsg.ReadInt32(); fsMsg.Position = fsMsg.Position + cbBody; // Read the metadata and add the provider specific information msgInfo = new QueuedMsgInfo(fsMsg.ReadString16()); msgInfo.PersistID = msgInfo.ID; msgInfo.ProviderData = path; return(msgInfo); } catch { throw new FormatException(string.Format("Bad message file [{0}].", path)); } } }
/// <summary> /// Updates a message file's metadata. /// </summary> /// <param name="path">The file path.</param> /// <param name="msgInfo">The message metadata.</param> internal void WriteMessageMetadata(string path, QueuedMsgInfo msgInfo) { using (var fsMsg = new EnhancedFileStream(path, FileMode.Open, FileAccess.ReadWrite)) { // Seek past the message's header and body. int cbBody; byte[] md5Hash; fsMsg.Position = MsgHeaderSize; cbBody = fsMsg.ReadInt32(); fsMsg.Position = fsMsg.Position + cbBody; // Write the metadata and truncate the file. fsMsg.WriteString16(msgInfo.ToString()); fsMsg.SetLength(fsMsg.Position); // Regenerate the MD5 Hash fsMsg.Position = MsgMD5Offset + MD5Hasher.DigestSize; md5Hash = MD5Hasher.Compute(fsMsg, fsMsg.Length - fsMsg.Position); fsMsg.Position = MsgMD5Offset; fsMsg.WriteBytesNoLen(md5Hash); } }
//--------------------------------------------------------------------- // Static members /// <summary> /// Verifies that a log file is not corrupt. /// </summary> /// <param name="path">The path to the log file.</param> /// <param name="transactionID">Returns as the file's transaction <see cref="Guid" />.</param> /// <returns><c>true</c> if the log file is valid, <c>false</c> if it is corrupt.</returns> public static bool Validate(string path, out Guid transactionID) { int cb; try { using (var file = new EnhancedFileStream(path, FileMode.Open, FileAccess.ReadWrite)) { if (file.ReadInt32() != Magic || // Magic number file.ReadInt32() != 0) { // Format Versopn throw new Exception(); } switch (file.ReadInt32()) // Mode { case (int)OperationLogMode.Undo: case (int)OperationLogMode.Redo: break; default: throw new Exception(); } file.ReadInt32(); // Reserved transactionID = new Guid(file.ReadBytes(16)); while (!file.Eof) { if (file.ReadInt32() != Magic) { throw new Exception(); } cb = file.ReadInt32(); if (cb < 0 || cb + file.Position > file.Length) { throw new Exception(); } file.Position += cb; } return(true); } } catch { transactionID = Guid.Empty; return(false); } }
/// <summary> /// Returns the list of the <see cref="ILogPosition" />s with each operation in the log. /// </summary> /// <param name="reverse">Pass <c>true</c> to return the positions in the reverse order that they were appended to the log.</param> /// <returns>The operation position list.</returns> /// <exception cref="TransactionException">Thrown if the log is not open or is corrupt.</exception> public List <ILogPosition> GetPositions(bool reverse) { var list = new List <ILogPosition>(); long length; int cb; using (TimedLock.Lock(this)) { if (file == null) { throw new TransactionException(ClosedMsg); } length = file.Length; file.Position = HeaderSize; while (!file.Eof) { list.Add(new FileLogPosition(file.Position)); if (file.ReadInt32() != Magic) { throw new TransactionException(CorruptMsg); } cb = file.ReadInt32(); if (cb < 0 || cb + file.Position > length) { throw new TransactionException(CorruptMsg); } file.Position += cb; } } if (reverse) { list.Reverse(); } return(list); }
/// <summary> /// Loads the message index file. If the index does not exist or is corrupt then /// the index will be rebuilt by performing a full scan of message files under /// the root folder. /// </summary> /// <param name="newIndexFile">Pass as <c>true</c> if a new index file is being created.</param> private void LoadIndex(bool newIndexFile) { string formatErr = string.Format("Invalid or missing index file [{0}].", indexPath); int cMessages; string[] files; Dictionary <Guid, string> msgFiles; TimedLock.AssertLocked(this); messages = new Dictionary <Guid, QueuedMsgInfo>(); // List the message files and build a hash table mapping each message GUID // the fully qualified path to the file. files = Helper.GetFilesByPattern(root + "*.msg", SearchOption.AllDirectories); msgFiles = new Dictionary <Guid, string>(files.Length); foreach (string path in files) { string file = Path.GetFileName(path).ToLowerInvariant(); Guid msgID; if (!file.EndsWith(".msg")) { continue; } try { msgID = new Guid(file.Substring(0, file.Length - 4)); } catch { continue; } msgFiles[msgID] = path; } // Load the index file. try { // Parse the index file header fsIndex.Position = 0; if (fsIndex.ReadInt32() != IndexMagic) // Magic Number { throw new FormatException(formatErr); } if (fsIndex.ReadInt32() != 0) // Format Version { throw new FormatException(formatErr); } fsIndex.ReadInt32(); // Reserved if (fsIndex.ReadInt32() != 0) { throw new FormatException(string.Format("Index file [{0}] was not closed properly. Full message folder scan will be performed.", indexPath)); } cMessages = fsIndex.ReadInt32(); // Message Count // Parse the message metadata for (int i = 0; i < cMessages; i++) { QueuedMsgInfo msgInfo; msgInfo = new QueuedMsgInfo(fsIndex.ReadString16()); msgInfo.PersistID = msgInfo.ID; msgInfo.ProviderData = root + fsIndex.ReadString16(); // Make the paths absolute messages[msgInfo.ID] = msgInfo; } // Perform an extra consistency check by listing all of the message files // under the root folder and comparing the message GUIDs encoded into the // file names with the GUIDs loaded from the index file and then bringing // the index into sync with the actual message files. bool updated = false; int cLoaded = 0; // Delete any metadata for messages that exist in the index // but don't exist on the file system. var delList = new List <Guid>(); foreach (Guid msgID in messages.Keys) { if (!msgFiles.ContainsKey(msgID)) { delList.Add(msgID); } } foreach (Guid msgID in delList) { messages.Remove(msgID); } if (delList.Count > 0) { updated = true; SysLog.LogWarning(string.Format("Message index [{0}] has message metadata for messages that do not exist. [{1}] messages will be removed from the index.", indexPath, delList.Count)); } // Load metadata for messages that exist in the file system // but were not in the index. foreach (Guid msgID in msgFiles.Keys) { if (!messages.ContainsKey(msgID)) { string path = msgFiles[msgID]; try { messages[msgID] = ReadMessageMetadata(msgID, path); cLoaded++; } catch (Exception e) { SysLog.LogException(e); } } } if (cLoaded > 0) { updated = true; SysLog.LogWarning(string.Format("Message index [{0}] is missing metadata for [{1}] messages. Missing entries will be added.", indexPath, cLoaded)); } if (updated) { SaveIndex(true); } // Mark the index as "open" for crash detection. fsIndex.Position = OpenFlagOffset; fsIndex.WriteInt32(1); fsIndex.Flush(); } catch { if (newIndexFile) { SysLog.LogWarning("Rebuilding missing message index file [{0}].", indexPath); } else { SysLog.LogWarning("Rebuilding corrupt message index file [{0}].", indexPath); } // Clear the index file if there was a serious error and then // rebuild it from scratch by scanning the message metadata. fsIndex.SetLength(0); fsIndex.Flush(); messages.Clear(); foreach (Guid msgID in msgFiles.Keys) { try { messages.Add(msgID, ReadMessageMetadata(msgID, msgFiles[msgID])); } catch (Exception e2) { SysLog.LogException(e2); } } // Save the index, marking the file as "open" for crash detection SaveIndex(true); } }
/// <summary> /// Loads the cache index file. /// </summary> private void LoadIndex() { var indexPath = Path.Combine(settings.PhraseCacheFolder, IndexFileName); try { if (!File.Exists(indexPath)) { // No index exists yet, so create an empty one. SaveIndex(false); return; } using (var fs = new EnhancedFileStream(indexPath, FileMode.Open)) { int count; if (fs.ReadInt32() != Magic) { throw new SwitchException("[PhraseCache] cannot read index file [{0}] due to an invalid magic number. The existing cache will be purged.", indexPath); } count = fs.ReadInt32(); for (int i = 0; i < count; i++) { string text; string voice; string actualVoice; PhraseType phraseType; TtsEncoding encoding; TtsSampleRate rate; DateTime lastAccessUtc; string path; Phrase phrase; phraseType = (PhraseType)Enum.Parse(typeof(PhraseType), fs.ReadString16()); text = fs.ReadString32(); voice = fs.ReadString16(); actualVoice = fs.ReadString16(); encoding = (TtsEncoding)Enum.Parse(typeof(TtsEncoding), fs.ReadString16()); rate = (TtsSampleRate)Enum.Parse(typeof(TtsSampleRate), fs.ReadString16()); lastAccessUtc = new DateTime(fs.ReadInt64()); path = fs.ReadString16(); phrase = new Phrase(phraseType, voice, encoding, rate, text) { Path = path, ActualVoice = actualVoice, LastAccessUtc = lastAccessUtc, }; index[GetCacheKey(phrase)] = phrase; } } } catch (Exception e) { // We're going to handle all exceptions leaving the loaded phrase // table empty. This will have the effect of starting the cache // from scratch. Eventually, all of the existing files will be // purged and the presumably bad index file will be overwritten. index.Clear(); SysLog.LogException(e); } }