using PleaseIgnore.IntelMap.Properties;
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
namespace PleaseIgnore.IntelMap {
///
/// Monitors the log directory for new log entries to a specific
/// channel.
///
///
/// When running, makes use of the
/// to perform background operations. The
/// following sequence of events are processed:
///
/// -
///
/// Called after the initial call to
/// . Responsible for initializing the
/// internal operations and searching the log directory for
/// possible active logs. Will not be called by subsequent
/// calls to unless an intervening call
/// to has been made.
///
/// -
///
/// Generated by an internal instance of
/// to notify us of new files
/// being created within the log directory, allowing us to
/// switch to newer log files.
///
/// -
///
/// Generated by an internal instance of
/// to notify us of changes
/// to files in the log directory, allowing us to rescan and
/// reopen log files appropriately. Unfortunately, due to
/// performance optimizations in Windows, change notifications
/// are often sent when the file is closed, not when
/// new data has been written to the file.
///
/// -
///
/// Generated periodically by the
/// to allow us to rescan the log
/// files. Has primary responsibility for generating
/// events.
///
/// -
///
/// Called after the initial call to
/// . Destroys all internal tracking
/// structures. Will be not be called by subsequent calls
/// to unless an intervening call to
/// has been made.
///
///
/// Internal and
/// members are provided for the benefit of user testing. Redefinition may
/// lead to behavior defects or loss of thread safety.
///
///
[DefaultEvent("IntelReported"), DefaultProperty("Name")]
public class IntelChannel : Component {
// Internal members should be referenced by any other class within
// PleaseIgnore.IntelMap. They are made internal purely for the
// benefit of implementing unit tests.
// Regular Expression used to break apart each entry in the log file.
private static readonly Regex Parser = new Regex(
"^\uFEFF?" + @"\[\s*(\d{4})\.(\d{2})\.(\d{2})\s+(\d{2}):(\d{2}):(\d{2})\s*\](.*)$",
RegexOptions.CultureInvariant);
// Regular Expression used to extract the timestamp from the filename.
private static readonly Regex FilenameParser = new Regex(
@"_(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})\.txt$",
RegexOptions.CultureInvariant);
// Default directory to find EVE logs
private static readonly string defaultLogDirectory = System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
"EVE",
"logs",
"Chatlogs");
// The timer period to use when scheduling the timer
private const int timerPeriod = 5000;
// The default value for expireLog
private const string defaultExpireLog = "00:30:00";
// Synchronization primitive
internal readonly object syncRoot = new object();
// Raises the periodic timer for log scanning
private readonly Timer logTimer;
// The local file system watcher object
private FileSystemWatcher watcher;
// The channel file name stub
[ContractPublicPropertyName("Name")]
private string channelFileName;
// The path to search for EVE chat logs
[ContractPublicPropertyName("Path")]
private string logDirectory = defaultLogDirectory;
// The current component status
[ContractPublicPropertyName("Status")]
private volatile IntelStatus status;
// The currently processed log file
private StreamReader reader;
// The last time we parsed a log entry from the current log
private DateTime lastEntry;
// The time to wait before calling a log file "dead"
[ContractPublicPropertyName("LogExpiration")]
private TimeSpan expireLog = TimeSpan.Parse(
defaultExpireLog,
CultureInfo.InvariantCulture);
// true if the timer is currently running
private bool timerEnabled;
///
/// Initializes a new instance of the class.
///
public IntelChannel() : this(null, null) {
}
///
/// Initializes a new instance of the class
/// with the specified .
///
///
/// The initial value for .
///
public IntelChannel(string name)
: this(name, null) {
}
///
/// Initializes a new instance of the class
/// with the specified .
///
///
/// Optional parent .
///
public IntelChannel(IContainer container)
: this(null, container) {
}
///
/// Initializes a new instance of the class
/// with the specified and .
///
///
/// The initial value for .
///
///
/// Optional parent .
///
public IntelChannel(string name, IContainer container) {
this.channelFileName = name;
this.logTimer = new Timer(this.timer_Callback);
if (container != null) {
container.Add(this);
}
}
///
~IntelChannel() {
this.Dispose(false);
}
///
/// 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 default directory to search for EVE chat logs.
///
public static string DefaultPath {
get {
Contract.Ensures(!String.IsNullOrEmpty(Contract.Result()));
return defaultLogDirectory;
}
}
///
/// Gets or sets the directory to search for log files.
///
public string Path {
get {
Contract.Ensures(!String.IsNullOrEmpty(Contract.Result()));
return this.logDirectory;
}
set {
Contract.Requires(!String.IsNullOrEmpty(value));
lock (this.syncRoot) {
if (value != this.logDirectory) {
this.logDirectory = value;
if (this.watcher != null) {
try {
this.watcher.Path = value;
this.ScanFiles();
} catch (ArgumentException) {
this.watcher.Dispose();
this.watcher = null;
this.Status = IntelStatus.InvalidPath;
}
}
this.OnPropertyChanged(new PropertyChangedEventArgs("Path"));
}
}
}
}
///
/// Gets or sets the channel name of this
///
///
/// The channel name cannot be modified when
/// is .
///
[DefaultValue((string)null)]
public string Name {
get {
Contract.Ensures(!String.IsNullOrEmpty(Contract.Result())
|| !this.IsRunning);
var channelName = this.channelFileName;
var site = this.Site;
if (!String.IsNullOrEmpty(channelName)) {
return channelName;
} else if (site != null) {
return site.Name;
} else {
return null;
}
}
set {
lock (this.syncRoot) {
if (this.IsRunning) {
throw new InvalidOperationException();
}
this.channelFileName = value;
}
}
}
///
/// Gets the number of reports that have been made by this
/// .
///
public int IntelCount { get; private set; }
///
/// Gets the log file currently being observed for new intel.
///
public FileInfo LogFile { get; private set; }
///
/// Gets the current operational status of the
/// object.
///
public virtual IntelStatus Status {
get {
return this.status;
}
private set {
Contract.Ensures(Status == value);
if (this.status != value) {
this.status = value;
this.UpdateTimer();
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 or sets the time between log entries before declaring
/// that the channel is no longer being monitored.
///
[DefaultValue(typeof(TimeSpan), defaultExpireLog)]
public TimeSpan LogExpiration {
get {
Contract.Ensures(Contract.Result() > TimeSpan.Zero);
return this.expireLog;
}
set {
Contract.Requires(
value > TimeSpan.Zero,
"value");
Contract.Ensures(LogExpiration == value);
lock (this) {
if (this.expireLog != value) {
this.expireLog = value;
this.OnPropertyChanged(new PropertyChangedEventArgs("LogExpiration"));
}
}
}
}
///
/// Initiate the acquisition of log entries from the EVE chat logs. This
/// method enables events.
///
public virtual void Start() {
Contract.Requires(
Status != IntelStatus.Disposed,
null);
Contract.Requires(
!String.IsNullOrEmpty(Name));
Contract.Ensures(Status != IntelStatus.Stopped);
Contract.Ensures(IsRunning);
lock (this.syncRoot) {
if (this.status == IntelStatus.Stopped) {
this.Status = IntelStatus.Starting;
this.channelFileName = this.Name;
this.OnStart();
}
}
}
///
/// Stops the from providing location data and events.
///
public virtual void Stop() {
Contract.Ensures((Status == IntelStatus.Stopped)
|| (Status == IntelStatus.Disposed));
Contract.Ensures(!IsRunning);
lock (this.syncRoot) {
if ((this.status != IntelStatus.Stopped)
|| (this.status == IntelStatus.Disposed)) {
this.Status = IntelStatus.Stopping;
this.OnStop();
this.UpdateTimer();
}
}
}
///
protected override void Dispose(bool disposing) {
Contract.Ensures(Status == IntelStatus.Disposed);
if (disposing) {
lock (this.syncRoot) {
if (this.status != IntelStatus.Disposed) {
// Normal shutdown
this.Stop();
this.Status = IntelStatus.Disposed;
// Dispose child objects
this.logTimer.Dispose();
// Clear any lingering object references
this.IntelReported = null;
this.PropertyChanged = null;
}
}
} else {
// We really can't touch anything safely
this.status = IntelStatus.Disposed;
}
base.Dispose(disposing);
}
///
public override string ToString() {
return String.Format(
CultureInfo.CurrentCulture,
Resources.IntelChannel_ToString,
this.GetType().Name,
this.Name ?? Resources.IntelChannel_NoName,
this.Status);
}
///
/// Creates the instance of used
/// to monitor the file system.
///
///
/// An instance of that will be
/// used to monitor the directory for the creation/modification
/// of log files.
///
///
/// will be called from within
/// synchronized members so should not attempt to perform any
/// additional synchronization itself.
///
protected virtual FileSystemWatcher CreateFileSystemWatcher() {
Contract.Ensures(Contract.Result() != null);
var watcher = new FileSystemWatcher();
watcher.BeginInit();
watcher.Changed += this.watcher_Changed;
watcher.Created += this.watcher_Created;
watcher.EnableRaisingEvents = true;
watcher.Filter = this.Name + "_*.txt";
watcher.IncludeSubdirectories = false;
watcher.NotifyFilter = NotifyFilters.Size | NotifyFilters.LastWrite
| NotifyFilters.DirectoryName | NotifyFilters.FileName;
watcher.Path = this.Path;
watcher.EndInit();
return watcher;
}
///
/// Raises the event.
///
///
/// Arguments of the event being raised.
///
///
/// will be called from within
/// synchronized members so should not attempt to perform any
/// additional synchronization itself.
///
internal protected void OnIntelReported(IntelEventArgs e) {
Contract.Requires(e != null, "e");
ThreadPool.QueueUserWorkItem(delegate(object state) {
Contract.Requires(state is IntelEventArgs);
var handler = this.IntelReported;
if (handler != null) {
handler(this, (IntelEventArgs)state);
}
}, e);
this.lastEntry = DateTime.UtcNow;
++this.IntelCount;
this.OnPropertyChanged(new PropertyChangedEventArgs("IntelCount"));
}
///
/// Raises the event.
///
///
/// Arguments of the event being raised.
///
///
/// will be called from within
/// synchronized members so should not attempt to perform any
/// additional synchronization itself.
///
internal protected void OnPropertyChanged(PropertyChangedEventArgs e) {
Contract.Requires(e != null, "e");
Debug.Assert(String.IsNullOrEmpty(e.PropertyName)
|| (GetType().GetProperty(e.PropertyName) != null));
ThreadPool.QueueUserWorkItem(delegate(object state) {
Contract.Requires(state is PropertyChangedEventArgs);
var handler = this.PropertyChanged;
if (handler != null) {
handler(this, (PropertyChangedEventArgs)state);
}
}, e);
}
///
/// Called after has been called.
///
///
/// will be called with synchronized
/// access to the object state.
///
protected virtual void OnStart() {
Contract.Requires(
Status == IntelStatus.Starting);
Contract.Ensures((Status == IntelStatus.Active)
|| (Status == IntelStatus.Waiting)
|| (Status == IntelStatus.InvalidPath));
// Create the file system watcher object
try {
this.watcher = this.CreateFileSystemWatcher();
} catch(ArgumentException) {
this.Status = IntelStatus.InvalidPath;
}
// Open the log file with the latest timestamp in its filename
this.ScanFiles();
}
///
/// Called when a new log file is created for the channel we
/// are monitoring.
///
///
/// Instance of describing the
/// new file.
///
///
/// will be called with synchronized
/// access to the object state.
///
protected virtual void OnFileCreated(FileSystemEventArgs e) {
Contract.Requires(e != null, "e");
// Assume it's going to be a better file....for now
OpenFile(new FileInfo(e.FullPath));
}
///
/// Called when a log file associated with the channel we are
/// monitoring has been modified.
///
///
/// Instance of describing the
/// modified file.
///
///
/// will be called with synchronized
/// access to the object state.
///
protected virtual void OnFileChanged(FileSystemEventArgs e) {
Contract.Requires(e != null, "e");
// Only process this message if we have nothing else to go on
if (this.reader == null) {
this.OpenFile(new FileInfo(e.FullPath));
}
}
///
/// Called every couple of seconds to sweep the log file for
/// new entries.
///
///
/// will be called with synchronized
/// access to the object state.
///
protected virtual void OnTick() {
if (this.watcher == null) {
// Try (again) to create the watcher object
try {
this.watcher = this.CreateFileSystemWatcher();
this.Status = IntelStatus.Waiting;
this.ScanFiles();
} catch (ArgumentException) {
// Still doesn't seem to exist
}
}
if (this.reader != null) {
// Read new log entries from the current log
try {
string line;
while ((line = reader.ReadLine()) != null) {
Trace.WriteLine("R " + line, IntelExtensions.WebTraceCategory);
var match = Parser.Match(line);
if (match.Success) {
var e = new IntelEventArgs(
this.Name,
new DateTime(
match.Groups[1].ToInt32(),
match.Groups[2].ToInt32(),
match.Groups[3].ToInt32(),
match.Groups[4].ToInt32(),
match.Groups[5].ToInt32(),
match.Groups[6].ToInt32(),
DateTimeKind.Utc),
match.Groups[7].Value);
this.OnIntelReported(e);
}
}
} catch (IOException) {
this.CloseFile();
}
// Close the log if it has been idle for too long
if (this.lastEntry + this.expireLog < DateTime.UtcNow) {
this.CloseFile();
}
}
}
///
/// Called after has been called.
///
///
/// will be called with synchronized
/// access to the object state.
///
protected virtual void OnStop() {
Contract.Requires(
Status == IntelStatus.Stopping);
Contract.Ensures(Status == IntelStatus.Stopped);
if (this.reader != null) {
this.reader.Close();
this.reader = null;
}
if (this.LogFile != null) {
this.LogFile = null;
this.OnPropertyChanged(new PropertyChangedEventArgs("LogFile"));
}
this.Status = IntelStatus.Stopped;
}
///
/// Rescans the active directory looking for valid log files
///
///
/// if we were able to open a log file;
/// otherwise, .
///
protected bool ScanFiles() {
try {
var downtime = IntelExtensions.LastDowntime;
var file = new DirectoryInfo(this.Path)
.GetFiles(this.Name + "_*.txt", SearchOption.TopDirectoryOnly)
.Select(x => new {
File = x,
Match = FilenameParser.Match(x.Name)
})
.Where(x => x.Match.Success)
.Select(x => new {
File = x.File,
Timestamp = new DateTime(
x.Match.Groups[1].ToInt32(),
x.Match.Groups[2].ToInt32(),
x.Match.Groups[3].ToInt32(),
x.Match.Groups[4].ToInt32(),
x.Match.Groups[5].ToInt32(),
x.Match.Groups[6].ToInt32(),
DateTimeKind.Utc)
})
.Where(x => x.Timestamp > downtime)
.OrderByDescending(x => x.Timestamp)
.FirstOrDefault(x => this.OpenFile(x.File));
if (file == null) {
this.CloseFile();
}
return file != null;
} catch (IOException) {
return false;
}
}
///
/// Closes the existing log file and opens a new log file.
///
///
/// The new log file to track.
///
///
/// if we were able to open the file;
/// otherwise, .
///
internal protected bool OpenFile(FileInfo fileInfo) {
Contract.Requires(fileInfo != null, "fileInfo");
//Contract.Ensures(Status == IntelChannelStatus.Active);
var oldStatus = this.status;
var oldFile = this.LogFile;
// Close the existing file (if any)
if (this.reader != null) {
try {
this.reader.Close();
} catch (IOException) {
} finally {
this.reader = null;
}
}
// Clear the status (defer raising PropertyChanged)
this.LogFile = null;
this.status = (this.watcher != null)
? IntelStatus.Waiting
: IntelStatus.InvalidPath;
// Try to open the file stream
FileStream stream = null;
try {
stream = fileInfo.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
stream.Seek(0, SeekOrigin.End);
// XXX: We rely upon StreamReader's BOM detection. EVE seems
// to generate Little Endian UTF-16 log files. We could hard
// code that, but we don't know if that would cause other
// problems.
this.reader = new StreamReader(stream, true);
this.status = IntelStatus.Active;
this.LogFile = fileInfo;
this.lastEntry = DateTime.UtcNow;
} catch (IOException) {
// Don't leak FileStream references
if (stream != null) {
try {
stream.Close();
} catch (IOException) {
}
}
}
// Raise any deferred PropertyChanged events
if (this.status != oldStatus) {
this.OnPropertyChanged(new PropertyChangedEventArgs("Status"));
}
if (this.LogFile != oldFile) {
this.OnPropertyChanged(new PropertyChangedEventArgs("LogFile"));
}
// Success if we opened a reader
this.UpdateTimer();
return (this.reader != null);
}
///
/// Closes the existing log file
///
protected void CloseFile() {
Contract.Ensures((Status == IntelStatus.Waiting)
|| (Status == IntelStatus.InvalidPath));
if (this.reader != null) {
try {
this.reader.Close();
} catch (IOException) {
} finally {
this.reader = null;
}
}
if (this.LogFile != null) {
this.LogFile = null;
this.OnPropertyChanged(new PropertyChangedEventArgs("LogFile"));
}
this.Status = (this.watcher != null)
? IntelStatus.Waiting
: IntelStatus.InvalidPath;
}
///
/// Updates the timer for
///
private void UpdateTimer() {
switch (this.status) {
case IntelStatus.Active:
case IntelStatus.InvalidPath:
// Operations that require us to ping the filesystem regularly
if (!this.timerEnabled) {
this.logTimer.Change(timerPeriod, timerPeriod);
this.timerEnabled = true;
}
break;
case IntelStatus.Disposed:
// The timer object is no longer valid
break;
default:
// Operations when we are not actively monitoring the filesystem
if (this.timerEnabled) {
this.logTimer.Change(Timeout.Infinite, Timeout.Infinite);
this.timerEnabled = false;
}
break;
}
}
///
/// Handler for event.
///
private void watcher_Created(object sender, FileSystemEventArgs e) {
Contract.Requires(e != null);
ThreadPool.QueueUserWorkItem(delegate(object state) {
Contract.Requires(state is FileSystemEventArgs);
lock (this.syncRoot) {
if (this.IsRunning) {
this.OnFileCreated((FileSystemEventArgs)e);
}
}
}, e);
}
///
/// Handler for event.
///
private void watcher_Changed(object sender, FileSystemEventArgs e) {
Contract.Requires(e != null);
ThreadPool.QueueUserWorkItem(delegate(object state) {
Contract.Requires(state is FileSystemEventArgs);
lock (this.syncRoot) {
if (this.IsRunning) {
this.OnFileChanged((FileSystemEventArgs)e);
}
}
}, e);
}
///
/// Handler for the callback
///
private void timer_Callback(object state) {
lock (this.syncRoot) {
if (this.IsRunning) {
this.OnTick();
}
}
}
[ContractInvariantMethod]
private void ObjectInvariant() {
Contract.Invariant(!String.IsNullOrEmpty(this.channelFileName)
|| !this.IsRunning);
Contract.Invariant(!String.IsNullOrEmpty(this.logDirectory));
Contract.Invariant(this.IntelCount >= 0);
}
}
}