using PleaseIgnore.IntelMap.Properties; using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.Contracts; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Threading; namespace PleaseIgnore.IntelMap { /// /// Manages the list of watched by an instance /// of . /// /// [DefaultEvent("IntelReported")] public class IntelChannelContainer : Component, IContainer, INestedContainer, INotifyPropertyChanged { // Default value of the ChannelUpdateInterval property internal const string defaultUpdateInterval = "24:00:00"; // Default value of the RetryInterval property internal const string defaultRetryInterval = "00:15:00"; // Thread Synchronization object private readonly object syncRoot = new object(); // List of active IntelChannel objects private readonly List channels = new List(); // Timer object used to fetch updated channel lists private readonly Timer updateTimer; // The current channel processing state [ContractPublicPropertyName("Status")] private volatile IntelStatus status; // The update period for the channel list [ContractPublicPropertyName("ChannelUpdateInterval")] private TimeSpan? updateInterval = TimeSpan.Parse( defaultUpdateInterval, CultureInfo.InvariantCulture); // The network retry period for the channel list [ContractPublicPropertyName("RetryInterval")] private TimeSpan retryInterval = TimeSpan.Parse( defaultRetryInterval, CultureInfo.InvariantCulture); // The intel upload count [ContractPublicPropertyName("IntelCount")] private int uploadCount; // URI to use when fetching the channel list [ContractPublicPropertyName("ChannelListUri")] private Uri channelListUri = IntelExtensions.ChannelsUrl; // Directory to use when overriding the IntelChannel's Path [ContractPublicPropertyName("Path")] public string logDirectory; // The contents of the channel list the last time we fetched it private string[] channelList; /// /// Initializes a new instance of the /// class. /// public IntelChannelContainer() : this(null) { Contract.Ensures(Status == IntelStatus.Stopped); Contract.Ensures(Container == null); } /// /// Initializes a new instance of the /// class and adds it to the specified container. /// public IntelChannelContainer(IContainer container) { Contract.Ensures(Status == IntelStatus.Stopped); Contract.Ensures(Container == container); this.updateTimer = new Timer(this.timer_Callback); if (container != null) { container.Add(this); } } /// /// Occurs when a new log entry has been read from the chat logs. /// public event EventHandler IntelReported; /// /// Occurs when a property value changes. /// public event PropertyChangedEventHandler PropertyChanged; /// /// Gets the current operational status of the /// object and its children. /// public IntelStatus Status { get { return this.status; } private set { Contract.Ensures(Status == value); if (this.status != value) { this.status = value; this.OnPropertyChanged(new PropertyChangedEventArgs("Status")); } } } /// /// Gets a value indicating whether the /// is currently running and watching for log entries. /// public bool IsRunning { get { return this.status.IsRunning(); } } /// /// Gets an instance of /// /// /// Calling on an /// will remove it from /// . It may be readded when the /// channel list is redownloaded. /// public IntelChannelCollection Channels { get { lock (this.syncRoot) { return new IntelChannelCollection(this.channels.ToArray()); } } } /// /// Gets or sets the time between downloads of the intel /// channel list. /// /// /// The time between downloads of the intel channel list or /// to disable periodic downloads of /// the channel list. /// [DefaultValue(typeof(TimeSpan), defaultUpdateInterval)] public TimeSpan? ChannelUpdateInterval { get { Contract.Ensures(!Contract.Result().HasValue || (Contract.Result() > TimeSpan.Zero)); return this.updateInterval; } set { Contract.Requires(!IsRunning); Contract.Requires( !value.HasValue || (value > TimeSpan.Zero), "value"); Contract.Ensures(ChannelUpdateInterval == value); if (this.updateInterval != value) { this.updateInterval = value; this.OnPropertyChanged(new PropertyChangedEventArgs("ChannelUpdatePeriod")); } } } /// /// Gets or sets the time to wait after a failed attempt to /// download the channel list before trying again. /// [DefaultValue(typeof(TimeSpan), defaultRetryInterval)] public TimeSpan RetryInterval { get { Contract.Ensures(Contract.Result() > TimeSpan.Zero); return this.retryInterval; } set { Contract.Requires(!IsRunning); Contract.Requires( value > TimeSpan.Zero, "value"); Contract.Ensures(RetryInterval == value); if (this.retryInterval != value) { this.retryInterval = value; this.OnPropertyChanged(new PropertyChangedEventArgs("RetryInterval")); } } } /// /// Gets or sets the to use when downloading /// the channel list. /// [AmbientValue((string)null)] public string ChannelListUri { get { Contract.Ensures(!String.IsNullOrEmpty(Contract.Result())); return (this.channelListUri ?? IntelExtensions.ChannelsUrl).OriginalString; } set { Contract.Requires(!IsRunning); var uri = (value != null) ? new Uri(value) : IntelExtensions.ChannelsUrl; if (!uri.IsAbsoluteUri) { // TODO: Proper exception throw new ArgumentException(); } if (this.channelListUri != uri) { this.channelListUri = uri; this.OnPropertyChanged(new PropertyChangedEventArgs("ChannelListUri")); } } } /// /// Gets or sets the directory to search for log files. /// [DefaultValue((string)null)] public string Path { get { return this.logDirectory; } set { lock (this.syncRoot) { if (value != this.logDirectory) { this.logDirectory = value; this.channels.ForEach(x => x.Path = (value ?? IntelChannel.DefaultPath)); this.OnPropertyChanged(new PropertyChangedEventArgs("Path")); } } } } /// /// Gets an object that can be used for synchronization of /// the component state. /// /// /// An that can be used by classes /// derived from to /// synchronize their own internal state. /// protected object SyncRoot { get { Contract.Ensures(Contract.Result() != null); return this.syncRoot; } } /// /// Gets the number of reports that have been made by this /// . /// public int IntelCount { get { return this.uploadCount; } } /// /// Downloads the channel list and begins the acquisition of log /// entries from the EVE chat logs. This method enables /// events. /// public void Start() { Contract.Requires( Status != IntelStatus.Disposed, null); Contract.Requires( Status != IntelStatus.FatalError); Contract.Ensures(IsRunning); lock (this.syncRoot) { if (this.status == IntelStatus.Stopped) { try { this.Status = IntelStatus.Starting; this.OnStart(); this.Status = IntelStatus.Waiting; this.OnPropertyChanged(new PropertyChangedEventArgs("IsRunning")); } catch { this.Status = IntelStatus.FatalError; throw; } } } } /// /// Stops the from providing /// location data and events. /// events will no longer be raised. /// public void Stop() { Contract.Ensures(!IsRunning); lock (this.syncRoot) { if (this.IsRunning) { try { this.Status = IntelStatus.Stopping; this.OnStop(); this.Status = IntelStatus.Stopped; } catch { this.Status = IntelStatus.FatalError; throw; } finally { this.OnPropertyChanged(new PropertyChangedEventArgs("IsRunning")); } } } } /// protected override void Dispose(bool disposing) { Contract.Ensures(Status == IntelStatus.Disposed); Contract.Ensures(!IsRunning); if (disposing) { lock (this.syncRoot) { if (this.status != IntelStatus.Disposed) { try { this.Status = IntelStatus.Disposing; this.updateTimer.Dispose(); channels.ForEach(x => x.Dispose()); } catch { // Ignore any exceptions during disposal } finally { channels.Clear(); } } } this.IntelReported = null; this.PropertyChanged = null; } this.status = IntelStatus.Disposed; base.Dispose(disposing); } /// /// Creates an instance of to manage /// the monitoring of an intel channel log file. /// /// /// The base file name of the intel channel to monitor. /// /// /// An instance of to use when /// monitoring the log file. /// /// /// Classes derived from are /// free to override and completely /// replace the logic without calling the base implementation. /// In this case, the derivative class must register handlers to /// call and /// under the appropriate circumstances. The /// must be initialzied to a proper /// linking instance of . /// protected virtual IntelChannel CreateChannel(string channelName) { Contract.Requires(!String.IsNullOrEmpty(channelName)); Contract.Ensures(Contract.Result() != null); Contract.Ensures(Contract.Result().Site != null); Contract.Ensures(Contract.Result().Site.Container == this); Contract.Ensures(Contract.Result().Name == channelName); var channel = new IntelChannel(); channel.Site = new ChannelSite(this, channel, channelName); if (this.logDirectory != null) { channel.Path = this.logDirectory; } channel.IntelReported += channel_IntelReported; channel.PropertyChanged += channel_PropertyChanged; return channel; } /// /// Raises the event. /// /// /// Arguments of the event being raised. /// /// /// makes no changes to the internal /// object state and can be safely called at any time. If the /// logic within is replaced, the /// event needs to be /// forwarded to . /// protected virtual void OnIntelReported(IntelEventArgs e) { Contract.Requires(e != null, "e"); Interlocked.Increment(ref this.uploadCount); this.OnPropertyChanged(new PropertyChangedEventArgs("IntelCount")); var handler = this.IntelReported; if (handler != null) { handler(this, e); } } /// /// Raises the event. /// /// /// Arguments of the event being raised. /// /// /// makes no changes to the internal /// object state and can be safely called at any time. The /// event is scheduled for asynchronous /// handling by the . /// protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) { Contract.Requires(e != null, "e"); Debug.Assert(String.IsNullOrEmpty(e.PropertyName) || (this.GetType().GetProperty(e.PropertyName) != null)); var handler = this.PropertyChanged; if (handler != null) { ThreadPool.QueueUserWorkItem(delegate(object state) { handler(this, e); }); } } /// /// Called after has been called. /// /// /// will be called with synchronized /// access to the object state. /// protected virtual void OnStart() { this.channels.ForEach(x => x.Start()); this.updateTimer.Change(0, Timeout.Infinite); } /// /// Called periodically to download the intel channel list and /// update the list of components. /// /// /// As downloads data from a remote /// server, locks should not be maintained on object state during /// the call to as this may lead to /// significantly impairments of the UI. /// protected virtual void OnUpdateList() { // A lot may have happened... if (!this.IsRunning) { return; } string[] list; try { // Download the new list outside the lock list = GetChannelList(this.channelListUri); } catch (WebException) { // Retry in a few minutes lock (this.syncRoot) { if (this.IsRunning) { this.updateTimer.Change(this.retryInterval, TimeSpan.Zero); } } return; } // Alter program state within the lock lock (this.syncRoot) { if (!this.IsRunning) { return; } if (this.channelList == null) { // Initializing the channel list this.channels.AddRange(list.Select(x => this.CreateChannel(x))); this.channelList = list; this.channels.ForEach(x => x.Start()); this.OnPropertyChanged(new PropertyChangedEventArgs("Channels")); } else { // Patching the existing channel list var toAdd = list .Except(this.channelList, StringComparer.OrdinalIgnoreCase) .Select(x => this.CreateChannel(x)) .ToList(); var toRemove = this.channels .Where(x => !list.Contains(x.Name, StringComparer.OrdinalIgnoreCase)) .ToList(); this.channels.AddRange(toAdd); this.channelList = list; toRemove.ForEach(x => x.Dispose()); toAdd.ForEach(x => x.Start()); if ((toAdd.Count > 0) || (toRemove.Count > 0)) { this.OnPropertyChanged(new PropertyChangedEventArgs("Channels")); } } // Schedule the next update this.OnUpdateStatus(); if (this.updateInterval.HasValue) { this.updateTimer.Change(this.updateInterval.Value, TimeSpan.Zero); } } } /// /// Updates the property to reflect the aggregate /// state of those components in . /// /// /// If a derived class replaces , it /// must ensure that is called /// when there is a change to the /// property by monitoring the /// event. /// protected virtual void OnUpdateStatus() { lock (this.syncRoot) { if (this.IsRunning) { this.Status = this.channels.Aggregate( IntelStatus.Waiting, (sum, x) => IntelExtensions.Combine(sum, x.Status)); } } } /// /// Called after has been called. /// /// /// will be called with synchronized /// access to the object state. /// protected virtual void OnStop() { this.channels.ForEach(x => x.Stop()); this.updateTimer.Change(Timeout.Infinite, Timeout.Infinite); } /// /// Calls . /// private void channel_IntelReported(object sender, IntelEventArgs e) { Contract.Requires(e != null); this.OnIntelReported(e); } /// /// Calls , trapping any exceptions. /// private void channel_PropertyChanged(object sender, PropertyChangedEventArgs e) { Contract.Requires(e != null); if (String.IsNullOrEmpty(e.PropertyName) || (e.PropertyName == "Status")) { this.OnUpdateStatus(); } } /// /// Calls , trapping any exceptions. /// private void timer_Callback(object state) { try { this.OnUpdateList(); } catch { // Fail on error lock (this.syncRoot) { if (this.status != IntelStatus.Disposed) { this.updateTimer.Dispose(); this.Status = IntelStatus.FatalError; } } throw; } } /// /// Code Contracts class invariants. /// [ContractInvariantMethod] private void ObjectInvariant() { Contract.Invariant(!this.updateInterval.HasValue || (this.updateInterval > TimeSpan.Zero)); Contract.Invariant(this.retryInterval > TimeSpan.Zero); Contract.Invariant(this.channelListUri != null); Contract.Invariant(this.uploadCount >= 0); Contract.Invariant(Contract.ForAll(this.channels, x => x != null)); } /// ComponentCollection IContainer.Components { get { lock (this.syncRoot) { return new ComponentCollection(channels.ToArray()); } } } /// void IContainer.Add(IComponent component, string name) { throw new NotSupportedException(Resources.IntelChannelCollection_ReadOnly); } /// void IContainer.Add(IComponent component) { throw new NotSupportedException(Resources.IntelChannelCollection_ReadOnly); } /// void IContainer.Remove(IComponent component) { // Called when the channel is being disposed if (this.status != IntelStatus.Disposing) { lock (this.syncRoot) { var count = this.channels.RemoveAll(x => x == component); if (count > 0) { this.OnPropertyChanged(new PropertyChangedEventArgs("Channels")); } } } } /// IComponent INestedContainer.Owner { get { return this; } } /// /// Downloads the list of channels to monitor from the Test /// Alliance Intel Map server. /// /// /// A of channel /// filenames. /// /// /// There was a problem contacting the server or the server /// response was invalid. /// public static string[] GetChannelList() { Contract.Ensures(Contract.Result() != null); Contract.Ensures(Contract.Result().Length > 0); return GetChannelList(IntelExtensions.ChannelsUrl); } /// /// Downloads the list of channels to monitor from a specific /// reporting server. /// /// /// The server URI to download maps from. /// /// /// A of channel /// filenames. /// /// /// There was a problem contacting the server or the server /// response was invalid. /// public static string[] GetChannelList(Uri serviceUri) { Contract.Requires(serviceUri != null, "serviceUri"); Contract.Requires(serviceUri.IsAbsoluteUri); Contract.Ensures(Contract.Result() != null); Contract.Ensures(Contract.Result().Length > 0); // TODO: More thorough sanity check of the server response var channels = WebRequest .Create(serviceUri) .GetResponse() .ReadContent() .Split('\n', '\r') .Select(x => x.Trim()) .Where(x => x.Length > 0) .Select(x => x.Split(',')[0]) .ToArray(); if (channels.Length == 0) { throw new WebException(Resources.IntelException); } else { return channels; } } /// /// Implementation of for linking an instance of /// to its parent /// . /// private class ChannelSite : INestedSite { [ContractPublicPropertyName("Container")] private readonly IntelChannelContainer container; [ContractPublicPropertyName("Component")] private readonly IntelChannel component; [ContractPublicPropertyName("Name")] private readonly string name; internal ChannelSite(IntelChannelContainer container, IntelChannel component, string name) { Contract.Requires(container != null); Contract.Requires(component != null); Contract.Requires(!String.IsNullOrEmpty(name)); this.container = container; this.component = component; this.name = name; } public IComponent Component { get { return this.component; } } public IContainer Container { get { return this.container; } } public bool DesignMode { get { return this.container.DesignMode; } } public string Name { get { return this.name; } set { throw new NotSupportedException(); } } public string FullName { get { var site = this.container.Site; var siteName = (site != null) ? site.Name : null; if (!String.IsNullOrEmpty(siteName)) { return siteName + '.' + this.name; } else { return this.name; } } } public object GetService(Type serviceType) { if (serviceType == typeof(ISite)) { return this; } else if(serviceType == typeof(INestedSite)) { return this; } else if (serviceType == typeof(IContainer)) { return this.container; } else if (serviceType == typeof(INestedContainer)) { return this.container; } else { return this.container.GetService(serviceType); } } } } }