diff --git a/ModuleManager/ModuleManager.csproj b/ModuleManager/ModuleManager.csproj
index de2f3035..4c2f5b19 100644
--- a/ModuleManager/ModuleManager.csproj
+++ b/ModuleManager/ModuleManager.csproj
@@ -62,8 +62,12 @@
+
+
+
+
diff --git a/ModuleManager/Threading/BackgroundTask.cs b/ModuleManager/Threading/BackgroundTask.cs
new file mode 100644
index 00000000..64891c04
--- /dev/null
+++ b/ModuleManager/Threading/BackgroundTask.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Threading;
+
+namespace ModuleManager.Threading
+{
+ public static class BackgroundTask
+ {
+ public static ITaskStatus Start(Action action)
+ {
+ if (action == null) throw new ArgumentNullException(nameof(action));
+
+ TaskStatus status = new TaskStatus();
+
+ void RunAction()
+ {
+ try
+ {
+ action();
+ status.Finished();
+ }
+ catch (Exception ex)
+ {
+ status.Error(ex);
+ }
+ }
+
+ Thread thread = new Thread(RunAction);
+ thread.Start();
+
+ return new TaskStatusWrapper(status);
+ }
+ }
+}
diff --git a/ModuleManager/Threading/ITaskStatus.cs b/ModuleManager/Threading/ITaskStatus.cs
new file mode 100644
index 00000000..e60c76bd
--- /dev/null
+++ b/ModuleManager/Threading/ITaskStatus.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace ModuleManager.Threading
+{
+ public interface ITaskStatus
+ {
+ bool IsRunning { get; }
+ bool IsFinished { get; }
+ bool IsExitedWithError { get; }
+ Exception Exception { get; }
+ }
+}
diff --git a/ModuleManager/Threading/TaskStatus.cs b/ModuleManager/Threading/TaskStatus.cs
new file mode 100644
index 00000000..92f8c237
--- /dev/null
+++ b/ModuleManager/Threading/TaskStatus.cs
@@ -0,0 +1,55 @@
+using System;
+
+namespace ModuleManager.Threading
+{
+ public class TaskStatus : ITaskStatus
+ {
+ private bool isRunning = true;
+ private Exception exception = null;
+ private object lockObject = new object();
+
+ public bool IsRunning => isRunning;
+ public Exception Exception => exception;
+
+ public bool IsFinished
+ {
+ get
+ {
+ lock (lockObject)
+ {
+ return !isRunning && exception == null;
+ }
+ }
+ }
+
+ public bool IsExitedWithError
+ {
+ get
+ {
+ lock (lockObject)
+ {
+ return !isRunning && exception != null;
+ }
+ }
+ }
+
+ public void Finished()
+ {
+ lock (lockObject)
+ {
+ if (!isRunning) throw new InvalidOperationException("Task is not running");
+ isRunning = false;
+ }
+ }
+
+ public void Error(Exception exception)
+ {
+ lock(lockObject)
+ {
+ if (!isRunning) throw new InvalidOperationException("Task is not running");
+ this.exception = exception ?? throw new ArgumentNullException(nameof(exception));
+ isRunning = false;
+ }
+ }
+ }
+}
diff --git a/ModuleManager/Threading/TaskStatusWrapper.cs b/ModuleManager/Threading/TaskStatusWrapper.cs
new file mode 100644
index 00000000..eff24f78
--- /dev/null
+++ b/ModuleManager/Threading/TaskStatusWrapper.cs
@@ -0,0 +1,19 @@
+using System;
+
+namespace ModuleManager.Threading
+{
+ public class TaskStatusWrapper : ITaskStatus
+ {
+ private ITaskStatus inner;
+
+ public TaskStatusWrapper(ITaskStatus inner)
+ {
+ this.inner = inner;
+ }
+
+ public bool IsRunning => inner.IsRunning;
+ public bool IsFinished => inner.IsFinished;
+ public bool IsExitedWithError => inner.IsExitedWithError;
+ public Exception Exception => inner.Exception;
+ }
+}
diff --git a/ModuleManagerTests/ModuleManagerTests.csproj b/ModuleManagerTests/ModuleManagerTests.csproj
index 03ec31ff..bd5c52f6 100644
--- a/ModuleManagerTests/ModuleManagerTests.csproj
+++ b/ModuleManagerTests/ModuleManagerTests.csproj
@@ -67,6 +67,8 @@
+
+
diff --git a/ModuleManagerTests/Threading/BackgroundTaskTest.cs b/ModuleManagerTests/Threading/BackgroundTaskTest.cs
new file mode 100644
index 00000000..c9ebc594
--- /dev/null
+++ b/ModuleManagerTests/Threading/BackgroundTaskTest.cs
@@ -0,0 +1,72 @@
+using System;
+using Xunit;
+using ModuleManager.Threading;
+
+namespace ModuleManagerTests.Threading
+{
+ public class BackgroundTaskTest
+ {
+ [Fact]
+ public void Test__Start()
+ {
+ bool finish = false;
+ void Run()
+ {
+ while (!finish) continue;
+ }
+
+ ITaskStatus status = BackgroundTask.Start(Run);
+
+ Assert.True(status.IsRunning);
+ Assert.False(status.IsFinished);
+ Assert.False(status.IsExitedWithError);
+ Assert.Null(status.Exception);
+
+ finish = true;
+
+ while (status.IsRunning) continue;
+
+ Assert.False(status.IsRunning);
+ Assert.True(status.IsFinished);
+ Assert.False(status.IsExitedWithError);
+ Assert.Null(status.Exception);
+ }
+
+ [Fact]
+ public void Test__Start__Exception()
+ {
+ bool finish = false;
+ Exception ex = new Exception();
+ void Run()
+ {
+ while (!finish) continue;
+ throw ex;
+ }
+
+ ITaskStatus status = BackgroundTask.Start(Run);
+
+ Assert.True(status.IsRunning);
+ Assert.False(status.IsFinished);
+ Assert.False(status.IsExitedWithError);
+ Assert.Null(status.Exception);
+
+ finish = true;
+
+ while (status.IsRunning) continue;
+
+ Assert.False(status.IsRunning);
+ Assert.False(status.IsFinished);
+ Assert.True(status.IsExitedWithError);
+ Assert.Same(ex, status.Exception);
+ }
+
+ [Fact]
+ public void Test__Run__ArgumentNull()
+ {
+ Assert.Throws(delegate
+ {
+ BackgroundTask.Start(null);
+ });
+ }
+ }
+}
diff --git a/ModuleManagerTests/Threading/TaskStatusTest.cs b/ModuleManagerTests/Threading/TaskStatusTest.cs
new file mode 100644
index 00000000..a32983ff
--- /dev/null
+++ b/ModuleManagerTests/Threading/TaskStatusTest.cs
@@ -0,0 +1,121 @@
+using System;
+using Xunit;
+using ModuleManager.Threading;
+
+namespace ModuleManagerTests.Threading
+{
+ public class TaskStatusTest
+ {
+ [Fact]
+ public void Test__Cosntructor()
+ {
+ TaskStatus status = new TaskStatus();
+
+ Assert.True(status.IsRunning);
+ Assert.False(status.IsFinished);
+ Assert.False(status.IsExitedWithError);
+ Assert.Null(status.Exception);
+ }
+
+ [Fact]
+ public void TestFinished()
+ {
+ TaskStatus status = new TaskStatus();
+
+ status.Finished();
+
+ Assert.False(status.IsRunning);
+ Assert.True(status.IsFinished);
+ Assert.False(status.IsExitedWithError);
+ Assert.Null(status.Exception);
+ }
+
+ [Fact]
+ public void TestError()
+ {
+ TaskStatus status = new TaskStatus();
+ Exception ex = new Exception();
+
+ status.Error(ex);
+
+ Assert.False(status.IsRunning);
+ Assert.False(status.IsFinished);
+ Assert.True(status.IsExitedWithError);
+ Assert.Same(ex, status.Exception);
+ }
+
+ [Fact]
+ public void TestFinished__AlreadyFinished()
+ {
+ TaskStatus status = new TaskStatus();
+
+ status.Finished();
+
+ Assert.Throws(delegate
+ {
+ status.Finished();
+ });
+
+ Assert.False(status.IsRunning);
+ Assert.True(status.IsFinished);
+ Assert.False(status.IsExitedWithError);
+ Assert.Null(status.Exception);
+ }
+
+ [Fact]
+ public void TestFinished__AlreadyErrored()
+ {
+ TaskStatus status = new TaskStatus();
+ Exception ex = new Exception();
+
+ status.Error(ex);
+
+ Assert.Throws(delegate
+ {
+ status.Finished();
+ });
+
+ Assert.False(status.IsRunning);
+ Assert.False(status.IsFinished);
+ Assert.True(status.IsExitedWithError);
+ Assert.Same(ex, status.Exception);
+ }
+
+ [Fact]
+ public void TestError__AlreadyFinished()
+ {
+ TaskStatus status = new TaskStatus();
+
+ status.Finished();
+
+ Assert.Throws(delegate
+ {
+ status.Error(new Exception());
+ });
+
+ Assert.False(status.IsRunning);
+ Assert.True(status.IsFinished);
+ Assert.False(status.IsExitedWithError);
+ Assert.Null(status.Exception);
+ }
+
+ [Fact]
+ public void TestError__AlreadyErrored()
+ {
+ TaskStatus status = new TaskStatus();
+ Exception ex = new Exception();
+
+ status.Error(ex);
+
+ Assert.Throws(delegate
+ {
+ status.Error(new Exception());
+ });
+
+ Assert.False(status.IsRunning);
+ Assert.False(status.IsFinished);
+ Assert.True(status.IsExitedWithError);
+ Assert.Same(ex, status.Exception);
+ }
+ }
+}