//******************************************************************************************************
// InterprocessCache.cs - Gbtc
//
// Copyright © 2012, Grid Protection Alliance. All Rights Reserved.
//
// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
// the NOTICE file distributed with this work for additional information regarding copyright ownership.
// The GPA licenses this file to you under the Eclipse Public License -v 1.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain a copy of the License at:
//
// http://www.opensource.org/licenses/eclipse-1.0.php
//
// Unless agreed to in writing, the subject software distributed under the License is distributed on an
// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
// License for the specific language governing permissions and limitations.
//
// Code Modification History:
// ----------------------------------------------------------------------------------------------------
// 03/21/2011 - J. Ritchie Carroll
// Generated original version of source code.
// 04/06/2011 - J. Ritchie Carroll
// Added Flush() method to block current thread and wait for any pending save to complete.
// 12/14/2012 - Starlynn Danyelle Gilliam
// Modified Header.
//
//******************************************************************************************************
using System;
using System.Collections;
using System.IO;
using System.Threading;
using System.Timers;
using GSF.Collections;
using GSF.Threading;
using Timer = System.Timers.Timer;
namespace GSF.IO
{
/// <summary>
/// Represents a serialized data cache that can be saved or read from multiple applications using interprocess synchronization.
/// </summary>
/// <remarks>
/// Note that all file data in this class gets serialized to and from memory, as such, the design intention for this class is for
/// use with smaller data sets such as serialized lists or dictionaries that need interprocess synchronized loading and saving.
/// </remarks>
public class InterprocessCache : IDisposable
{
#region [ Members ]
// Constants
private const int WriteEvent = 0;
private const int ReadEvent = 1;
/// <summary>
/// Default maximum retry attempts allowed for loading <see cref="InterprocessCache"/>.
/// </summary>
public const int DefaultMaximumRetryAttempts = 10;
/// <summary>
/// Default wait interval, in milliseconds, before retrying load of <see cref="InterprocessCache"/>.
/// </summary>
public const double DefaultRetryDelayInterval = 200.0D;
// Fields
private string m_fileName; // Path and file name of file needing interprocess synchronization
private byte[] m_fileData; // Data loaded or to be saved
private bool m_autoSave; // Flag to auto save when file data has changed
private InterprocessReaderWriterLock m_fileLock; // Interprocess reader/writer lock used to synchronize file access
private ReaderWriterLockSlim m_dataLock; // Thread level reader/writer lock used to synchronize file data access
private ManualResetEventSlim m_loadIsReady; // Wait handle used so that system will wait for file data load
private ManualResetEventSlim m_saveIsReady; // Wait handle used so that system will wait for file data save
private FileSystemWatcher m_fileWatcher; // Optional file watcher used to reload changes
private readonly int m_maximumConcurrentLocks; // Maximum concurrent reader locks allowed
private int m_maximumRetryAttempts; // Maximum retry attempts allowed for loading file
private readonly BitArray m_retryQueue; // Retry event queue
private Timer m_retryTimer; // File I/O retry timer
private long m_lastRetryTime; // Time of last retry attempt
private int m_retryCount; // Total number of retries attempted so far
private bool m_disposed; // Class disposed flag
#endregion
#region [ Constructors ]
/// <summary>
/// Creates a new instance of the <see cref="InterprocessCache"/>.
/// </summary>
public InterprocessCache()
: this(InterprocessReaderWriterLock.DefaultMaximumConcurrentLocks)
{
}
/// <summary>
/// Creates a new instance of the <see cref="InterprocessCache"/> with the specified number of <paramref name="maximumConcurrentLocks"/>.
/// </summary>
/// <param name="maximumConcurrentLocks">Maximum concurrent reader locks to allow.</param>
public InterprocessCache(int maximumConcurrentLocks)
{
// Initialize field values
m_dataLock = new ReaderWriterLockSlim();
m_loadIsReady = new ManualResetEventSlim(false);
m_saveIsReady = new ManualResetEventSlim(true);
m_maximumConcurrentLocks = maximumConcurrentLocks;
m_maximumRetryAttempts = DefaultMaximumRetryAttempts;
m_retryQueue = new BitArray(2);
m_fileData = new byte[0];
// Setup retry timer
m_retryTimer = new Timer();
m_retryTimer.Elapsed += m_retryTimer_Elapsed;
m_retryTimer.AutoReset = false;
m_retryTimer.Interval = DefaultRetryDelayInterval;
}
/// <summary>
/// Releases the unmanaged resources before the <see cref="InterprocessCache"/> object is reclaimed by <see cref="GC"/>.
/// </summary>
~InterprocessCache()
{
Dispose(false);
}
#endregion
#region [ Properties ]
/// <summary>
/// Path and file name for the cache needing interprocess synchronization.
/// </summary>
public virtual string FileName
{
get
{
return m_fileName;
}
set
{
if ((object)value == null)
throw new ArgumentNullException("FileName", "FileName cannot be null");
m_fileName = FilePath.GetAbsolutePath(value);
// Initialize reader/writer lock for given file name
if ((object)m_fileLock != null)
m_fileLock.Dispose();
m_fileLock = new InterprocessReaderWriterLock(m_fileName, m_maximumConcurrentLocks);
}
}
/// <summary>
/// Gets or sets file data for the cache to be saved or that has been loaded.
/// </summary>
/// <remarks>
/// Setting value to <c>null</c> will create a zero-length file.
/// </remarks>
public virtual byte[] FileData
{
get
{
// Calls to this property are blocked until data is available
WaitForLoad();
m_dataLock.EnterReadLock();
try
{
// Make a copy of the file data for external use
if ((object)m_fileData != null)
return m_fileData.Copy(0, m_fileData.Length);
}
finally
{
m_dataLock.ExitReadLock();
}
return null;
}
set
{
if ((object)m_fileName == null)
throw new ArgumentNullException("FileName", "FileName property must be defined before setting FileData");
bool dataChanged = false;
// If value is null, assume user means zero-length file
if ((object)value == null)
value = new byte[0];
m_dataLock.EnterWriteLock();
try
{
if (m_autoSave)
dataChanged = (m_fileData.CompareTo(value) != 0);
m_fileData = value;
}
finally
{
m_dataLock.ExitWriteLock();
}
// Initiate save if data has changed
if (dataChanged)
{
m_saveIsReady.Reset();
ThreadPool.QueueUserWorkItem(SynchronizedWrite);
}
}
}
/// <summary>
/// Gets or sets flag that determines if <see cref="InterprocessCache"/> should automatically initiate a save when <see cref="FileData"/> has been updated.
/// </summary>
public virtual bool AutoSave
{
get
{
return m_autoSave;
}
set
{
m_autoSave = value;
}
}
/// <summary>
/// Gets or sets flag that enables system to monitor for changes in <see cref="FileName"/> and automatically reload <see cref="FileData"/>.
/// </summary>
public virtual bool ReloadOnChange
{
get
{
return (object)m_fileWatcher != null;
}
set
{
if (value && (object)m_fileWatcher == null)
{
if ((object)m_fileName == null)
throw new ArgumentNullException("FileName", "FileName property must be defined before enabling ReloadOnChange");
// Setup file watcher to monitor for external updates
m_fileWatcher = new FileSystemWatcher();
m_fileWatcher.Path = FilePath.GetDirectoryName(m_fileName);
m_fileWatcher.Filter = FilePath.GetFileName(m_fileName);
m_fileWatcher.EnableRaisingEvents = true;
m_fileWatcher.Changed += m_fileWatcher_Changed;
}
else if (!value && (object)m_fileWatcher != null)
{
// Disable file watcher
m_fileWatcher.Changed -= m_fileWatcher_Changed;
m_fileWatcher.Dispose();
m_fileWatcher = null;
}
}
}
/// <summary>
/// Gets the maximum concurrent reader locks allowed.
/// </summary>
public virtual int MaximumConcurrentLocks
{
get
{
return m_maximumConcurrentLocks;
}
}
/// <summary>
/// Maximum retry attempts allowed for loading or saving cache file data.
/// </summary>
public virtual int MaximumRetryAttempts
{
get
{
return m_maximumRetryAttempts;
}
set
{
m_maximumRetryAttempts = value;
}
}
/// <summary>
/// Wait interval, in milliseconds, before retrying load or save of cache file data.
/// </summary>
public virtual double RetryDelayInterval
{
get
{
return m_retryTimer.Interval;
}
set
{
m_retryTimer.Interval = value;
}
}
#endregion
#region [ Methods ]
/// <summary>
/// Releases all the resources used by the <see cref="InterprocessCache"/> object.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases the unmanaged resources used by the <see cref="InterprocessCache"/> object and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
if (!m_disposed)
{
try
{
if (disposing)
{
if ((object)m_fileWatcher != null)
{
m_fileWatcher.Changed -= m_fileWatcher_Changed;
m_fileWatcher.Dispose();
}
m_fileWatcher = null;
if ((object)m_retryTimer != null)
{
m_retryTimer.Elapsed -= m_retryTimer_Elapsed;
m_retryTimer.Dispose();
}
m_retryTimer = null;
if ((object)m_loadIsReady != null)
m_loadIsReady.Dispose();
m_loadIsReady = null;
if ((object)m_saveIsReady != null)
m_saveIsReady.Dispose();
m_saveIsReady = null;
if ((object)m_dataLock != null)
m_dataLock.Dispose();
m_dataLock = null;
if ((object)m_fileLock != null)
m_fileLock.Dispose();
m_fileLock = null;
m_fileName = null;
}
}
finally
{
m_disposed = true; // Prevent duplicate dispose.
}
}
}
/// <summary>
/// Initiates interprocess synchronized cache file save.
/// </summary>
/// <remarks>
/// Subclasses should always call <see cref="WaitForLoad()"/> before calling this method.
/// </remarks>
public virtual void Save()
{
if ((object)m_fileName == null)
throw new ArgumentNullException("FileName", "FileName is null, cannot initiate save");
if ((object)m_fileData == null)
throw new ArgumentNullException("FileData", "FileData is null, cannot initiate save");
m_saveIsReady.Reset();
ThreadPool.QueueUserWorkItem(SynchronizedWrite);
}
/// <summary>
/// Initiates interprocess synchronized cache file load.
/// </summary>
/// <remarks>
/// Subclasses should always call <see cref="WaitForLoad()"/> before calling this method.
/// </remarks>
public virtual void Load()
{
if ((object)m_fileName == null)
throw new ArgumentNullException("FileName", "FileName is null, cannot initiate load");
m_loadIsReady.Reset();
ThreadPool.QueueUserWorkItem(SynchronizedRead);
}
/// <summary>
/// Blocks current thread and waits for any pending load to complete; wait time is <c><see cref="RetryDelayInterval"/> * <see cref="MaximumRetryAttempts"/></c>.
/// </summary>
public virtual void WaitForLoad()
{
WaitForLoad((int)(RetryDelayInterval * MaximumRetryAttempts));
}
/// <summary>
/// Blocks current thread and waits for specified <paramref name="millisecondsTimeout"/> for any pending load to complete.
/// </summary>
/// <param name="millisecondsTimeout">The number of milliseconds to wait, or <see cref="Timeout.Infinite"/>(-1) to wait indefinitely.</param>
public virtual void WaitForLoad(int millisecondsTimeout)
{
// Calls to this method are blocked until data is available
if (!m_loadIsReady.IsSet && !m_loadIsReady.Wait(millisecondsTimeout))
throw new TimeoutException("Timeout waiting to read data from " + m_fileName);
}
/// <summary>
/// Blocks current thread and waits for any pending save to complete; wait time is <c><see cref="RetryDelayInterval"/> * <see cref="MaximumRetryAttempts"/></c>.
/// </summary>
public virtual void WaitForSave()
{
WaitForSave((int)(RetryDelayInterval * MaximumRetryAttempts));
}
/// <summary>
/// Blocks current thread and waits for specified <paramref name="millisecondsTimeout"/> for any pending save to complete.
/// </summary>
/// <param name="millisecondsTimeout">The number of milliseconds to wait, or <see cref="Timeout.Infinite"/>(-1) to wait indefinitely.</param>
public virtual void WaitForSave(int millisecondsTimeout)
{
// Calls to this method are blocked until data is saved
if (!m_saveIsReady.IsSet && !m_saveIsReady.Wait(millisecondsTimeout))
throw new TimeoutException("Timeout waiting to save data to " + m_fileName);
}
/// <summary>
/// Handles serialization of file to disk; virtual method allows customization (e.g., pre-save encryption and/or data merge).
/// </summary>
/// <param name="fileStream"><see cref="FileStream"/> used to serialize data.</param>
/// <param name="fileData">File data to be serialized.</param>
/// <remarks>
/// Consumers overriding this method should not directly call <see cref="FileData"/> property to avoid potential dead-locks.
/// </remarks>
protected virtual void SaveFileData(FileStream fileStream, byte[] fileData)
{
fileStream.Write(fileData, 0, fileData.Length);
}
/// <summary>
/// Handles deserialization of file from disk; virtual method allows customization (e.g., pre-load decryption and/or data merge).
/// </summary>
/// <param name="fileStream"><see cref="FileStream"/> used to deserialize data.</param>
/// <returns>Deserialized file data.</returns>
/// <remarks>
/// Consumers overriding this method should not directly call <see cref="FileData"/> property to avoid potential dead-locks.
/// </remarks>
protected virtual byte[] LoadFileData(FileStream fileStream)
{
return fileStream.ReadStream();
}
/// <summary>
/// Synchronously writes file data when no reads are active.
/// </summary>
private void SynchronizedWrite(object state)
{
try
{
if (!m_disposed)
{
if (m_fileLock.TryEnterWriteLock((int)m_retryTimer.Interval))
{
FileStream fileStream = null;
try
{
fileStream = new FileStream(m_fileName, FileMode.Create, FileAccess.Write, FileShare.None);
if (m_dataLock.TryEnterReadLock((int)m_retryTimer.Interval))
{
try
{
// Disable file watch notification before update
if ((object)m_fileWatcher != null)
m_fileWatcher.EnableRaisingEvents = false;
SaveFileData(fileStream, m_fileData);
m_saveIsReady.Set();
}
finally
{
m_dataLock.ExitReadLock();
// Reenable file watch notification
if ((object)m_fileWatcher != null)
Lines 1–500 of 699. Load more
//****************************************************************************************************** // InterprocessCache.cs - Gbtc // // Copyright © 2012, Grid Protection Alliance. All Rights Reserved. // // Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See // the NOTICE file distributed with this work for additional information regarding copyright ownership. // The GPA licenses this file to you under the Eclipse Public License -v 1.0 (the "License"); you may // not use this file except in compliance with the License. You may obtain a copy of the License at: // // http://www.opensource.org/licenses/eclipse-1.0.php // // Unless agreed to in writing, the subject software distributed under the License is distributed on an // "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the // License for the specific language governing permissions and limitations. // // Code Modification History: // ---------------------------------------------------------------------------------------------------- // 03/21/2011 - J. Ritchie Carroll // Generated original version of source code. // 04/06/2011 - J. Ritchie Carroll // Added Flush() method to block current thread and wait for any pending save to complete. // 12/14/2012 - Starlynn Danyelle Gilliam // Modified Header. // //****************************************************************************************************** using System; using System.Collections; using System.IO; using System.Threading; using System.Timers; using GSF.Collections; using GSF.Threading; using Timer = System.Timers.Timer; namespace GSF.IO { /// <summary> /// Represents a serialized data cache that can be saved or read from multiple applications using interprocess synchronization. /// </summary> /// <remarks> /// Note that all file data in this class gets serialized to and from memory, as such, the design intention for this class is for /// use with smaller data sets such as serialized lists or dictionaries that need interprocess synchronized loading and saving. /// </remarks> public class InterprocessCache : IDisposable { #region [ Members ] // Constants private const int WriteEvent = 0; private const int ReadEvent = 1; /// <summary> /// Default maximum retry attempts allowed for loading <see cref="InterprocessCache"/>. /// </summary> public const int DefaultMaximumRetryAttempts = 10; /// <summary> /// Default wait interval, in milliseconds, before retrying load of <see cref="InterprocessCache"/>. /// </summary> public const double DefaultRetryDelayInterval = 200.0D; // Fields private string m_fileName; // Path and file name of file needing interprocess synchronization private byte[] m_fileData; // Data loaded or to be saved private bool m_autoSave; // Flag to auto save when file data has changed private InterprocessReaderWriterLock m_fileLock; // Interprocess reader/writer lock used to synchronize file access private ReaderWriterLockSlim m_dataLock; // Thread level reader/writer lock used to synchronize file data access private ManualResetEventSlim m_loadIsReady; // Wait handle used so that system will wait for file data load private ManualResetEventSlim m_saveIsReady; // Wait handle used so that system will wait for file data save private FileSystemWatcher m_fileWatcher; // Optional file watcher used to reload changes private readonly int m_maximumConcurrentLocks; // Maximum concurrent reader locks allowed private int m_maximumRetryAttempts; // Maximum retry attempts allowed for loading file private readonly BitArray m_retryQueue; // Retry event queue private Timer m_retryTimer; // File I/O retry timer private long m_lastRetryTime; // Time of last retry attempt private int m_retryCount; // Total number of retries attempted so far private bool m_disposed; // Class disposed flag #endregion #region [ Constructors ] /// <summary> /// Creates a new instance of the <see cref="InterprocessCache"/>. /// </summary> public InterprocessCache() : this(InterprocessReaderWriterLock.DefaultMaximumConcurrentLocks) { } /// <summary> /// Creates a new instance of the <see cref="InterprocessCache"/> with the specified number of <paramref name="maximumConcurrentLocks"/>. /// </summary> /// <param name="maximumConcurrentLocks">Maximum concurrent reader locks to allow.</param> public InterprocessCache(int maximumConcurrentLocks) { // Initialize field values m_dataLock = new ReaderWriterLockSlim(); m_loadIsReady = new ManualResetEventSlim(false); m_saveIsReady = new ManualResetEventSlim(true); m_maximumConcurrentLocks = maximumConcurrentLocks; m_maximumRetryAttempts = DefaultMaximumRetryAttempts; m_retryQueue = new BitArray(2); m_fileData = new byte[0]; // Setup retry timer m_retryTimer = new Timer(); m_retryTimer.Elapsed += m_retryTimer_Elapsed; m_retryTimer.AutoReset = false; m_retryTimer.Interval = DefaultRetryDelayInterval; } /// <summary> /// Releases the unmanaged resources before the <see cref="InterprocessCache"/> object is reclaimed by <see cref="GC"/>. /// </summary> ~InterprocessCache() { Dispose(false); } #endregion #region [ Properties ] /// <summary> /// Path and file name for the cache needing interprocess synchronization. /// </summary> public virtual string FileName { get { return m_fileName; } set { if ((object)value == null) throw new ArgumentNullException("FileName", "FileName cannot be null"); m_fileName = FilePath.GetAbsolutePath(value); // Initialize reader/writer lock for given file name if ((object)m_fileLock != null) m_fileLock.Dispose(); m_fileLock = new InterprocessReaderWriterLock(m_fileName, m_maximumConcurrentLocks); } } /// <summary> /// Gets or sets file data for the cache to be saved or that has been loaded. /// </summary> /// <remarks> /// Setting value to <c>null</c> will create a zero-length file. /// </remarks> public virtual byte[] FileData { get { // Calls to this property are blocked until data is available WaitForLoad(); m_dataLock.EnterReadLock(); try { // Make a copy of the file data for external use if ((object)m_fileData != null) return m_fileData.Copy(0, m_fileData.Length); } finally { m_dataLock.ExitReadLock(); } return null; } set { if ((object)m_fileName == null) throw new ArgumentNullException("FileName", "FileName property must be defined before setting FileData"); bool dataChanged = false; // If value is null, assume user means zero-length file if ((object)value == null) value = new byte[0]; m_dataLock.EnterWriteLock(); try { if (m_autoSave) dataChanged = (m_fileData.CompareTo(value) != 0); m_fileData = value; } finally { m_dataLock.ExitWriteLock(); } // Initiate save if data has changed if (dataChanged) { m_saveIsReady.Reset(); ThreadPool.QueueUserWorkItem(SynchronizedWrite); } } } /// <summary> /// Gets or sets flag that determines if <see cref="InterprocessCache"/> should automatically initiate a save when <see cref="FileData"/> has been updated. /// </summary> public virtual bool AutoSave { get { return m_autoSave; } set { m_autoSave = value; } } /// <summary> /// Gets or sets flag that enables system to monitor for changes in <see cref="FileName"/> and automatically reload <see cref="FileData"/>. /// </summary> public virtual bool ReloadOnChange { get { return (object)m_fileWatcher != null; } set { if (value && (object)m_fileWatcher == null) { if ((object)m_fileName == null) throw new ArgumentNullException("FileName", "FileName property must be defined before enabling ReloadOnChange"); // Setup file watcher to monitor for external updates m_fileWatcher = new FileSystemWatcher(); m_fileWatcher.Path = FilePath.GetDirectoryName(m_fileName); m_fileWatcher.Filter = FilePath.GetFileName(m_fileName); m_fileWatcher.EnableRaisingEvents = true; m_fileWatcher.Changed += m_fileWatcher_Changed; } else if (!value && (object)m_fileWatcher != null) { // Disable file watcher m_fileWatcher.Changed -= m_fileWatcher_Changed; m_fileWatcher.Dispose(); m_fileWatcher = null; } } } /// <summary> /// Gets the maximum concurrent reader locks allowed. /// </summary> public virtual int MaximumConcurrentLocks { get { return m_maximumConcurrentLocks; } } /// <summary> /// Maximum retry attempts allowed for loading or saving cache file data. /// </summary> public virtual int MaximumRetryAttempts { get { return m_maximumRetryAttempts; } set { m_maximumRetryAttempts = value; } } /// <summary> /// Wait interval, in milliseconds, before retrying load or save of cache file data. /// </summary> public virtual double RetryDelayInterval { get { return m_retryTimer.Interval; } set { m_retryTimer.Interval = value; } } #endregion #region [ Methods ] /// <summary> /// Releases all the resources used by the <see cref="InterprocessCache"/> object. /// </summary> public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// <summary> /// Releases the unmanaged resources used by the <see cref="InterprocessCache"/> object and optionally releases the managed resources. /// </summary> /// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param> protected virtual void Dispose(bool disposing) { if (!m_disposed) { try { if (disposing) { if ((object)m_fileWatcher != null) { m_fileWatcher.Changed -= m_fileWatcher_Changed; m_fileWatcher.Dispose(); } m_fileWatcher = null; if ((object)m_retryTimer != null) { m_retryTimer.Elapsed -= m_retryTimer_Elapsed; m_retryTimer.Dispose(); } m_retryTimer = null; if ((object)m_loadIsReady != null) m_loadIsReady.Dispose(); m_loadIsReady = null; if ((object)m_saveIsReady != null) m_saveIsReady.Dispose(); m_saveIsReady = null; if ((object)m_dataLock != null) m_dataLock.Dispose(); m_dataLock = null; if ((object)m_fileLock != null) m_fileLock.Dispose(); m_fileLock = null; m_fileName = null; } } finally { m_disposed = true; // Prevent duplicate dispose. } } } /// <summary> /// Initiates interprocess synchronized cache file save. /// </summary> /// <remarks> /// Subclasses should always call <see cref="WaitForLoad()"/> before calling this method. /// </remarks> public virtual void Save() { if ((object)m_fileName == null) throw new ArgumentNullException("FileName", "FileName is null, cannot initiate save"); if ((object)m_fileData == null) throw new ArgumentNullException("FileData", "FileData is null, cannot initiate save"); m_saveIsReady.Reset(); ThreadPool.QueueUserWorkItem(SynchronizedWrite); } /// <summary> /// Initiates interprocess synchronized cache file load. /// </summary> /// <remarks> /// Subclasses should always call <see cref="WaitForLoad()"/> before calling this method. /// </remarks> public virtual void Load() { if ((object)m_fileName == null) throw new ArgumentNullException("FileName", "FileName is null, cannot initiate load"); m_loadIsReady.Reset(); ThreadPool.QueueUserWorkItem(SynchronizedRead); } /// <summary> /// Blocks current thread and waits for any pending load to complete; wait time is <c><see cref="RetryDelayInterval"/> * <see cref="MaximumRetryAttempts"/></c>. /// </summary> public virtual void WaitForLoad() { WaitForLoad((int)(RetryDelayInterval * MaximumRetryAttempts)); } /// <summary> /// Blocks current thread and waits for specified <paramref name="millisecondsTimeout"/> for any pending load to complete. /// </summary> /// <param name="millisecondsTimeout">The number of milliseconds to wait, or <see cref="Timeout.Infinite"/>(-1) to wait indefinitely.</param> public virtual void WaitForLoad(int millisecondsTimeout) { // Calls to this method are blocked until data is available if (!m_loadIsReady.IsSet && !m_loadIsReady.Wait(millisecondsTimeout)) throw new TimeoutException("Timeout waiting to read data from " + m_fileName); } /// <summary> /// Blocks current thread and waits for any pending save to complete; wait time is <c><see cref="RetryDelayInterval"/> * <see cref="MaximumRetryAttempts"/></c>. /// </summary> public virtual void WaitForSave() { WaitForSave((int)(RetryDelayInterval * MaximumRetryAttempts)); } /// <summary> /// Blocks current thread and waits for specified <paramref name="millisecondsTimeout"/> for any pending save to complete. /// </summary> /// <param name="millisecondsTimeout">The number of milliseconds to wait, or <see cref="Timeout.Infinite"/>(-1) to wait indefinitely.</param> public virtual void WaitForSave(int millisecondsTimeout) { // Calls to this method are blocked until data is saved if (!m_saveIsReady.IsSet && !m_saveIsReady.Wait(millisecondsTimeout)) throw new TimeoutException("Timeout waiting to save data to " + m_fileName); } /// <summary> /// Handles serialization of file to disk; virtual method allows customization (e.g., pre-save encryption and/or data merge). /// </summary> /// <param name="fileStream"><see cref="FileStream"/> used to serialize data.</param> /// <param name="fileData">File data to be serialized.</param> /// <remarks> /// Consumers overriding this method should not directly call <see cref="FileData"/> property to avoid potential dead-locks. /// </remarks> protected virtual void SaveFileData(FileStream fileStream, byte[] fileData) { fileStream.Write(fileData, 0, fileData.Length); } /// <summary> /// Handles deserialization of file from disk; virtual method allows customization (e.g., pre-load decryption and/or data merge). /// </summary> /// <param name="fileStream"><see cref="FileStream"/> used to deserialize data.</param> /// <returns>Deserialized file data.</returns> /// <remarks> /// Consumers overriding this method should not directly call <see cref="FileData"/> property to avoid potential dead-locks. /// </remarks> protected virtual byte[] LoadFileData(FileStream fileStream) { return fileStream.ReadStream(); } /// <summary> /// Synchronously writes file data when no reads are active. /// </summary> private void SynchronizedWrite(object state) { try { if (!m_disposed) { if (m_fileLock.TryEnterWriteLock((int)m_retryTimer.Interval)) { FileStream fileStream = null; try { fileStream = new FileStream(m_fileName, FileMode.Create, FileAccess.Write, FileShare.None); if (m_dataLock.TryEnterReadLock((int)m_retryTimer.Interval)) { try { // Disable file watch notification before update if ((object)m_fileWatcher != null) m_fileWatcher.EnableRaisingEvents = false; SaveFileData(fileStream, m_fileData); m_saveIsReady.Set(); } finally { m_dataLock.ExitReadLock(); // Reenable file watch notification if ((object)m_fileWatcher != null)