Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ internal void Import(IEnumerable<SerializableSettingsCharacter> serial)
character.Dispose();
}

// Import the characters, their identies, etc
// Import the characters, their identities, etc
Items.Clear();
foreach (SerializableSettingsCharacter serialCharacter in serial)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,11 @@ internal void Import(ICollection<MonitoredCharacterSettings> monitoredCharacters
Items.Add(character);
character.Monitored = true;
character.UISettings = characterSettings.Settings;

EveMonClient.OnMonitoredCharactersChanged();
}

// Notify once after all characters have been imported, instead of
// per-character which caused N redundant LayoutTabPages + Settings.Save calls
EveMonClient.OnMonitoredCharactersChanged();
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,10 @@ public void Notify(NotificationEventArgs notification)

case NotificationBehaviour.Merge:
// Merge the notifications with the same key
// Snapshot Items to prevent IndexOutOfRangeException if the notification
// chain (via ThreadSafeInvoke) re-enters and modifies Items concurrently
long key = notification.InvalidationKey;
foreach (NotificationEventArgs other in Items.Where(x => x.InvalidationKey == key))
foreach (NotificationEventArgs other in Items.ToArray().Where(x => x.InvalidationKey == key))
{
notification.Append(other);
}
Expand Down
38 changes: 35 additions & 3 deletions src/EVEMon.Common/Extensions/EventHandlerExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.ComponentModel;
using System.Linq;
using System.Windows.Forms;

