diff --git a/azure-pipelines.yaml b/azure-pipelines.yaml index 7c83e8f2..ce23fb5b 100644 --- a/azure-pipelines.yaml +++ b/azure-pipelines.yaml @@ -41,6 +41,11 @@ jobs: inputs: version: $(DotNetCoreVersion) + - task: UseDotNet@2 + displayName: Use .NET Core 8.0.x + inputs: + version: 8.0.x + - task: DotNetCoreCLI@2 displayName: Build solution Xamarin.Android.Tools.sln inputs: diff --git a/src/Microsoft.Android.Build.BaseTasks/AndroidAsyncTask.cs b/src/Microsoft.Android.Build.BaseTasks/AndroidAsyncTask.cs deleted file mode 100644 index a04d86ff..00000000 --- a/src/Microsoft.Android.Build.BaseTasks/AndroidAsyncTask.cs +++ /dev/null @@ -1,52 +0,0 @@ -// https://github.com/xamarin/xamarin-android/blob/9fca138604c53989e1cff7fc0c2e939583b4da28/src/Xamarin.Android.Build.Tasks/Tasks/AndroidTask.cs#L27 - -using System; -using Xamarin.Build; -using static System.Threading.Tasks.TaskExtensions; - -namespace Microsoft.Android.Build.Tasks -{ - public abstract class AndroidAsyncTask : AsyncTask - { - public abstract string TaskPrefix { get; } - - public override bool Execute () - { - try { - return RunTask (); - } catch (Exception ex) { - this.LogUnhandledException (TaskPrefix, ex); - return false; - } - } - - /// - /// Typically `RunTaskAsync` will be the preferred method to override, - /// however this method can be overridden instead for Tasks that will - /// run quickly and do not need to be asynchronous. - /// - public virtual bool RunTask () - { - Yield (); - try { - this.RunTask (() => RunTaskAsync ()) - .Unwrap () - .ContinueWith (Complete); - - // This blocks on AsyncTask.Execute, until Complete is called - return base.Execute (); - } finally { - Reacquire (); - } - } - - /// - /// Override this method for simplicity of AsyncTask usage: - /// * Yield / Reacquire is handled for you - /// * RunTaskAsync is already on a background thread - /// - public virtual System.Threading.Tasks.Task RunTaskAsync () => System.Threading.Tasks.Task.CompletedTask; - - protected object ProjectSpecificTaskObjectKey (object key) => (key, WorkingDirectory); - } -} diff --git a/src/Microsoft.Android.Build.BaseTasks/AsyncTask.cs b/src/Microsoft.Android.Build.BaseTasks/AsyncTask.cs new file mode 100644 index 00000000..7a8a6072 --- /dev/null +++ b/src/Microsoft.Android.Build.BaseTasks/AsyncTask.cs @@ -0,0 +1,437 @@ +// https://github.com/xamarin/xamarin-android/blob/9fca138604c53989e1cff7fc0c2e939583b4da28/src/Xamarin.Android.Build.Tasks/Tasks/AndroidTask.cs#L27 +// https://github.com/xamarin/Xamarin.Build.AsyncTask/blob/db4ce14dacfef47435c238b1b681c124e60ea1a0/Xamarin.Build.AsyncTask/AsyncTask.cs + +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using Microsoft.Build.Utilities; +using Microsoft.Build.Framework; +using System.Threading; +using static System.Threading.Tasks.TaskExtensions; +using System.Collections; +using System.Collections.Generic; + +namespace Microsoft.Android.Build.Tasks +{ + /// + /// Base class for tasks that need long-running cancellable asynchronous tasks + /// that don't block the UI thread in the IDE. + /// + public abstract class AsyncTask : Task, ICancelableTask + { + public abstract string TaskPrefix { get; } + + readonly CancellationTokenSource cts = new CancellationTokenSource (); + readonly Queue logMessageQueue = new Queue (); + readonly Queue warningMessageQueue = new Queue (); + readonly Queue errorMessageQueue = new Queue (); + readonly Queue customMessageQueue = new Queue (); + readonly Queue telemetryMessageQueue = new Queue (); + readonly ManualResetEvent logDataAvailable = new ManualResetEvent (false); + readonly ManualResetEvent errorDataAvailable = new ManualResetEvent (false); + readonly ManualResetEvent warningDataAvailable = new ManualResetEvent (false); + readonly ManualResetEvent customDataAvailable = new ManualResetEvent (false); + readonly ManualResetEvent telemetryDataAvailable = new ManualResetEvent (false); + readonly ManualResetEvent taskCancelled = new ManualResetEvent (false); + readonly ManualResetEvent completed = new ManualResetEvent (false); + bool isRunning = true; + object eventlock = new object (); + int uiThreadId = 0; + + /// + /// Indicates if the task will yield the node during tool execution. + /// + public bool YieldDuringToolExecution { get; set; } + + /// + /// The cancellation token to notify the cancellation requests + /// + public CancellationToken CancellationToken => cts.Token; + + /// + /// Gets the current working directory for the task, which is captured at task + /// creation time from . + /// + protected string WorkingDirectory { get; private set; } + + [Obsolete ("Do not use the Log.LogXXXX from within your Async task as it will Lock the Visual Studio UI. Use the this.LogXXXX methods instead.")] + private new TaskLoggingHelper Log => base.Log; + + /// + /// Initializes the task. + /// + protected AsyncTask () + { + YieldDuringToolExecution = false; + WorkingDirectory = Directory.GetCurrentDirectory (); + uiThreadId = Thread.CurrentThread.ManagedThreadId; + } + + public void Cancel () + => taskCancelled.Set (); + + protected void Complete (System.Threading.Tasks.Task task) + { + if (task.Exception != null) { + var ex = task.Exception.GetBaseException (); + this.LogUnhandledException (TaskPrefix, ex); + } + Complete (); + } + + public void Complete () + => completed.Set (); + + public void LogDebugTaskItems (string message, string [] items) + { + LogDebugMessage (message); + + if (items == null) + return; + + foreach (var item in items) + LogDebugMessage (" {0}", item); + } + + public void LogDebugTaskItems (string message, ITaskItem [] items) + { + LogDebugMessage (message); + + if (items == null) + return; + + foreach (var item in items) + LogDebugMessage (" {0}", item.ItemSpec); + } + + public void LogTelemetry (string eventName, IDictionary properties) + { + if (uiThreadId == Thread.CurrentThread.ManagedThreadId) { +#pragma warning disable 618 + Log.LogTelemetry (eventName, properties); + return; +#pragma warning restore 618 + } + + var data = new TelemetryEventArgs () { + EventName = eventName, + Properties = properties + }; + EnqueueMessage (telemetryMessageQueue, data, telemetryDataAvailable); + } + + public void LogMessage (string message) + => LogMessage (message, importance: MessageImportance.Normal); + + public void LogMessage (string message, params object [] messageArgs) + => LogMessage (string.Format (message, messageArgs)); + + public void LogDebugMessage (string message) + => LogMessage (message, importance: MessageImportance.Low); + + public void LogDebugMessage (string message, params object [] messageArgs) + => LogMessage (string.Format (message, messageArgs), importance: MessageImportance.Low); + + public void LogMessage (string message, MessageImportance importance = MessageImportance.Normal) + { + if (uiThreadId == Thread.CurrentThread.ManagedThreadId) { +#pragma warning disable 618 + Log.LogMessage (importance, message); + return; +#pragma warning restore 618 + } + + var data = new BuildMessageEventArgs ( + message: message, + helpKeyword: null, + senderName: null, + importance: importance + ); + EnqueueMessage (logMessageQueue, data, logDataAvailable); + } + + public void LogError (string message) + => LogCodedError (code: null, message: message, file: null, lineNumber: 0); + + public void LogError (string message, params object [] messageArgs) + => LogCodedError (code: null, message: string.Format (message, messageArgs)); + + public void LogCodedError (string code, string message) + => LogCodedError (code: code, message: message, file: null, lineNumber: 0); + + public void LogCodedError (string code, string message, params object [] messageArgs) + => LogCodedError (code: code, message: string.Format (message, messageArgs), file: null, lineNumber: 0); + + public void LogCodedError (string code, string file, int lineNumber, string message, params object [] messageArgs) + => LogCodedError (code: code, message: string.Format (message, messageArgs), file: file, lineNumber: lineNumber); + + public void LogCodedError (string code, string message, string file, int lineNumber) + { + if (uiThreadId == Thread.CurrentThread.ManagedThreadId) { +#pragma warning disable 618 + Log.LogError ( + subcategory: null, + errorCode: code, + helpKeyword: null, + file: file, + lineNumber: lineNumber, + columnNumber: 0, + endLineNumber: 0, + endColumnNumber: 0, + message: message + ); + return; +#pragma warning restore 618 + } + + var data = new BuildErrorEventArgs ( + subcategory: null, + code: code, + file: file, + lineNumber: lineNumber, + columnNumber: 0, + endLineNumber: 0, + endColumnNumber: 0, + message: message, + helpKeyword: null, + senderName: null + ); + EnqueueMessage (errorMessageQueue, data, errorDataAvailable); + } + + public void LogWarning (string message) + => LogCodedWarning (code: null, message: message, file: null, lineNumber: 0); + + public void LogWarning (string message, params object [] messageArgs) + => LogCodedWarning (code: null, message: string.Format (message, messageArgs)); + + public void LogCodedWarning (string code, string message) + => LogCodedWarning (code: code, message: message, file: null, lineNumber: 0); + + public void LogCodedWarning (string code, string message, params object [] messageArgs) + => LogCodedWarning (code: code, message: string.Format (message, messageArgs), file: null, lineNumber: 0); + + public void LogCodedWarning (string code, string file, int lineNumber, string message, params object [] messageArgs) + => LogCodedWarning (code: code, message: string.Format (message, messageArgs), file: file, lineNumber: lineNumber); + + public void LogCodedWarning (string code, string message, string file, int lineNumber) + { + if (uiThreadId == Thread.CurrentThread.ManagedThreadId) { +#pragma warning disable 618 + Log.LogWarning ( + subcategory: null, + warningCode: code, + helpKeyword: null, + file: file, + lineNumber: lineNumber, + columnNumber: 0, + endLineNumber: 0, + endColumnNumber: 0, + message: message + ); + return; +#pragma warning restore 618 + } + var data = new BuildWarningEventArgs ( + subcategory: null, + code: code, + file: file, + lineNumber: lineNumber, + columnNumber: 0, + endLineNumber: 0, + endColumnNumber: 0, + message: message, + helpKeyword: null, + senderName: null + ); + EnqueueMessage (warningMessageQueue, data, warningDataAvailable); + } + + public void LogCustomBuildEvent (CustomBuildEventArgs e) + { + if (uiThreadId == Thread.CurrentThread.ManagedThreadId) { + BuildEngine.LogCustomEvent (e); + return; + } + EnqueueMessage (customMessageQueue, e, customDataAvailable); + } + + bool ExecuteWaitForCompletion () + { + WaitForCompletion (); +#pragma warning disable 618 + return !Log.HasLoggedErrors; +#pragma warning restore 618 + } + + void EnqueueMessage (Queue queue, object item, ManualResetEvent resetEvent) + { + lock (queue.SyncRoot) { + queue.Enqueue (item); + lock (eventlock) { + if (isRunning) + resetEvent.Set (); + } + } + } + + void LogInternal (Queue queue, Action action, ManualResetEvent resetEvent) + { + lock (queue.SyncRoot) { + while (queue.Count > 0) { + var args = (T) queue.Dequeue (); + action (args); + } + resetEvent.Reset (); + } + } + + protected void Yield () + { + if (YieldDuringToolExecution && BuildEngine is IBuildEngine3) + ((IBuildEngine3) BuildEngine).Yield (); + } + + protected void Reacquire () + { + if (YieldDuringToolExecution && BuildEngine is IBuildEngine3) + ((IBuildEngine3) BuildEngine).Reacquire (); + } + + protected void WaitForCompletion () + { + WaitHandle [] handles = new WaitHandle [] { + logDataAvailable, + errorDataAvailable, + warningDataAvailable, + customDataAvailable, + telemetryDataAvailable, + taskCancelled, + completed, + }; + try { + while (isRunning) { + var index = (WaitHandleIndex) System.Threading.WaitHandle.WaitAny (handles, TimeSpan.FromMilliseconds (10)); + switch (index) { + case WaitHandleIndex.LogDataAvailable: + LogInternal (logMessageQueue, (e) => { +#pragma warning disable 618 + Log.LogMessage (e.Importance, e.Message); +#pragma warning restore 618 + }, logDataAvailable); + break; + case WaitHandleIndex.ErrorDataAvailable: + LogInternal (errorMessageQueue, (e) => { +#pragma warning disable 618 + Log.LogError ( + subcategory: null, + errorCode: e.Code, + helpKeyword: null, + file: e.File, + lineNumber: e.LineNumber, + columnNumber: e.ColumnNumber, + endLineNumber: e.EndLineNumber, + endColumnNumber: e.EndColumnNumber, + message: e.Message); +#pragma warning restore 618 + }, errorDataAvailable); + break; + case WaitHandleIndex.WarningDataAvailable: + LogInternal (warningMessageQueue, (e) => { +#pragma warning disable 618 + Log.LogWarning (subcategory: null, + warningCode: e.Code, + helpKeyword: null, + file: e.File, + lineNumber: e.LineNumber, + columnNumber: e.ColumnNumber, + endLineNumber: e.EndLineNumber, + endColumnNumber: e.EndColumnNumber, + message: e.Message); +#pragma warning restore 618 + }, warningDataAvailable); + break; + case WaitHandleIndex.CustomDataAvailable: + LogInternal (customMessageQueue, (e) => { + BuildEngine.LogCustomEvent (e); + }, customDataAvailable); + break; + case WaitHandleIndex.TelemetryDataAvailable: + LogInternal (telemetryMessageQueue, (e) => { + BuildEngine5.LogTelemetry (e.EventName, e.Properties); + }, telemetryDataAvailable); + break; + case WaitHandleIndex.TaskCancelled: + Cancel (); + cts.Cancel (); + isRunning = false; + break; + case WaitHandleIndex.Completed: + isRunning = false; + break; + } + } + + } finally { + + } + } + + public override bool Execute () + { + try { + return RunTask (); + } catch (Exception ex) { + this.LogUnhandledException (TaskPrefix, ex); + return false; + } + } + + /// + /// Typically `RunTaskAsync` will be the preferred method to override, + /// however this method can be overridden instead for Tasks that will + /// run quickly and do not need to be asynchronous. + /// + public virtual bool RunTask () + { + Yield (); + try { + this.RunTask (() => RunTaskAsync ()) + .Unwrap () + .ContinueWith (Complete); + + // This blocks on Execute, until Complete is called + return ExecuteWaitForCompletion (); + } finally { + Reacquire (); + } + } + + /// + /// Override this method for simplicity of AsyncTask usage: + /// + /// + /// Yield / Reacquire is handled for you + /// + /// + /// RunTaskAsync is already on a background thread + /// + /// + /// + public virtual System.Threading.Tasks.Task RunTaskAsync () => System.Threading.Tasks.Task.CompletedTask; + + protected object ProjectSpecificTaskObjectKey (object key) => (key, WorkingDirectory); + + private enum WaitHandleIndex + { + LogDataAvailable, + ErrorDataAvailable, + WarningDataAvailable, + CustomDataAvailable, + TelemetryDataAvailable, + TaskCancelled, + Completed, + } + } +} diff --git a/src/Microsoft.Android.Build.BaseTasks/AsyncTaskExtensions.cs b/src/Microsoft.Android.Build.BaseTasks/AsyncTaskExtensions.cs index fce4bc64..6b643ee7 100644 --- a/src/Microsoft.Android.Build.BaseTasks/AsyncTaskExtensions.cs +++ b/src/Microsoft.Android.Build.BaseTasks/AsyncTaskExtensions.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Xamarin.Build; namespace Microsoft.Android.Build.Tasks { diff --git a/src/Microsoft.Android.Build.BaseTasks/MSBuildExtensions.cs b/src/Microsoft.Android.Build.BaseTasks/MSBuildExtensions.cs index fd0909d7..323c874e 100644 --- a/src/Microsoft.Android.Build.BaseTasks/MSBuildExtensions.cs +++ b/src/Microsoft.Android.Build.BaseTasks/MSBuildExtensions.cs @@ -9,7 +9,6 @@ using Microsoft.Build.Utilities; using Microsoft.Build.Framework; -using Xamarin.Build; namespace Microsoft.Android.Build.Tasks { diff --git a/src/Microsoft.Android.Build.BaseTasks/MSBuildReferences.projitems b/src/Microsoft.Android.Build.BaseTasks/MSBuildReferences.projitems index 96c56c0e..5d6a62a5 100644 --- a/src/Microsoft.Android.Build.BaseTasks/MSBuildReferences.projitems +++ b/src/Microsoft.Android.Build.BaseTasks/MSBuildReferences.projitems @@ -17,7 +17,6 @@ - diff --git a/src/Microsoft.Android.Build.BaseTasks/UnhandledExceptionLogger.cs b/src/Microsoft.Android.Build.BaseTasks/UnhandledExceptionLogger.cs index 67d20868..4cfe3c0b 100644 --- a/src/Microsoft.Android.Build.BaseTasks/UnhandledExceptionLogger.cs +++ b/src/Microsoft.Android.Build.BaseTasks/UnhandledExceptionLogger.cs @@ -5,7 +5,6 @@ using System.IO; using System.Threading.Tasks; using Microsoft.Build.Utilities; -using Xamarin.Build; namespace Microsoft.Android.Build.Tasks { diff --git a/tests/Microsoft.Android.Build.BaseTasks-Tests/AsyncTaskExtensionsTests.cs b/tests/Microsoft.Android.Build.BaseTasks-Tests/AsyncTaskExtensionsTests.cs index ecfc8667..13a9762d 100644 --- a/tests/Microsoft.Android.Build.BaseTasks-Tests/AsyncTaskExtensionsTests.cs +++ b/tests/Microsoft.Android.Build.BaseTasks-Tests/AsyncTaskExtensionsTests.cs @@ -2,7 +2,6 @@ using System.Threading.Tasks; using Microsoft.Android.Build.Tasks; using NUnit.Framework; -using Xamarin.Build; namespace Microsoft.Android.Build.BaseTasks.Tests { @@ -11,11 +10,16 @@ public class AsyncTaskExtensionsTests { const int Iterations = 32; + class TestAsyncTask : AsyncTask + { + public override string TaskPrefix => "TEST"; + } + [Test] public async Task RunTask () { bool set = false; - await new AsyncTask ().RunTask (delegate { set = true; }); // delegate { } has void return type + await new TestAsyncTask ().RunTask (delegate { set = true; }); // delegate { } has void return type Assert.IsTrue (set); } @@ -23,7 +27,7 @@ public async Task RunTask () public async Task RunTaskOfT () { bool set = false; - Assert.IsTrue (await new AsyncTask ().RunTask (() => set = true), "RunTask should return true"); + Assert.IsTrue (await new TestAsyncTask ().RunTask (() => set = true), "RunTask should return true"); Assert.IsTrue (set); } @@ -31,7 +35,7 @@ public async Task RunTaskOfT () public async Task WhenAll () { bool set = false; - await new AsyncTask ().WhenAll (new [] { 0 }, _ => set = true); + await new TestAsyncTask ().WhenAll (new [] { 0 }, _ => set = true); Assert.IsTrue (set); } @@ -40,7 +44,7 @@ public async Task WhenAllWithLock () { var input = new int [Iterations]; var output = new List (); - await new AsyncTask ().WhenAllWithLock (input, (i, l) => { + await new TestAsyncTask ().WhenAllWithLock (input, (i, l) => { lock (l) output.Add (i); }); Assert.AreEqual (Iterations, output.Count); @@ -50,7 +54,7 @@ public async Task WhenAllWithLock () public void ParallelForEach () { bool set = false; - new AsyncTask ().ParallelForEach (new [] { 0 }, _ => set = true); + new TestAsyncTask ().ParallelForEach (new [] { 0 }, _ => set = true); Assert.IsTrue (set); } @@ -59,7 +63,7 @@ public void ParallelForEachWithLock () { var input = new int [Iterations]; var output = new List (); - new AsyncTask ().ParallelForEachWithLock (input, (i, l) => { + new TestAsyncTask ().ParallelForEachWithLock (input, (i, l) => { lock (l) output.Add (i); }); Assert.AreEqual (Iterations, output.Count); diff --git a/tests/Microsoft.Android.Build.BaseTasks-Tests/AsyncTaskTests.cs b/tests/Microsoft.Android.Build.BaseTasks-Tests/AsyncTaskTests.cs new file mode 100644 index 00000000..912b4c1a --- /dev/null +++ b/tests/Microsoft.Android.Build.BaseTasks-Tests/AsyncTaskTests.cs @@ -0,0 +1,105 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Linq; +using Microsoft.Android.Build.BaseTasks.Tests.Utilities; +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Framework; +using NUnit.Framework; + +namespace Microsoft.Android.Build.BaseTasks.Tests +{ + [TestFixture] + public class AsyncTaskTests + { + List errors; + List warnings; + List messages; + MockBuildEngine engine; + + [SetUp] + public void TestSetup () + { + errors = new List (); + warnings = new List (); + messages = new List (); + engine = new MockBuildEngine (TestContext.Out, errors, warnings, messages); + } + + class AsyncTaskTest : AsyncTask + { + public override string TaskPrefix => "ATT"; + } + + public class AsyncMessage : AsyncTask + { + public override string TaskPrefix => "AM"; + + public string Text { get; set; } + + public override bool Execute () + { + LogTelemetry ("Test", new Dictionary () { { "Property", "Value" } }); + return base.Execute (); + } + + public override async Task RunTaskAsync () + { + await Task.Delay (5000); + LogMessage (Text); + Complete (); + } + } + + class AsyncTaskExceptionTest : AsyncTask + { + public override string TaskPrefix => "ATET"; + + public string ExceptionMessage { get; set; } + + public override Task RunTaskAsync () + { + throw new System.InvalidOperationException (ExceptionMessage); + } + } + + + [Test] + public void RunAsyncTask () + { + var task = new AsyncTaskTest () { + BuildEngine = engine, + }; + + Assert.IsTrue (task.Execute (), "Empty AsyncTask should have ran successfully."); + } + + [Test] + public void RunAsyncTaskOverride () + { + var message = "Hello Async World!"; + var task = new AsyncMessage () { + BuildEngine = engine, + Text = message, + }; + var taskSucceeded = task.Execute (); + Assert.IsTrue (messages.Any (e => e.Message.Contains (message)), + $"Task did not contain expected message text: '{message}'."); + } + + [Test] + public void RunAsyncTaskExpectedException () + { + var expectedException = "test exception!"; + var task = new AsyncTaskExceptionTest () { + BuildEngine = engine, + ExceptionMessage = expectedException, + }; + + Assert.IsFalse (task.Execute (), "Exception AsyncTask should have failed."); + Assert.IsTrue (errors.Count == 1, "Exception AsyncTask should have produced one error."); + Assert.IsTrue (errors[0].Message.Contains (expectedException), + $"Task did not contain expected error text: '{expectedException}'."); + } + + } +} diff --git a/tests/Microsoft.Android.Build.BaseTasks-Tests/Microsoft.Android.Build.BaseTasks-Tests.csproj b/tests/Microsoft.Android.Build.BaseTasks-Tests/Microsoft.Android.Build.BaseTasks-Tests.csproj index 7f0fb6d7..99bdc267 100644 --- a/tests/Microsoft.Android.Build.BaseTasks-Tests/Microsoft.Android.Build.BaseTasks-Tests.csproj +++ b/tests/Microsoft.Android.Build.BaseTasks-Tests/Microsoft.Android.Build.BaseTasks-Tests.csproj @@ -4,7 +4,7 @@ - net6.0 + net8.0 Microsoft.Android.Build.BaseTasks.Tests false $(TestOutputFullPath)