Skip to content

Commit

Permalink
BackgroundTaskManager
Browse files Browse the repository at this point in the history
  • Loading branch information
dellis1972 committed Nov 27, 2019
1 parent 2c822f1 commit 4721d76
Show file tree
Hide file tree
Showing 6 changed files with 319 additions and 10 deletions.
62 changes: 58 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# Xamarin.Build.AsyncTask

Provides the AsyncTask to streamline the creation of long-running tasks that
are cancellable and don't block the UI thread in Visual Studio.
are cancellable and don't block the UI thread in Visual Studio. It also
provides a set of Tasks and classes to start a long-running Task but
wait for it to complete later in the build process.

## Building

Expand Down Expand Up @@ -42,8 +44,8 @@ as a template:
await System.Threading.Tasks.Task.Delay(5000);
// Invoke Complete to signal you're done.
Complete();
});
Complete();
});
return base.Execute();
}
Expand All @@ -56,4 +58,56 @@ as a template:

## CI Builds

Building and pushing from [VSTS](https://devdiv.visualstudio.com/DevDiv/XamarinVS/_build/index?context=allDefinitions&path=%5CXamarin&definitionId=7445&_a=completed).
Building and pushing from [VSTS](https://devdiv.visualstudio.com/DevDiv/XamarinVS/_build/index?context=allDefinitions&path=%5CXamarin&definitionId=7445&_a=completed).


## Running a Background Task

If you want to start a background task and continue with the build
and then wait for that background task later, you will need to use
the `IsBackgroundTask` property. By default this property is `False`.
This means any derived `AsyncTask` will wait for all `tasks` to complete
before returning to MSBuild.

When the `IsBackgroundTask` property is set to true, `base.Execute` will
not longer block until all `tasks` have completed. Instead it will
automatically register itself with the `BackgroundTaskManager` and then
return. This feature is especially useful if you want to have code which
runs in the backgroudn while the build continues.

In this situation what will happen is the background tasks will be started,
and the build will just continue as normal. At the end of the build when
the `BackgroundTaskManager` is disposed by MSBuild it will wait on all
the registered `AsyncTask's` before disposing. This means all the background
threads will be completed before the build finishes.

However there might be situations where you want to start a background task
and then wait at a later point in the build for it to complete. For this we
have the `WaitForBackgroundTasks` Task. This MSBuild Task will wait for all
registered `AsyncTask` items to complete before returning.

The `AsyncTask` has a `Category` property. By default this is `default`. When
`IsBackgroundTask` is set to `True` this `Category` is used to register the
task with the `BackgroundTaskManager`. `WaitForBackgroundTasks` also has a
`Categories` property. This is a list of the categories it should wait for.
By default it is `default`. You can use the `Category`/`Categories` to run multiple
different background tasks and wait on them at different parts of the build.

Here is an exampe. This makes use of the `MyAsyncTask` we defined earlier, but
supports running it in the background. We then also use the `WaitForBackgroundTasks`
to wait on that task later in the build process.

```xml
<UsingTask TaskName="WaitForBackgroundTask" AssemblyFile="$(AsyncTask)">
<Target Name="_StartLongTask">
<!-- Start MyAsyncTask in the background -->
<MyAsyncTask IsBackgroundTask="true" Category="example" />
</Target>
<Target Name="_WaitForBackgroundTask" DependsOnTarget="_StartLongTask">
<!-- Wait for it to complete -->
<WaitForBackgroundTasks Categories="example" />
</Target>
```

This can give you flexability to run very long running tasks in the background
without holding up the build process.
54 changes: 51 additions & 3 deletions Xamarin.Build.AsyncTask/AsyncTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ protected void Complete(System.Threading.Tasks.Task task)
Complete();
}

public string Category { get; set; } = BackgroundTaskManager.DefaultCategory;

public bool IsBackgroundTask { get; set; } = false;

public bool IsComplete {
get { return cts.IsCancellationRequested || completed.WaitOne (0) || taskCancelled.WaitOne (0); }
}

public void Complete() => completed.Set();

public void LogDebugTaskItems(string message, string[] items)
Expand Down Expand Up @@ -223,7 +231,16 @@ public void LogCustomBuildEvent(CustomBuildEventArgs e)

public override bool Execute()
{
WaitForCompletion();
if (IsBackgroundTask)
{
LogMessage ($"Backgrounding {this.GetType ()}");
var manager = BackgroundTaskManager.GetTaskManager (BuildEngine4);
manager.RegisterTask (this, Category);
#pragma warning disable 618
return !Log.HasLoggedErrors;
#pragma warning restore 618
}
WaitForCompletion ();
#pragma warning disable 618
return !Log.HasLoggedErrors;
#pragma warning restore 618
Expand All @@ -236,7 +253,7 @@ private void EnqueueMessage(Queue queue, object item, ManualResetEvent resetEven
queue.Enqueue(item);
lock (eventlock)
{
if (isRunning)
if (isRunning && !IsBackgroundTask)
resetEvent.Set();
}
}
Expand Down Expand Up @@ -267,7 +284,38 @@ protected void Reacquire()
((IBuildEngine3)BuildEngine).Reacquire();
}

protected void WaitForCompletion()
/// <summary>
/// Wait for a Task which was flagged with `IsBackgroundTask`.
/// </summary>
/// <param name="buildEngine">The buildEngine to use to log any messages the task has pending.</param>
internal void Wait (IBuildEngine buildEngine)
{
WaitForCompletion ();
if (logMessageQueue.Count > 0) {
while (logMessageQueue.Count > 0)
{
BuildMessageEventArgs e = (BuildMessageEventArgs)logMessageQueue.Dequeue();
buildEngine.LogMessageEvent(e);
}
}
if (warningMessageQueue.Count > 0)
{
while (warningMessageQueue.Count > 0)
{
BuildWarningEventArgs e = (BuildWarningEventArgs)warningMessageQueue.Dequeue();
buildEngine.LogWarningEvent(e);
}
}
if (errorMessageQueue.Count > 0) {
while (errorMessageQueue.Count > 0)
{
BuildErrorEventArgs e = (BuildErrorEventArgs)errorMessageQueue.Dequeue();
buildEngine.LogErrorEvent(e);
}
}
}

internal protected void WaitForCompletion()
{
WaitHandle[] handles = new WaitHandle[] {
logDataAvailable,
Expand Down
96 changes: 96 additions & 0 deletions Xamarin.Build.AsyncTask/BackgroundTaskManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using System;
using System.IO;
using System.Collections.Concurrent;
using System.Threading;
using Microsoft.Build.Utilities;
using Microsoft.Build.Framework;
using System.Collections;
using TPL = System.Threading.Tasks;

namespace Xamarin.Build
{
/// <summary>
/// A class for keeping track of any Task instances which are run as part of
/// a build.
/// Call `manager.RegisterTask` to register the task with the manager. You can
/// provide a `Category` for each task. This is so they can be waited on later
/// using the `WaitForBackgroundTasks` MSBuild task.
/// </summary>
public class BackgroundTaskManager : IDisposable
{
internal const string DefaultCategory = "default";
ConcurrentDictionary<string, ConcurrentBag<AsyncTask>> tasks = new ConcurrentDictionary<string, ConcurrentBag<AsyncTask>> ();
CancellationTokenSource tcs = new CancellationTokenSource ();
IBuildEngine4 buildEngine;

/// <summary>
/// Get an Instance of the TaskManager.
/// NOTE This MUST be called from the main thread in a Task, it cannot be called on a background thread.
/// </summary>
/// <param name="buildEngine4">An instance of the IBuildEngine4 interface.</param>
/// <returns>An instance of a TaskManager</returns>
public static BackgroundTaskManager GetTaskManager (IBuildEngine4 buildEngine4)
{
var manager = (BackgroundTaskManager)buildEngine4.GetRegisteredTaskObject (typeof (BackgroundTaskManager).FullName, RegisteredTaskObjectLifetime.Build);
if (manager == null)
{
manager = new BackgroundTaskManager (buildEngine4);
buildEngine4.RegisterTaskObject (typeof (BackgroundTaskManager).FullName, manager, RegisteredTaskObjectLifetime.Build, allowEarlyCollection: false);
}
return manager;
}

public BackgroundTaskManager(IBuildEngine4 buildEngine)
{
this.buildEngine = buildEngine;
}

/// <summary>
/// Register a new Task which will be running in the background.
/// If you have multiple tasks running you can split them up into
/// different categories. This can then we used to wait in differnt
/// parts of the build later on.
/// </summary>
/// <param name="task">The task you are running</param>
/// <param name="category">The category this task is in. </param>
public void RegisterTask (AsyncTask task, string category = DefaultCategory)
{
var bag = tasks.GetOrAdd (category, new ConcurrentBag<AsyncTask> ());
bag.Add (task);
}

/// <summary>
/// Returns an array of Tasks for that category.
/// </summary>
/// <param name="category">The category you want to get the list for.</param>
/// <returns>Either the array of task or an empty array if the category does not exist.</returns>
public AsyncTask [] this [string category]
{
get
{
ConcurrentBag<AsyncTask> result;
if (!tasks.TryGetValue (category, out result))
return Array.Empty<AsyncTask> ();
return result.ToArray ();
}
}

public void Dispose ()
{
// wait for all tasks to complete.
foreach (var bag in tasks) {
foreach (AsyncTask t in bag.Value) {
t.Wait (buildEngine);
}
}
tcs.Cancel ();
}

public CancellationToken CancellationToken { get { return tcs.Token; } }

/// <summary>
/// The number of registered categories.
/// </summary>
public int Count => tasks.Count;
}
}
2 changes: 1 addition & 1 deletion Xamarin.Build.AsyncTask/Readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ as a template:
await System.Threading.Tasks.Task.Delay(5000);

// Invoke Complete to signal you're done.
Complete();
Complete();
});

return base.Execute();
Expand Down
42 changes: 40 additions & 2 deletions Xamarin.Build.AsyncTask/Test.targets
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
</PropertyGroup>

<UsingTask TaskName="Xamarin.Build.WaitForBackgroundTasks" AssemblyFile="$(OutputPath)$(AssemblyName).dll" />
<Target Name="Test" >
<Error Condition="!Exists('$(OutputPath)$(AssemblyName).dll')" Text="Run Build first. '$(OutputPath)$(AssemblyName).dll' not found." />
<AsyncMessage Text="Hello Async World!" />
<LongTask Category="default" IsBackgroundTask="True" Text="Hello LogTask World!" />
<WaitForBackgroundTasks Categories="default" />
<LongTask Category="background" IsBackgroundTask="True" Text="Hello LogTask World Again!" />
</Target>

<UsingTask TaskName="AsyncMessage" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
Expand All @@ -28,7 +31,7 @@
{
System.Threading.Tasks.Task.Run(async () =>
{
await System.Threading.Tasks.Task.Delay(5000);
await System.Threading.Tasks.Task.Delay(500);
LogMessage(Text);
Complete();
});
Expand All @@ -40,5 +43,40 @@
</Code>
</Task>
</UsingTask>

<UsingTask TaskName="LongTask" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
<ParameterGroup>
<Category />
<IsBackgroundTask ParameterType="System.Boolean"/>
<Text Required="true" />
</ParameterGroup>
<Task>
<Reference Include="$(OutputPath)$(AssemblyName).dll" />
<Reference Include="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll"/>
<Reference Include="$(MSBuildToolsPath)\Microsoft.Build.Utilities.Core.dll"/>
<Reference Include="System.Threading.Tasks"/>
<Code Type="Class" Language="cs">
<![CDATA[
public class LongTask : Xamarin.Build.AsyncTask
{
public string Text { get; set; }
public override bool Execute()
{
var task = System.Threading.Tasks.Task.Run(async () =>
{
await System.Threading.Tasks.Task.Delay(1000);
LogMessage(Text);
LogWarning("A Sample Warning.");
Complete ();
});
return base.Execute();
}
}
]]>
</Code>
</Task>
</UsingTask>

</Project>
Loading

0 comments on commit 4721d76

Please sign in to comment.