namespace EVEMon.Common.Extensions
{
Expand Down Expand Up @@ -32,6 +33,11 @@ public static void ThreadSafeInvoke(this EventHandler eventHandler, object sende
// Check if our target requires an Invoke
if (sync != null && sync.InvokeRequired)
{
// Skip if the target control is disposed or has no handle;
// invoking on such a control causes unpredictable exceptions.
if (sync is Control ctrl && (ctrl.IsDisposed || !ctrl.IsHandleCreated))
continue;

// Yes it does, so invoke the handler using the target's BeginInvoke method, but wait for it to finish
// This is preferable to using Invoke so that if an exception is thrown its presented
// in the context of the handler, not the current thread
Expand All @@ -43,7 +49,13 @@ public static void ThreadSafeInvoke(this EventHandler eventHandler, object sende
}
catch (ObjectDisposedException)
{
// Ignore, already cleaned up -- code was likely changed to use `using`.
// Control was disposed between BeginInvoke and EndInvoke
}
catch (IndexOutOfRangeException) when (sync is Control c && (c.IsDisposed || !c.IsHandleCreated))
{
// Control.EndInvoke throws IndexOutOfRangeException when the
// control's internal async result list is cleared during disposal
// or handle recreation (e.g. resize triggering layout changes).
}

continue;
Expand Down Expand Up @@ -81,11 +93,31 @@ public static void ThreadSafeInvoke<T>(this EventHandler<T> eventHandler, object
// Check if our target requires an Invoke
if (sync != null && sync.InvokeRequired)
{
// Skip if the target control is disposed or has no handle;
// invoking on such a control causes unpredictable exceptions.
if (sync is Control ctrl && (ctrl.IsDisposed || !ctrl.IsHandleCreated))
continue;

// Yes it does, so invoke the handler using the target's BeginInvoke method, but wait for it to finish
// This is preferable to using Invoke so that if an exception is thrown its presented
// in the context of the handler, not the current thread
IAsyncResult result = sync.BeginInvoke(handler, new[] { sender, e });
sync.EndInvoke(result);
IAsyncResult result = sync.BeginInvoke(handler, new[] { sender, (object)e });

try
{
sync.EndInvoke(result);
}
catch (ObjectDisposedException)
{
// Control was disposed between BeginInvoke and EndInvoke
}
catch (IndexOutOfRangeException) when (sync is Control c && (c.IsDisposed || !c.IsHandleCreated))
{
// Control.EndInvoke throws IndexOutOfRangeException when the
// control's internal async result list is cleared during disposal
// or handle recreation (e.g. resize triggering layout changes).
}

continue;
}

Expand Down
20 changes: 17 additions & 3 deletions src/EVEMon.Common/Helpers/FileHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,23 @@ public static void CopyOrWarnTheUser(string srcFileName, string destFileName)
/// <returns></returns>
private static void CopyFile(string srcFileName, string destFileName)
{
using (Stream sourceStream = Util.GetFileStream(srcFileName, FileMode.Open, FileAccess.Read, FileShare.Read))
using (Stream destStream = Util.GetFileStream(destFileName, FileMode.Create, FileAccess.Write))
sourceStream.CopyTo(destStream);
// Use a retry loop to handle transient IOException from concurrent file access
const int maxRetries = 3;
for (int attempt = 0; attempt < maxRetries; attempt++)
{
try
{
using (Stream sourceStream = Util.GetFileStream(srcFileName, FileMode.Open, FileAccess.Read, FileShare.Read))
using (Stream destStream = Util.GetFileStream(destFileName, FileMode.Create, FileAccess.Write))
sourceStream.CopyTo(destStream);
return;
}
catch (IOException) when (attempt < maxRetries - 1)
{
// Brief delay before retry to let the other handle close
System.Threading.Thread.Sleep(100 * (attempt + 1));
}
}
}

/// <summary>
Expand Down
27 changes: 21 additions & 6 deletions src/EVEMon.Common/Models/CCPCharacter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ private CCPCharacter(CharacterIdentity identity, Guid guid)
EveMonClient.CharacterPlaneteryPinsCompleted += EveMonClient_CharacterPlaneteryPinsCompleted;
EveMonClient.ESIKeyInfoUpdated += EveMonClient_ESIKeyInfoUpdated;
EveMonClient.EveIDToNameUpdated += EveMonClient_EveIDToNameUpdated;
EveMonClient.TimerTick += EveMonClient_TimerTick;
// Note: TimerTick subscription is deferred until data querying starts
// to reduce UI thread overhead during startup
}

/// <summary>
Expand Down Expand Up @@ -473,8 +474,8 @@ private void Import(SerializableCCPCharacter serial)
// EVE notifications IDs
EVENotifications.Import(serial.EveNotificationsIDs);

// Kill Logs
KillLog.ImportFromCacheFile();
// Kill Logs are loaded lazily on first access to avoid
// synchronous file I/O during startup

// Fire the global event
EveMonClient.OnCharacterUpdated(this);
Expand Down Expand Up @@ -758,19 +759,27 @@ private void EveMonClient_ESIKeyInfoUpdated(object sender, EventArgs e)
if (!Identity.ESIKeys.Any())
return;

bool needsTimerTick = false;

if (m_characterDataQuerying == null && Identity.ESIKeys.Any())
{
m_characterDataQuerying = new CharacterDataQuerying(this);
ResetLastAPIUpdates(m_lastAPIUpdates.Where(lastUpdate => Enum.IsDefined(
typeof(ESIAPICharacterMethods), lastUpdate.Method)));
needsTimerTick = true;
}

if (m_corporationDataQuerying == null && Identity.ESIKeys.Any())
{
m_corporationDataQuerying = new CorporationDataQuerying(this);
ResetLastAPIUpdates(m_lastAPIUpdates.Where(lastUpdate => Enum.IsDefined(
typeof(ESIAPICorporationMethods), lastUpdate.Method)));
needsTimerTick = true;
}

// Subscribe to TimerTick only once data querying is active
if (needsTimerTick)
EveMonClient.TimerTick += EveMonClient_TimerTick;
}

/// <summary>
Expand Down Expand Up @@ -895,11 +904,17 @@ private void EveMonClient_CharacterIndustryJobsCompleted(object sender, Industry
if (!CorporationIndustryJobs.Any(job => job.ActiveJobState ==
ActiveJobState.Ready && !job.NotificationSend))
{
EveMonClient.Notifications.NotifyCharacterIndustryJobCompletion(this,
m_jobsCompletedForCharacter);
// Snapshot the list before notifying — the notification constructor
// iterates the enumerable, and Clear() below (or another timer tick
// calling AddRange) would modify the list mid-iteration, causing
// IndexOutOfRangeException.
var completedSnapshot = m_jobsCompletedForCharacter.ToList();

// Now that we have send the notification clear the list
// Now that we have a snapshot, clear the list before notifying
m_jobsCompletedForCharacter.Clear();

EveMonClient.Notifications.NotifyCharacterIndustryJobCompletion(this,
completedSnapshot);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,14 @@ private void UpdateOnTimerTick()
bool isCorporateMonitor = true;
if (Items.Count > 0)
{
// Snapshot the items list to avoid IndexOutOfRangeException if the
// notification chain causes Items to be modified during iteration
var snapshot = Items.ToList();

// Add the not notified "Ready" jobs to the completed list
var jobsCompleted = new LinkedList<IndustryJob>();
var characterJobs = new LinkedList<IndustryJob>();
foreach (IndustryJob job in Items)
foreach (IndustryJob job in snapshot)
{
if (job.IsActive && job.TTC.Length == 0 && !job.NotificationSend)
{
Expand Down
41 changes: 40 additions & 1 deletion src/EVEMon.Common/Models/Collections/KillLogCollection.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections;
using System.Collections.Generic;
using EVEMon.Common.Collections;
using EVEMon.Common.Enumerations.CCPAPI;
Expand All @@ -9,11 +10,12 @@

namespace EVEMon.Common.Models.Collections
{
public sealed class KillLogCollection : ReadonlyCollection<KillLog>
public sealed class KillLogCollection : ReadonlyCollection<KillLog>, IEnumerable<KillLog>, IEnumerable
{
private readonly CCPCharacter m_ccpCharacter;
private int m_killMailCounter;
private readonly List<KillLog> m_pendingItems;
private bool m_cacheLoaded;

#region Constructor

Expand All @@ -26,6 +28,43 @@ internal KillLogCollection(CCPCharacter ccpCharacter)
m_ccpCharacter = ccpCharacter;
m_killMailCounter = 0;
m_pendingItems = new List<KillLog>(32);
m_cacheLoaded = false;
}

#endregion


#region Lazy Cache Loading

/// <summary>
/// Ensures the kill log cache file has been loaded. Called lazily on first
/// enumeration to avoid synchronous file I/O during character construction.
/// </summary>
private void EnsureCacheLoaded()
{
if (m_cacheLoaded)
return;

m_cacheLoaded = true;
ImportFromCacheFile();
}

/// <summary>
/// Gets the enumerator, ensuring the cache is loaded first.
/// </summary>
IEnumerator<KillLog> IEnumerable<KillLog>.GetEnumerator()
{
EnsureCacheLoaded();
return Items.GetEnumerator();
}

/// <summary>
/// Gets the enumerator, ensuring the cache is loaded first.
/// </summary>
IEnumerator IEnumerable.GetEnumerator()
{
EnsureCacheLoaded();
return Items.GetEnumerator();
}

#endregion
Expand Down
Loading