From b17e42ec268dba46dfe7cc3744805a67bbf839c4 Mon Sep 17 00:00:00 2001 From: Dean Ellis Date: Mon, 19 Aug 2019 19:19:53 +0100 Subject: [PATCH] BackgroundTaskManager --- README.md | 62 +++++++++++- Xamarin.Build.AsyncTask/AsyncTask.cs | 54 ++++++++++- .../BackgroundTaskManager.cs | 96 +++++++++++++++++++ Xamarin.Build.AsyncTask/Readme.txt | 2 +- Xamarin.Build.AsyncTask/Test.targets | 42 +++++++- .../WaitForBackgroundTasks.cs | 73 ++++++++++++++ 6 files changed, 319 insertions(+), 10 deletions(-) create mode 100644 Xamarin.Build.AsyncTask/BackgroundTaskManager.cs create mode 100644 Xamarin.Build.AsyncTask/WaitForBackgroundTasks.cs diff --git a/README.md b/README.md index a7ebd27..1aa6995 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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(); } @@ -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). \ No newline at end of file +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 background 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 example. 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 + + + + + + + + + +``` + +This can give you flexibility to run very long running tasks in the background +without holding up the build process. \ No newline at end of file diff --git a/Xamarin.Build.AsyncTask/AsyncTask.cs b/Xamarin.Build.AsyncTask/AsyncTask.cs index 312c556..15c3685 100644 --- a/Xamarin.Build.AsyncTask/AsyncTask.cs +++ b/Xamarin.Build.AsyncTask/AsyncTask.cs @@ -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) @@ -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 @@ -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(); } } @@ -267,7 +284,38 @@ protected void Reacquire() ((IBuildEngine3)BuildEngine).Reacquire(); } - protected void WaitForCompletion() + /// + /// Wait for a Task which was flagged with `IsBackgroundTask`. + /// + /// The buildEngine to use to log any messages the task has pending. + 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, diff --git a/Xamarin.Build.AsyncTask/BackgroundTaskManager.cs b/Xamarin.Build.AsyncTask/BackgroundTaskManager.cs new file mode 100644 index 0000000..25e1dd0 --- /dev/null +++ b/Xamarin.Build.AsyncTask/BackgroundTaskManager.cs @@ -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 +{ + /// + /// 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. + /// + public class BackgroundTaskManager : IDisposable + { + internal const string DefaultCategory = "default"; + ConcurrentDictionary> tasks = new ConcurrentDictionary> (); + CancellationTokenSource tcs = new CancellationTokenSource (); + IBuildEngine4 buildEngine; + + /// + /// 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. + /// + /// An instance of the IBuildEngine4 interface. + /// An instance of a TaskManager + 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; + } + + /// + /// 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. + /// + /// The task you are running + /// The category this task is in. + public void RegisterTask (AsyncTask task, string category = DefaultCategory) + { + var bag = tasks.GetOrAdd (category, new ConcurrentBag ()); + bag.Add (task); + } + + /// + /// Returns an array of Tasks for that category. + /// + /// The category you want to get the list for. + /// Either the array of task or an empty array if the category does not exist. + public AsyncTask [] this [string category] + { + get + { + ConcurrentBag result; + if (!tasks.TryGetValue (category, out result)) + return Array.Empty (); + 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; } } + + /// + /// The number of registered categories. + /// + public int Count => tasks.Count; + } +} diff --git a/Xamarin.Build.AsyncTask/Readme.txt b/Xamarin.Build.AsyncTask/Readme.txt index 9e057bb..4238208 100644 --- a/Xamarin.Build.AsyncTask/Readme.txt +++ b/Xamarin.Build.AsyncTask/Readme.txt @@ -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(); diff --git a/Xamarin.Build.AsyncTask/Test.targets b/Xamarin.Build.AsyncTask/Test.targets index 2358f74..48cfe76 100644 --- a/Xamarin.Build.AsyncTask/Test.targets +++ b/Xamarin.Build.AsyncTask/Test.targets @@ -3,10 +3,13 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - + + + + @@ -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(); }); @@ -40,5 +43,40 @@ + + + + + + + + + + + + + + + { + await System.Threading.Tasks.Task.Delay(1000); + LogMessage(Text); + LogWarning("A Sample Warning."); + Complete (); + }); + + return base.Execute(); + } + } + ]]> + + + \ No newline at end of file diff --git a/Xamarin.Build.AsyncTask/WaitForBackgroundTasks.cs b/Xamarin.Build.AsyncTask/WaitForBackgroundTasks.cs new file mode 100644 index 0000000..2eccfb7 --- /dev/null +++ b/Xamarin.Build.AsyncTask/WaitForBackgroundTasks.cs @@ -0,0 +1,73 @@ +using System; +using System.IO; +using Microsoft.Build.Utilities; +using Microsoft.Build.Framework; +using System.Threading; +using System.Collections.Generic; +using TPL = System.Threading.Tasks; + +namespace Xamarin.Build +{ + /// + /// This Task will wait for all the background tasks to complete. + /// You can control which task categories to wait on by using the + /// `Categories` property. + /// + public class WaitForBackgroundTasks : AsyncTask + { + internal static readonly string [] DefaultCategories = { BackgroundTaskManager.DefaultCategory }; + /// + /// A list of background task categories to wait on. + /// Once all the tasks in each of these categories have Completed + /// the task will exit. + /// By default it will wait on the `default` category. + /// + public string [] Categories { get; set; } + + /// + /// The error code to use should any of the background tasks fail. + /// this will default to XAT0000 + /// + public string ErrorCode { get; set; } = "XAT0000"; + + public override bool Execute () + { + var manager = BackgroundTaskManager.GetTaskManager (BuildEngine4); + List tasks = new List (); + if (manager == null || manager.Count == 0) + { + Log.LogMessage (MessageImportance.Normal, $"No tasks found in TaskManager"); + return true; + } + foreach (string category in Categories ?? DefaultCategories) + { + if (manager [category].Length == 0) + { + Log.LogMessage (MessageImportance.Normal, $"Not tasks found for {category}"); + continue; + } + else + { + Log.LogMessage (MessageImportance.Normal, $"Waiting on Tasks {category}"); + tasks.AddRange (manager [category]); + } + } + if (tasks.Count == 0) + { + Log.LogMessage (MessageImportance.Normal, $"No Tasks found for Categories [{string.Join (",", Categories)}]"); + return true; + } + // We need to wait for the completion of any background tasks + // needs to be done on the UI Thread so we get correct logging. + foreach (var task in tasks) + { + Log.LogMessage (MessageImportance.Normal, $"Waiting on {task.GetType ()}"); + task.Wait (BuildEngine); + } + Complete (); + base.Execute (); + Log.LogMessage (MessageImportance.Normal, $"All Tasks in Categories [{string.Join (",", Categories)}] have Completed."); + return !Log.HasLoggedErrors; + } + } +}