using System;
using System.IO;
using System.Threading.Tasks;
using Artemis.Storage.Entities.Plugins;
namespace Artemis.Core
{
///
/// Represents an feature of a certain type provided by a plugin
///
public abstract class PluginFeature : CorePropertyChanged, IDisposable
{
private bool _isEnabled;
///
/// Gets the plugin feature info related to this feature
///
public PluginFeatureInfo Info { get; internal set; } = null!; // Will be set right after construction
///
/// Gets the plugin that provides this feature
///
public Plugin Plugin { get; internal set; } = null!; // Will be set right after construction
///
/// Gets the profiler that can be used to take profiling measurements
///
public Profiler Profiler { get; internal set; } = null!; // Will be set right after construction
///
/// Gets whether the plugin is enabled
///
public bool IsEnabled
{
get => _isEnabled;
internal set => SetAndNotify(ref _isEnabled, value);
}
internal int AutoEnableAttempts { get; set; }
///
/// Gets the identifier of this plugin feature
///
public string Id => $"{GetType().FullName}-{Plugin.Guid.ToString().Substring(0, 8)}"; // Not as unique as a GUID but good enough and stays readable
internal PluginFeatureEntity Entity { get; set; } = null!; // Will be set right after construction
///
/// Called when the feature is activated
///
public abstract void Enable();
///
/// Called when the feature is deactivated or when Artemis shuts down
///
public abstract void Disable();
///
/// Occurs when the feature is enabled
///
public event EventHandler? Enabled;
///
/// Occurs when the feature is disabled
///
public event EventHandler? Disabled;
///
/// Releases the unmanaged resources used by the plugin feature and optionally releases the managed resources.
///
///
/// to release both managed and unmanaged resources;
/// to release only unmanaged resources.
///
protected virtual void Dispose(bool disposing)
{
if (disposing) InternalDisable();
}
///
/// Triggers the Enabled event
///
protected virtual void OnEnabled()
{
Enabled?.Invoke(this, EventArgs.Empty);
}
///
/// Triggers the Disabled event
///
protected virtual void OnDisabled()
{
Disabled?.Invoke(this, EventArgs.Empty);
}
internal void StartUpdateMeasure()
{
Profiler.StartMeasurement("Update");
}
internal void StopUpdateMeasure()
{
Profiler.StopMeasurement("Update");
}
internal void StartRenderMeasure()
{
Profiler.StartMeasurement("Render");
}
internal void StopRenderMeasure()
{
Profiler.StopMeasurement("Render");
}
internal void SetEnabled(bool enable, bool isAutoEnable = false)
{
if (enable == IsEnabled)
return;
if (Plugin == null)
throw new ArtemisCoreException("Cannot enable a plugin feature that is not associated with a plugin");
lock (Plugin)
{
if (!Plugin.IsEnabled)
throw new ArtemisCoreException("Cannot enable a plugin feature of a disabled plugin");
if (!enable)
{
// Even if disable failed, still leave it in a disabled state to avoid more issues
InternalDisable();
IsEnabled = false;
OnDisabled();
return;
}
try
{
if (isAutoEnable)
AutoEnableAttempts++;
if (isAutoEnable && GetLockFileCreated())
{
// Don't wrap existing lock exceptions, simply rethrow them
if (Info.LoadException is ArtemisPluginLockException)
throw Info.LoadException;
throw new ArtemisPluginLockException(Info.LoadException);
}
CreateLockFile();
IsEnabled = true;
// Allow up to 15 seconds for plugins to activate.
// This means plugins that need more time should do their long running tasks in a background thread, which is intentional
// This would've been a perfect match for Thread.Abort but that didn't make it into .NET Core
Task enableTask = Task.Run(InternalEnable);
if (!enableTask.Wait(TimeSpan.FromSeconds(15)))
throw new ArtemisPluginException(Plugin, "Plugin load timeout");
Info.LoadException = null;
AutoEnableAttempts = 0;
OnEnabled();
}
// If enable failed, put it back in a disabled state
catch (Exception e)
{
IsEnabled = false;
Info.LoadException = e;
throw;
}
finally
{
// Clean up the lock file unless the failure was due to the lock file
// After all, we failed but not miserably :)
if (Info.LoadException is not ArtemisPluginLockException)
DeleteLockFile();
}
}
}
internal virtual void InternalEnable()
{
Enable();
}
internal virtual void InternalDisable()
{
if (IsEnabled)
Disable();
}
///
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#region Loading
internal void CreateLockFile()
{
if (Plugin == null)
throw new ArtemisCoreException("Cannot lock a plugin feature that is not associated with a plugin");
File.Create(Plugin.ResolveRelativePath($"{GetType().FullName}.lock")).Close();
}
internal void DeleteLockFile()
{
if (Plugin == null)
throw new ArtemisCoreException("Cannot lock a plugin feature that is not associated with a plugin");
if (GetLockFileCreated())
File.Delete(Plugin.ResolveRelativePath($"{GetType().FullName}.lock"));
}
internal bool GetLockFileCreated()
{
if (Plugin == null)
throw new ArtemisCoreException("Cannot lock a plugin feature that is not associated with a plugin");
return File.Exists(Plugin.ResolveRelativePath($"{GetType().FullName}.lock"));
}
#endregion
#region Timed updates
///
/// Registers a timed update that whenever the plugin is enabled calls the provided at the
/// provided
///
///
/// The interval at which the update should occur
///
/// The action to call every time the interval has passed. The delta time parameter represents the
/// time passed since the last update in seconds
///
/// An optional name used in exceptions and profiling
/// The resulting plugin update registration which can be used to stop the update
public TimedUpdateRegistration AddTimedUpdate(TimeSpan interval, Action action, string? name = null)
{
if (action == null)
throw new ArgumentNullException(nameof(action));
return new TimedUpdateRegistration(this, interval, action, name);
}
///
/// Registers a timed update that whenever the plugin is enabled calls the provided at the
/// provided
///
///
/// The interval at which the update should occur
///
/// The async action to call every time the interval has passed. The delta time parameter
/// represents the time passed since the last update in seconds
///
/// An optional name used in exceptions and profiling
/// The resulting plugin update registration
public TimedUpdateRegistration AddTimedUpdate(TimeSpan interval, Func asyncAction, string? name = null)
{
if (asyncAction == null)
throw new ArgumentNullException(nameof(asyncAction));
return new TimedUpdateRegistration(this, interval, asyncAction, name);
}
#endregion
}
}