using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using DelftTools.Utils.Collections;
using DelftTools.Utils.Collections.Generic;
using log4net;
namespace DelftTools.Shell.Core.Workflow
{
///
/// Class fires activities asynch and generates state changed
///
//TODO: increase coverage and simplity this class. It is on the verge of unmanagable
//TODO: make this stuff (lists) threadsafe damnit!
public class ActivityRunner : IActivityRunner
{
public event EventHandler IsRunningChanged;
public event EventHandler ActivitiesCollectionChanged;
private const int maxRunningTaskCount = 1;
private static readonly ILog log = LogManager.GetLogger(typeof(ActivityRunner));
private readonly IList runningTasks = new List();
private readonly IList todoTasks = new List();
private readonly IEventedList activities;
private bool running; // synchronization flag
private bool wasRunning;
public ActivityRunner()
{
activities = new EventedList();
activities.CollectionChanged += HandleActivitiesCollectionChanged;
}
///
/// Provides an evented summary of the current activities (running and todo).
/// DO NOT ADD TO THIS LIST useEnqueue instead
///
public IEventedList Activities
{
get
{
return activities;
}
}
private void HandleActivitiesCollectionChanged(object sender, NotifyCollectionChangingEventArgs e)
{
//only local changes...get this in the EventedList...
if (sender != activities)
{
return;
}
if (e.Action == NotifyCollectionChangeAction.Add)
{
((IActivity) e.Item).StatusChanged += HandleActivityStatusChanged;
}
else if (e.Action == NotifyCollectionChangeAction.Remove)
{
((IActivity) e.Item).StatusChanged -= HandleActivityStatusChanged;
}
}
private void HandleActivityStatusChanged(object sender, ActivityStatusChangedEventArgs e)
{
if (e.NewStatus == ActivityStatus.Cancelled)
{
var task = GetRunningTasksThreadSafe().First(t => Equals(t.Activity, sender));
if (ActivityStatusChanged != null)
{
//bubble the activity status change..
ActivityStatusChanged(sender, e);
}
running = true;
//done running,
using (new TryLock(runningTasks))
{
runningTasks.Remove(task);
CleanUp(task);
activities.Remove(task.Activity);
}
OnIsRunningChanged();
return;
}
if (ActivityStatusChanged != null)
{
//bubble the activity status change..
ActivityStatusChanged(sender, e);
}
OnIsRunningChanged();
}
private void StartTaskIfPossible(Action beforeActualRun = null)
{
//we can run if we are not busy and have something todo ;)
while (true)
{
AsyncActivityRunner taskToRun;
using (new TryLock(runningTasks))
{
if (runningTasks.Count >= maxRunningTaskCount || (todoTasks.Count <= 0))
{
break;
}
taskToRun = todoTasks[0];
runningTasks.Add(taskToRun);
todoTasks.RemoveAt(0);
}
Debug.WriteLine("Run activity {0}", (taskToRun.Activity.Name));
if (beforeActualRun != null)
{
beforeActualRun();
}
taskToRun.Run();
//'pop' the first task (FIFO)
}
}
private void Completed(object sender, EventArgs e)
{
var task = (AsyncActivityRunner) sender;
if (task.Activity.Status == ActivityStatus.Cancelled)
{
Debug.WriteLine(string.Format("Cancelled activity {0}", task.Activity.Name));
}
else
{
Debug.WriteLine(string.Format("Finished activity {0}", task.Activity.Name));
}
try
{
OnTaskCompleted(task);
}
finally
{
running = true;
using (new TryLock(runningTasks))
{
runningTasks.Remove(task);
CleanUp(task);
}
// continue with the next activity
StartTaskIfPossible();
OnIsRunningChanged();
}
}
private void CleanUp(AsyncActivityRunner task)
{
task.Completed -= Completed;
activities.Remove(task.Activity);
}
private void OnIsRunningChanged()
{
running = false;
var isRunning = IsRunning;
if (wasRunning != isRunning)
{
//TODO: get some logic to determine whether it really changed. (P.Changed?)
if (IsRunningChanged != null)
{
IsRunningChanged(this, EventArgs.Empty);
}
}
wasRunning = isRunning;
}
private void OnTaskCompleted(AsyncActivityRunner sender)
{
if (!sender.CompletedSuccesfully)
{
if (String.IsNullOrEmpty(sender.Activity.Name))
{
log.Error("An error occured while running a background activity: ", sender.Exception);
}
else
{
log.Error(String.Format("An error occured while running activity {0}: ", sender.Activity.Name), sender.Exception);
}
}
if (ActivityCompleted != null)
{
ActivityCompleted(this, new ActivityEventArgs(sender.Activity));
}
OnIsRunningChanged();
}
#region IActivityRunner Members
public bool IsRunning
{
get
{
using (new TryLock(runningTasks)) //prevent deadlock
{
return runningTasks.Count > 0 || activities.Count > 0 || running;
}
}
}
public bool IsRunningActivity(IActivity activity)
{
if (activity == null)
{
return false;
}
return GetRunningTasksThreadSafe().Any(task => task.Activity == activity) && !running;
}
private IEnumerable GetRunningTasksThreadSafe()
{
using (new TryLock(runningTasks)) // lock modifications
{
return runningTasks.ToList(); // return a local copy
}
}
public void Enqueue(IActivity activity)
{
var task = new AsyncActivityRunner(activity, RunActivity);
task.Completed += Completed;
using (new TryLock(runningTasks))
{
todoTasks.Add(task);
activities.Add(activity);
}
Debug.WriteLine(string.Format("Enqueued activity {0}", activity.Name));
//TODO: it might already be running so running would not be changed.
//fix and review
StartTaskIfPossible(OnIsRunningChanged);
}
public static void RunActivity(IActivity activity)
{
try
{
activity.Initialize();
if (activity.Status == ActivityStatus.Failed)
{
throw new Exception(string.Format("Initialization of {0} has failed.", activity.Name));
}
while (activity.Status != ActivityStatus.Done)
{
if (activity.Status == ActivityStatus.Cancelled)
{
log.WarnFormat("Execution of {0} has been canceled.", activity.Name);
break;
}
if (activity.Status != ActivityStatus.WaitingForData)
{
activity.Execute();
}
if (activity.Status == ActivityStatus.Failed)
{
throw new Exception(string.Format("Execution of {0} has failed.", activity.Name));
}
}
if (activity.Status != ActivityStatus.Cancelled)
{
activity.Finish();
if (activity.Status == ActivityStatus.Failed)
{
throw new Exception(string.Format("Finishing of {0} has failed.", activity.Name));
}
}
activity.Cleanup();
if (activity.Status == ActivityStatus.Failed)
{
throw new Exception(string.Format("Clean up of {0} has failed.", activity.Name));
}
}
catch (Exception exception)
{
Console.WriteLine(exception.Message); //for build server debugging
log.Error(exception.Message);
}
finally
{
try
{
if (activity.Status != ActivityStatus.Cleaned)
{
activity.Cleanup();
}
}
catch (Exception)
{
log.ErrorFormat("Clean up of {0} has failed.", activity.Name);
}
}
}
public void Cancel(IActivity activity)
{
var task = GetRunningTasksThreadSafe().FirstOrDefault(t => t.Activity == activity);
if (task != null)
{
//TODO: let the task cancel and complete.cleanup should be in Completed
task.Cancel();
running = true;
return;
}
//or remove it from todo
using (new TryLock(runningTasks))
{
task = todoTasks.ToList().FirstOrDefault(t => t.Activity == activity);
if (task != null)
{
todoTasks.Remove(task);
CleanUp(task);
}
}
OnIsRunningChanged();
}
//TODO: make cancelAll use cancel for each activity.
public void CancelAll()
{
foreach (var task in GetRunningTasksThreadSafe())
{
running = true;
task.Cancel();
}
//empty the todo on a cancel
using (new TryLock(runningTasks))
{
var currentTodoTasks = todoTasks.ToList();
foreach (var task in currentTodoTasks)
{
CleanUp(task);
if (activities.Contains(task.Activity))
{
activities.Remove(task.Activity);
}
}
todoTasks.Clear();
}
OnIsRunningChanged();
}
public event EventHandler ActivityCompleted;
public event EventHandler ActivityStatusChanged;
#endregion
}
///
/// Poor man's locking mechanism under danger of deadlock due to invokes & events :-(
///
public class TryLock : IDisposable
{
private readonly bool hasLock;
private object lockObject;
public TryLock(object lockObject, int timeOut = 100)
{
this.lockObject = lockObject;
hasLock = Monitor.TryEnter(lockObject, timeOut);
}
public void Dispose()
{
if (hasLock)
{
Monitor.Exit(lockObject);
}
lockObject = null;
}
}
}