using System; using System.IO; using System.Collections.Generic; using System.Collections.Concurrent; using System.Threading; using UAF; using System.Runtime.InteropServices; using System.Diagnostics; using System.Xml.Linq; using UAF.Interfaces.Logging; namespace DataFileAnalyzer { /// /// Keep the data file updater informed of all the changes that happen in the data folder /// so that it can update the files accordingly. /// Uses the NTFS journal to keep track of the changes that occurred while the /// application was not running. /// public class DataFolderWatcher : IDisposable { private readonly FileUpdater m_updater; private ILog m_logger = new ConsoleLog(); private string m_dataFolder; private List m_excludeFolders = new List(); private ILog m_debugLog = new NullLog(); //ConsoleLog() // The main thread private Thread m_thread = null; // Mutex to lock the members of the class (bool, int, etc...) private Mutex m_internalMutex = new Mutex(false, "DataFileAnalyzer.DataFolderWatcher.m_internalMutex"); // Event set when the main thread has been started private readonly AutoResetEvent m_startedSyncEvent = new AutoResetEvent(false); // Event set when the main thread has been stopped private readonly AutoResetEvent m_stopSyncEvent = new AutoResetEvent(false); // Stop as soon as possible private bool m_exitAsked = false; // The mainthread is running private bool m_running = false; // Have we loaded the data since the last time we started private bool m_loaded = false; private enum State { Stopped, CreatingJournal, Initializing, Running } private State m_state = State.Stopped; private NtfsFolderDictionary m_folderDictionary; private IntPtr m_usnJournalRootHandle = IntPtr.Zero; private static readonly int m_pollingInterval = 10; private static readonly UInt64 m_journalMaxSize = 10 * 1024 * 1024; private static readonly UInt64 m_journalAllocationDelta = 16 * 1024; private bool m_lastUsnEntryWatchedValid = false; private Int64 m_lastUsnEntryWatched = 0; private bool m_lastUsnEntryTreatedValid = false; private Int64 m_lastUsnEntryTreated = 0; private Win32Api.READ_USN_JOURNAL_DATA m_readUsnData; private SafeMemoryHandle m_readUsnDataBuffer; private SafeMemoryHandle m_deviceDataBuffer; private class USNOperations { public Int64 m_usn; public List m_operations = new List(); // When this operation is finished, consider all the USN up to m_lastLinkedUSN // as treated. This is avoids re-reading journal entries that will be ignored. public Int64 m_lastLinkedUSN; public USNOperations(Int64 _usn) { m_usn = _usn; m_lastLinkedUSN = _usn; } public bool IsFinished() { bool finished = true; foreach (UpdateOperation op in m_operations) finished &= ((op.State & UpdateOperation.States.Finished) != UpdateOperation.States.None); return finished; } } private Queue m_usnOperationsQueue = new Queue(); private Dictionary m_usnOperationByUSN = new Dictionary(); private USNOperations m_lastQueuedOperation = null; /// /// Constructor /// public DataFolderWatcher(FileUpdater _updater) { m_updater = _updater; m_logger = _updater.Logger; m_dataFolder = null; m_folderDictionary = new NtfsFolderDictionary(m_logger); m_readUsnData = new Win32Api.READ_USN_JOURNAL_DATA(); m_readUsnDataBuffer = new SafeMemoryHandle(Marshal.SizeOf(m_readUsnData)); m_deviceDataBuffer = new SafeMemoryHandle(sizeof(UInt64) * 0x4000); } public void Dispose() { m_deviceDataBuffer.Dispose(); m_readUsnDataBuffer.Dispose(); } /// /// The logger. Cannot be set to a null value. /// public ILog Logger { get { return m_logger; } set { if (value != null) m_logger = value; } } /// /// Init done? /// public bool Initializing { get { m_internalMutex.WaitOne(); bool result = (m_state != State.Running) || !m_lastUsnEntryTreatedValid; m_internalMutex.ReleaseMutex(); return result; } } /// /// The data folder of the library has changed /// public void OnDataFolderChanged(string _folder, List _excludedFolders) { m_dataFolder = PathUtils.Normalize(_folder); m_excludeFolders.Clear(); foreach (string folder in _excludedFolders) m_excludeFolders.Add(PathUtils.Normalize(Path.Combine(m_dataFolder, folder))); m_folderDictionary.OnDataFolderChanged(m_dataFolder, m_excludeFolders); } /// /// Try to get the last USN entry treated. Returns false if no valid entry stored. /// private bool GetLastUsnEntryTreated(out Int64 _lastUsn) { m_internalMutex.WaitOne(); bool result = m_lastUsnEntryTreatedValid; _lastUsn = m_lastUsnEntryTreated; m_internalMutex.ReleaseMutex(); return result; } /// /// Set the last USN entry treated. /// private void SetLastUsnEntryTreated(bool valid, Int64 _lastUsn) { m_internalMutex.WaitOne(); m_lastUsnEntryTreatedValid = valid; m_lastUsnEntryTreated = _lastUsn; m_internalMutex.ReleaseMutex(); } /// /// Is this a path to one of the excluded folders? /// private bool IsInExcludeFolder(string _path) { string normalizedPath = PathUtils.Normalize(_path); foreach (string folder in m_excludeFolders) { if (normalizedPath.StartsWith(folder)) return true; } return false; } /// /// Does _path points to a data file that should be watched? /// private bool IsDataFile(string _path) { string normalizedPath = PathUtils.Normalize(_path); if (normalizedPath.StartsWith(m_dataFolder)) { foreach (string folder in m_excludeFolders) { if (normalizedPath.StartsWith(folder)) return false; } return true; } return false; } /// /// Start watching. /// public void Start(FileLibrary _library) { if (m_dataFolder == null) { m_logger.Error("[Watcher] Couldn't start the data folder watcher: no folder set!"); return; } if (!m_loaded) { // Reset m_lastUsnEntryTreatedValid = false; } m_thread = new Thread(MainThread); m_thread.Name = "DataFileAnalyzer.DataFolderWatcher.MainThread"; m_thread.Start(); m_startedSyncEvent.WaitOne(); m_running = true; } /// /// Stop watching. /// public void Stop() { if (m_running) { ExitAsked = true; m_stopSyncEvent.WaitOne(); m_exitAsked = false; m_thread = null; m_dataFolder = null; m_running = false; m_loaded = false; } } /// /// Ask for the other thread(s) to exit. /// private bool ExitAsked { get { m_internalMutex.WaitOne(); bool result = m_exitAsked; m_internalMutex.ReleaseMutex(); return result; } set { m_internalMutex.WaitOne(); m_exitAsked = value; m_internalMutex.ReleaseMutex(); } } #region Watcher thread /// /// Open or create the USN journal of the volume containing the data folder. /// /// true if init done private bool OpenVolume() { if (!WindowsUtils.CurrentUserIsAdmin()) { m_logger.Error("[Watcher] The current Windows user is not an administrator on this machine."); return false; } string equivalentDataPath; IntPtr rootDriveHandle = Win32Api.GetDriveRootHandle(m_dataFolder, "ntfs", out equivalentDataPath); if (rootDriveHandle.ToInt32() == Win32Api.INVALID_HANDLE_VALUE) { m_logger.Error("[Watcher] Could not get the drive root handle for path '" + m_dataFolder + "'"); return false; } m_usnJournalRootHandle = rootDriveHandle; m_folderDictionary.SetJournalHandle(m_usnJournalRootHandle); return true; } /// /// Close the USN journal. /// private void CloseVolume() { if (m_usnJournalRootHandle.ToInt32() != Win32Api.INVALID_HANDLE_VALUE) { Win32Api.CloseHandle(m_usnJournalRootHandle); m_usnJournalRootHandle = IntPtr.Zero; } m_folderDictionary.SetJournalHandle(IntPtr.Zero); } /// /// Watching thread: Read the NTFS journal of the volume at regular intervals and /// check if the entries are relevant. /// private void MainThread() { if (!OpenVolume()) { m_exitAsked = true; m_lastUsnEntryWatchedValid = false; m_state = State.Stopped; } else { // Set the first USN to watch m_lastUsnEntryWatchedValid = GetLastUsnEntryTreated(out m_lastUsnEntryWatched); m_state = m_lastUsnEntryWatchedValid ? State.Running : State.Initializing; if (m_lastUsnEntryTreatedValid) m_logger.Message("[Watcher] Start running. Last USN treated: {0:X}.", m_lastUsnEntryTreated); else m_logger.Message("[Watcher] Start initializing. No valid USN treated."); } // Unlock parent thread m_startedSyncEvent.Set(); // Main loop while (!ExitAsked) { switch (m_state) { case State.Stopped: ExitAsked = true; break; case State.CreatingJournal: UpdateStateCreatingJournal(); break; case State.Initializing: UpdateStateInitializing(); break; case State.Running: UpdateStateRunning(); break; } // Sleep a while before polling again Thread.Sleep(m_pollingInterval); } UpdateLastUsnEntryTreated(); m_state = State.Stopped; m_lastUsnEntryWatchedValid = false; m_usnOperationsQueue.Clear(); m_usnOperationByUSN.Clear(); m_lastQueuedOperation = null; CloseVolume(); if (m_lastUsnEntryTreatedValid) m_logger.Message("[Watcher] Stopped. Last USN treated: {0}", m_lastUsnEntryTreated); else m_logger.Message("[Watcher] Stopped. No last USN treated."); // Unlock parent thread m_stopSyncEvent.Set(); } /// /// Go to the CreatingJournal state /// private void GotoCreatingJournalState() { AskForLibraryReset(); m_lastUsnEntryWatchedValid = false; SetLastUsnEntryTreated(false, 0); m_state = State.CreatingJournal; } /// /// Update state CreatingJournal /// private void UpdateStateCreatingJournal() { Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); Win32Api.USN_JOURNAL_DATA journalData; bool journalCreated = Win32Api.QueryUSNJournalState(m_usnJournalRootHandle, out journalData); if (!journalCreated) { // Try creating a new journal journalCreated = Win32Api.CreateUSNJournal(m_usnJournalRootHandle, m_journalMaxSize, m_journalAllocationDelta); } if (journalCreated) { stopwatch.Stop(); m_logger.Message("[Watcher] USN Journal created in {0}.", stopwatch.Elapsed.ToReadableString()); m_state = State.Initializing; } } /// /// Go to the Initializing state /// private void GotoInitializingState() { AskForLibraryReset(); m_lastUsnEntryWatchedValid = false; SetLastUsnEntryTreated(false, 0); m_state = State.Initializing; } /// /// Update state Initializing. The volume is parsed, /// we collect data about all the files in the data folder, /// we notify the updater of all the files we saw then we run. /// private void UpdateStateInitializing() { Win32Api.USN_JOURNAL_DATA journalData; bool ok = Win32Api.QueryUSNJournalState(m_usnJournalRootHandle, out journalData); if (!ok) { GotoCreatingJournalState(); return; } Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); // Init the dictionary m_lastUsnEntryWatched = (journalData.NextUsn == 0) ? 0 : journalData.NextUsn - 1; m_lastUsnEntryWatchedValid = Win32Api.GetHighestValidUSN(m_usnJournalRootHandle, journalData.LowestValidUsn, ref m_lastUsnEntryWatched); if (!m_lastUsnEntryWatchedValid || !m_folderDictionary.InitFromFolder()) { m_lastUsnEntryWatchedValid = false; m_state = State.Stopped; return; } ListAllFiles(); m_logger.Message("[Watcher] Listing all files done in {0}. Start watching at USN {1}", stopwatch.Elapsed.ToReadableString(), m_lastUsnEntryWatched); m_state = State.Running; } /// /// Update state running. Parse the USN journal looking for interesting entries. /// private void UpdateStateRunning() { UpdateLastUsnEntryTreated(); Win32Api.USN_JOURNAL_DATA journalData; if (!Win32Api.QueryUSNJournalState(m_usnJournalRootHandle, out journalData)) { m_logger.Message("[Watcher] Couldn't get journal state. Re-creating journal."); GotoCreatingJournalState(); return; } if (!m_lastUsnEntryWatchedValid || m_lastUsnEntryWatched >= journalData.NextUsn) { m_logger.Message("[Watcher] The last watched USN is invalid. Re-initializing."); GotoInitializingState(); return; } Int64 boundaryUsn = journalData.NextUsn; // Parse the latest journal entries and get the files that has been modified m_readUsnData.StartUsn = 0; // Will be overwritten m_readUsnData.ReasonMask = Win32Api.USN_REASON_ALL; m_readUsnData.ReturnOnlyOnClose = 0; m_readUsnData.Timeout = 0; m_readUsnData.bytesToWaitFor = 0; m_readUsnData.UsnJournalId = journalData.UsnJournalID; m_readUsnDataBuffer.ZeroMemory(); m_readUsnDataBuffer.FromStructure(m_readUsnData, true); bool stopReading = false; Int64 startUsn = m_lastUsnEntryWatched; //m_debugLog.Message("[Read journal] Start reading from USN {0:X}", startUsn); m_readUsnDataBuffer.BeginHandleUse(); m_deviceDataBuffer.BeginHandleUse(); while (!stopReading) { // Set readData.StartUsn m_readUsnDataBuffer.WriteInt64(startUsn); m_deviceDataBuffer.ZeroMemory(); uint bytesRead; if (Win32Api.DeviceIoControl( m_usnJournalRootHandle, Win32Api.FSCTL_READ_USN_JOURNAL, m_readUsnDataBuffer.Handle, m_readUsnDataBuffer.Size, m_deviceDataBuffer.Handle, m_deviceDataBuffer.Size, out bytesRead, IntPtr.Zero)) { uint totalBytes = bytesRead; // Skip the next USN stored at the beginning of the buffer bytesRead -= sizeof(Int64); if (bytesRead == 0) { stopReading = true; } else { IntPtr usnRecordPointer = m_deviceDataBuffer.Add(sizeof(Int64)); while (bytesRead > 0) { Win32Api.UsnEntry usnEntry = new Win32Api.UsnEntry(usnRecordPointer); Int64 usn = usnEntry.USN; if (usn >= boundaryUsn) { stopReading = true; break; } else { if (!m_lastUsnEntryWatchedValid || usn != m_lastUsnEntryWatched) { WatchUsnEntry(usnEntry); m_lastUsnEntryWatchedValid = true; m_lastUsnEntryWatched = usn; } usnRecordPointer = IntPtr.Add(usnRecordPointer, (int)usnEntry.RecordLength); bytesRead -= usnEntry.RecordLength; } } } startUsn = m_deviceDataBuffer.ReadInt64(); } else { Win32Api.GetLastErrorEnum lastWin32Error = (Win32Api.GetLastErrorEnum)Marshal.GetLastWin32Error(); if (lastWin32Error != Win32Api.GetLastErrorEnum.ERROR_HANDLE_EOF) { m_logger.Error("[Watcher] Error during the FSCTL_READ_USN_JOURNAL operation: 0x{0:X}", Marshal.GetLastWin32Error()); GotoInitializingState(); } stopReading = true; } } m_readUsnDataBuffer.EndHandleUse(); m_deviceDataBuffer.EndHandleUse(); //m_debugLog.Message("[Read journal] \t\tLast USN watched: {0:X}", m_lastUsnEntryWatched); } /// /// Analyze the journal entry. Update the folder dictionary and signal the data file updater if needed. /// private void WatchUsnEntry(Win32Api.UsnEntry _usnEntry) { UInt64 frn = _usnEntry.FileReferenceNumber; UInt64 parentFrn = _usnEntry.ParentFileReferenceNumber; //m_debugLog.Message("WatchUsnEntry 0x{0:X}:", _usnEntry.USN); //m_debugLog.Message("\tReason=0x{0:X}", _usnEntry.Reason); //m_debugLog.Message("\tName={0}", _usnEntry.Name); //m_debugLog.Message("\tFull path={0}", m_folderDictionary.GetFRNFullPath(frn)); //m_debugLog.Message("\tOld name={0}", _usnEntry.OldName); //m_debugLog.Message("\tFRN={0:X}", frn); //m_debugLog.Message("\tParent FRN={0:X}", parentFrn); if ((_usnEntry.Reason & Win32Api.USN_REASON_CLOSE) == 0) { IgnoreUSN(_usnEntry.USN); return; } bool add = false; bool delete = false; bool move = false; bool modified = false; if ((_usnEntry.Reason & Win32Api.USN_REASON_FILE_CREATE) != 0) { // New file/folder add = m_folderDictionary.IsFRNInDictionary(parentFrn) && !m_folderDictionary.IsFRNInDictionary(frn); } else if ((_usnEntry.Reason & Win32Api.USN_REASON_FILE_DELETE) != 0) { // File/folder deleted delete = m_folderDictionary.IsFRNInDictionary(frn); } else if ((_usnEntry.Reason & (Win32Api.USN_REASON_RENAME_NEW_NAME | Win32Api.USN_REASON_RENAME_OLD_NAME)) != 0) { Debug.Assert((_usnEntry.Reason & Win32Api.USN_REASON_RENAME_NEW_NAME) != 0, "Non-handled scenario in the NTFS journal parsing!"); if ((_usnEntry.Reason & Win32Api.USN_REASON_RENAME_NEW_NAME) != 0) { bool inFolder = m_folderDictionary.IsFRNInDictionary(frn); bool parentInFolder = m_folderDictionary.IsFRNInDictionary(parentFrn); if (inFolder) { if (!parentInFolder) { // File/folder moved to an outside directory. Delete. delete = true; } else { // File/folder renamed or moved inside the data folder move = true; } } else if (parentInFolder) { // File/folder moved to the data folder. Add. add = true; } } else { m_logger.Error("[Watcher] Non-handled scenario in the NTFS journal parsing! Call a programmer."); } } else if ( (_usnEntry.Reason & ( Win32Api.USN_REASON_DATA_EXTEND | Win32Api.USN_REASON_DATA_OVERWRITE | Win32Api.USN_REASON_DATA_TRUNCATION | Win32Api.USN_REASON_NAMED_DATA_EXTEND | Win32Api.USN_REASON_NAMED_DATA_OVERWRITE | Win32Api.USN_REASON_NAMED_DATA_TRUNCATION)) != 0) { modified = m_folderDictionary.IsFRNInDictionary(frn); } if (add) { if (_usnEntry.IsFile) { m_folderDictionary.AddFile(_usnEntry.Name, frn, parentFrn); FileChanged(m_folderDictionary.GetFRNFullPath(frn), _usnEntry.USN); //m_debugLog.Message("\tAdded: {0}", m_folderDictionary.GetFRNFullPath(frn)); } else { List addedFiles; m_folderDictionary.AddFolder(_usnEntry.Name.ToLower(), frn, parentFrn, out addedFiles); //m_debugLog.Message("\tAdded folder: {0}", m_folderDictionary.GetFRNFullPath(_usnEntry.FileReferenceNumber)); foreach (string addedFile in addedFiles) { FileChanged(addedFile, _usnEntry.USN); //m_debugLog.Message("\t\tAdded: {0}", m_folderDictionary.GetFRNFullPath(addedFrn)); } } } else if (delete) { if (_usnEntry.IsFile) { string fullPath = m_folderDictionary.GetFRNFullPath(frn); m_folderDictionary.RemoveFile(frn); FileChanged(fullPath, _usnEntry.USN); //m_debugLog.Message("\tRemoved: {0}", fullPath); } else { List removedFiles; m_folderDictionary.RemoveFolder(frn, out removedFiles); foreach (string removedFile in removedFiles) { //m_debugLog.Message("\tRemoved: {0}", removedFile); FileChanged(removedFile, _usnEntry.USN); } } } else if (move) { if (_usnEntry.IsFile) { string oldFullPath = m_folderDictionary.GetFRNFullPath(frn); m_folderDictionary.MoveFile(frn, _usnEntry.Name, parentFrn); FileChanged(oldFullPath, _usnEntry.USN); FileChanged(m_folderDictionary.GetFRNFullPath(frn), _usnEntry.USN); //m_debugLog.Message("\tMoved: {0} -> {1}", oldFullPath, m_folderDictionary.GetFRNFullPath(frn)); } else { string oldFullPath = m_folderDictionary.GetFRNFullPath(frn); List addedFiles; List removedFiles; m_folderDictionary.MoveFolder(frn, _usnEntry.Name, parentFrn, out addedFiles, out removedFiles); //m_debugLog.Message("\tMoved folder: {0} -> {1}", oldFullPath, m_folderDictionary.GetFRNFullPath(frn)); foreach (string removedFile in removedFiles) { FileChanged(removedFile, _usnEntry.USN); //m_debugLog.Message("\t\tRemoved: {0}", removedFile); } foreach (string addedFile in addedFiles) { FileChanged(addedFile, _usnEntry.USN); //m_debugLog.Message("\t\tAdded: {0}", m_folderDictionary.GetFRNFullPath(addedFrn)); } } } else if (modified) { Debug.Assert(_usnEntry.IsFile, "Non-handled scenario in the NTFS journal parsing!"); if (_usnEntry.IsFile) { FileChanged(m_folderDictionary.GetFRNFullPath(frn), _usnEntry.USN); } else { m_logger.Error("[Watcher] Non-handled scenario in the NTFS journal parsing! Call a programmer."); } } else { IgnoreUSN(_usnEntry.USN); } } /// /// This file has been changed. Ask the updater to check this path and update the file. /// private void FileChanged(string _path, Int64 _usn) { UpdateOperation updateOperation = m_updater.AskForFileUpdate(_path); NewUpdateOperation(_usn, updateOperation); } /// /// Keep trace of the created operation so we can know when the USN entry has been /// completely treated. /// private void NewUpdateOperation(Int64 _usn, UpdateOperation _newUpdateOperation) { USNOperations usnOperation; if (!m_usnOperationByUSN.TryGetValue(_usn, out usnOperation)) { usnOperation = new USNOperations(_usn); m_usnOperationByUSN.Add(_usn, usnOperation); m_usnOperationsQueue.Enqueue(usnOperation); m_lastQueuedOperation = usnOperation; } usnOperation.m_operations.Add(_newUpdateOperation); } /// /// This USN has been ignored. Try to avoid reading it again. /// private void IgnoreUSN(Int64 _usn) { if (m_lastQueuedOperation != null) { m_lastQueuedOperation.m_lastLinkedUSN = _usn; } else { // If no running operations, then consider this USN as // the last USN treated so we don't read the journal entry // again SetLastUsnEntryTreated(true, _usn); } } /// /// Force the updater to recheck all the files of the data folder. /// private void ListAllFiles() { uint checkExitPeriod = 100; uint checkExitCounter = checkExitPeriod; foreach (string filename in m_folderDictionary.GetAllFiles()) { FileChanged(filename, m_lastUsnEntryWatched); // Check exit if (checkExitCounter == 0) { if (ExitAsked) break; checkExitCounter = checkExitPeriod; } else { --checkExitCounter; } } } /// /// Check which operations are finished to update the last treated USN entry. /// private void UpdateLastUsnEntryTreated() { while (m_usnOperationsQueue.Count != 0) { USNOperations usnOperation = m_usnOperationsQueue.Peek(); if (usnOperation.IsFinished()) { // Update the last treated USN SetLastUsnEntryTreated(true, usnOperation.m_lastLinkedUSN); // Delete the operation m_usnOperationsQueue.Dequeue(); m_usnOperationByUSN.Remove(usnOperation.m_usn); } else { break; } } if (m_usnOperationsQueue.Count == 0) m_lastQueuedOperation = null; } /// /// Cancel all the operations /// private void AskForLibraryReset() { m_updater.ClearLibrary(); m_usnOperationsQueue.Clear(); m_usnOperationByUSN.Clear(); m_lastQueuedOperation = null; } #endregion #region Load/Save /// /// Load the state of the watcher that was saved in a library XML. /// Called from the library thread before the watcher thread is running. /// /// true if no error occurred. If no LastTreatedUSN found, then return false. public bool Load(XElement _libraryElement) { if (m_running) return false; m_lastUsnEntryTreatedValid = false; XElement watcherElement = _libraryElement.Element("DataFolderWatcher"); if (watcherElement != null) { XAttribute attr = watcherElement.Attribute("LastTreatedUSN"); if (attr != null && Int64.TryParse(attr.Value, out m_lastUsnEntryTreated)) { m_lastUsnEntryTreatedValid = m_folderDictionary.Load(watcherElement); m_loaded = m_lastUsnEntryTreatedValid; return m_loaded; } } return false; } /// /// Save the state of the watcher in a library XML. /// Called by the library thread. Can be called while /// the thread is running! /// Don't save if the last treated USN is invalid. /// /// true if no error occurred. public bool Save(XElement _libraryElement) { Int64 lastEntryTreated; if (GetLastUsnEntryTreated(out lastEntryTreated)) { XElement watcherElement = new XElement("DataFolderWatcher"); _libraryElement.Add(watcherElement); watcherElement.SetAttributeValue("LastTreatedUSN", lastEntryTreated); return m_folderDictionary.Save(watcherElement); } return false; } #endregion } }