diff --git a/libraries/Microsoft.Bot.Builder.Dialogs.Adaptive/Actions/ForEachElement.cs b/libraries/Microsoft.Bot.Builder.Dialogs.Adaptive/Actions/ForEachElement.cs index f43d22f973..9bcff70098 100644 --- a/libraries/Microsoft.Bot.Builder.Dialogs.Adaptive/Actions/ForEachElement.cs +++ b/libraries/Microsoft.Bot.Builder.Dialogs.Adaptive/Actions/ForEachElement.cs @@ -263,7 +263,6 @@ private DialogContext CreateChildContext(DialogContext dc, DialogState childDial private bool ShouldEndDialog(DialogTurnResult turnResult, out DialogTurnResult finalTurnResult) { - DialogTurnStatus[] endedDialog = { DialogTurnStatus.Complete, DialogTurnStatus.Cancelled }; finalTurnResult = null; // Insure BreakLoop ends the dialog @@ -280,12 +279,12 @@ private bool ShouldEndDialog(DialogTurnResult turnResult, out DialogTurnResult f // the result will be nested. while (finalTurnResult.Result != null && finalTurnResult.Result is DialogTurnResult dtr - && dtr.ParentEnded && endedDialog.Contains(dtr.Status)) + && dtr.ParentEnded && dtr.Status == DialogTurnStatus.Complete) { finalTurnResult = dtr; } - return finalTurnResult.ParentEnded && endedDialog.Contains(finalTurnResult.Status); + return finalTurnResult.ParentEnded && finalTurnResult.Status == DialogTurnStatus.Complete; } private void UpdateActionScopeState(DialogContext dc, DialogState state) diff --git a/libraries/Microsoft.Bot.Builder/ShowTypingMiddleware.cs b/libraries/Microsoft.Bot.Builder/ShowTypingMiddleware.cs index 30187cd0b9..86c15892dd 100644 --- a/libraries/Microsoft.Bot.Builder/ShowTypingMiddleware.cs +++ b/libraries/Microsoft.Bot.Builder/ShowTypingMiddleware.cs @@ -2,6 +2,8 @@ // Licensed under the MIT License. using System; +using System.Collections.Concurrent; +using System.Linq; using System.Security.Claims; using System.Security.Principal; using System.Threading; @@ -22,6 +24,7 @@ public class ShowTypingMiddleware : IMiddleware { private readonly TimeSpan _delay; private readonly TimeSpan _period; + private readonly ConcurrentDictionary _tasks = new ConcurrentDictionary(); /// /// Initializes a new instance of the class. @@ -58,29 +61,29 @@ public ShowTypingMiddleware(int delay = 500, int period = 2000) /// public async Task OnTurnAsync(ITurnContext turnContext, NextDelegate next, CancellationToken cancellationToken) { - using (var cts = new CancellationTokenSource()) + turnContext.OnSendActivities(async (ctx, activities, nextSend) => { - Task typingTask = null; - try + var containsMessage = activities.Any(e => e.Type == ActivityTypes.Message); + if (containsMessage) { - // Start a timer to periodically send the typing activity (bots running as skills should not send typing activity) - if (!IsSkillBot(turnContext) && turnContext.Activity.Type == ActivityTypes.Message) - { - // do not await task - we want this to run in the background and we will cancel it when its done - typingTask = SendTypingAsync(turnContext, _delay, _period, cts.Token); - } - - await next(cancellationToken).ConfigureAwait(false); - } - finally - { - if (typingTask != null && !typingTask.IsCanceled) - { - // Cancel the typing loop. - cts.Cancel(); - } + await FinishTypingTaskAsync(ctx).ConfigureAwait(false); } + + return await nextSend().ConfigureAwait(false); + }); + + // Start a timer to periodically send the typing activity (bots running as skills should not send typing activity) + if (!IsSkillBot(turnContext) && turnContext.Activity.Type == ActivityTypes.Message) + { + // Override the typing background task. + await FinishTypingTaskAsync(turnContext).ConfigureAwait(false); + StartTypingTask(turnContext); } + + await next(cancellationToken).ConfigureAwait(false); + + // Ensures there are no Tasks left running. + await FinishTypingTaskAsync(turnContext).ConfigureAwait(false); } private static bool IsSkillBot(ITurnContext turnContext) @@ -129,5 +132,50 @@ private static async Task SendTypingActivityAsync(ITurnContext turnContext, Canc // make sure to send the Activity directly on the Adapter rather than via the TurnContext await turnContext.Adapter.SendActivitiesAsync(turnContext, new Activity[] { typingActivity }, cancellationToken).ConfigureAwait(false); } + + /// + /// Starts the typing background task for the current conversation. + /// + /// The context object for this turn. + private void StartTypingTask(ITurnContext turnContext) + { + if (string.IsNullOrEmpty(turnContext?.Activity?.Conversation?.Id) && + _tasks.ContainsKey(turnContext.Activity.Conversation.Id)) + { + return; + } + + var cts = new CancellationTokenSource(); + + // do not await task - we want this to run in the background and we will cancel it when its done + var typingTask = SendTypingAsync(turnContext, _delay, _period, cts.Token); + _tasks.TryAdd(turnContext.Activity.Conversation.Id, (typingTask, cts)); + } + + /// + /// Finishes the typing background task for the current conversation. + /// + /// The context object for this turn. + private async Task FinishTypingTaskAsync(ITurnContext turnContext) + { + if (string.IsNullOrEmpty(turnContext?.Activity?.Conversation?.Id) && + !_tasks.ContainsKey(turnContext.Activity.Conversation.Id)) + { + return; + } + + // Cancel the typing loop. + _tasks.TryGetValue(turnContext.Activity.Conversation.Id, out var item); + var (typingTask, cts) = item; + cts?.Cancel(); + cts?.Dispose(); + if (typingTask != null) + { + await typingTask.ConfigureAwait(false); + typingTask.Dispose(); + } + + _tasks.TryRemove(turnContext.Activity.Conversation.Id, out _); + } } } diff --git a/libraries/Microsoft.Bot.Connector/Teams/TeamsOperations.cs b/libraries/Microsoft.Bot.Connector/Teams/TeamsOperations.cs index 9b0beab02f..418e1898d3 100644 --- a/libraries/Microsoft.Bot.Connector/Teams/TeamsOperations.cs +++ b/libraries/Microsoft.Bot.Connector/Teams/TeamsOperations.cs @@ -97,7 +97,7 @@ public TeamsOperations(TeamsConnectorClient client) var url = new System.Uri(new System.Uri(baseUrl + (baseUrl.EndsWith("/", System.StringComparison.InvariantCulture) ? string.Empty : "/")), "v3/teams/{teamId}/conversations").ToString(); url = url.Replace("{teamId}", System.Uri.EscapeDataString(teamId)); - return await GetResponseAsync(url, shouldTrace, invocationId).ConfigureAwait(false); + return await GetResponseAsync(url, shouldTrace, invocationId, cancellationToken: cancellationToken).ConfigureAwait(false); } /// @@ -151,7 +151,7 @@ public TeamsOperations(TeamsConnectorClient client) var url = new System.Uri(new System.Uri(baseUrl + (baseUrl.EndsWith("/", System.StringComparison.InvariantCulture) ? string.Empty : "/")), "v3/teams/{teamId}").ToString(); url = url.Replace("{teamId}", System.Uri.EscapeDataString(teamId)); - return await GetResponseAsync(url, shouldTrace, invocationId).ConfigureAwait(false); + return await GetResponseAsync(url, shouldTrace, invocationId, cancellationToken: cancellationToken).ConfigureAwait(false); } /// @@ -205,7 +205,7 @@ public TeamsOperations(TeamsConnectorClient client) var url = new System.Uri(new System.Uri(baseUrl + (baseUrl.EndsWith("/", System.StringComparison.InvariantCulture) ? string.Empty : "/")), "v1/meetings/{meetingId}").ToString(); url = url.Replace("{meetingId}", System.Uri.EscapeDataString(meetingId)); - return await GetResponseAsync(url, shouldTrace, invocationId, customHeaders).ConfigureAwait(false); + return await GetResponseAsync(url, shouldTrace, invocationId, customHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); } /// @@ -284,7 +284,7 @@ public TeamsOperations(TeamsConnectorClient client) url = url.Replace("{participantId}", System.Uri.EscapeDataString(participantId)); url = url.Replace("{tenantId}", System.Uri.EscapeDataString(tenantId)); - return await GetResponseAsync(url, shouldTrace, invocationId).ConfigureAwait(false); + return await GetResponseAsync(url, shouldTrace, invocationId, cancellationToken: cancellationToken).ConfigureAwait(false); } private async Task> GetResponseAsync(string url, bool shouldTrace, string invocationId, Dictionary> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken)) diff --git a/tests/Microsoft.Bot.Builder.Dialogs.Adaptive.Tests/ActionTests.cs b/tests/Microsoft.Bot.Builder.Dialogs.Adaptive.Tests/ActionTests.cs index 895594711e..14d08fc017 100644 --- a/tests/Microsoft.Bot.Builder.Dialogs.Adaptive.Tests/ActionTests.cs +++ b/tests/Microsoft.Bot.Builder.Dialogs.Adaptive.Tests/ActionTests.cs @@ -296,12 +296,6 @@ public async Task Action_Foreach_Nested() await TestUtils.RunTestScript(_resourceExplorerFixture.ResourceExplorer); } - [Fact] - public async Task Action_Foreach_Nested_WithCancel() - { - await TestUtils.RunTestScript(_resourceExplorerFixture.ResourceExplorer); - } - [Fact] public async Task Action_Foreach_Object() { diff --git a/tests/Microsoft.Bot.Builder.Dialogs.Adaptive.Tests/Tests/ActionTests/Action_Foreach_Nested_WithCancel.test.dialog b/tests/Microsoft.Bot.Builder.Dialogs.Adaptive.Tests/Tests/ActionTests/Action_Foreach_Nested_WithCancel.test.dialog deleted file mode 100644 index d46caf6b5e..0000000000 --- a/tests/Microsoft.Bot.Builder.Dialogs.Adaptive.Tests/Tests/ActionTests/Action_Foreach_Nested_WithCancel.test.dialog +++ /dev/null @@ -1,67 +0,0 @@ -{ - "$schema": "../../../tests.schema", - "$kind": "Microsoft.Test.Script", - "dialog": { - "$kind": "Microsoft.AdaptiveDialog", - "id": "root", - "triggers": [ - { - "$kind": "Microsoft.OnBeginDialog", - "actions": [ - { - "$kind": "Microsoft.SetProperty", - "property": "dialog.todo", - "value": "=['1', '2', '3']" - }, - { - "$kind": "Microsoft.Foreach", - "itemsProperty": "dialog.todo", - "actions": [ - { - "$kind": "Microsoft.SendActivity", - "activity": "I'm the Parent loop - index is: ${dialog.foreach.index}" - }, - { - "$kind": "Microsoft.Foreach", - "itemsProperty": "dialog.todo", - "actions": [ - { - "$kind": "Microsoft.SendActivity", - "activity": "I'm the child loop and I will cancel all dialogs" - }, - { - "$kind": "Microsoft.CancelAllDialogs" - }, - { - "$kind": "Microsoft.SendActivity", - "activity": "This shouldn't be sent" - } - ] - }, - { - "$kind": "Microsoft.SendActivity", - "activity": "This shouldn't be sent either" - } - ] - } - ] - } - ], - "autoEndDialog": true, - "defaultResultProperty": "dialog.result" - }, - "script": [ - { - "$kind": "Microsoft.Test.UserSays", - "text": "hi" - }, - { - "$kind": "Microsoft.Test.AssertReply", - "text": "I'm the Parent loop - index is: 0" - }, - { - "$kind": "Microsoft.Test.AssertReply", - "text": "I'm the child loop and I will cancel all dialogs" - } - ] -}