From b4c06a4dd23285466b2825a163adeb8d8e8423b3 Mon Sep 17 00:00:00 2001 From: Sindhu Nagesh Date: Mon, 22 Mar 2021 13:27:02 -0700 Subject: [PATCH] Revert "Updating previews/RBAC branch - Master to previews/RBAC (#1825)" This reverts commit ff88adabc302e7adca8d90657ae764e6a6c0c8f1. --- e2e/test/E2EMsTestBase.cs | 3 +- e2e/test/Helpers/ConsoleEventListener.cs | 78 +++++++--- e2e/test/iothub/twin/TwinE2ETests.cs | 6 +- .../E2ETestsSetup/test-resources.bicep | 2 +- .../E2ETestsSetup/test-resources.json | 6 +- .../device/devdoc/amqpTransportExceptions.md | 2 +- iothub/device/samples/readme.md | 73 ++++----- iothub/device/src/DeviceClient.cs | 23 +-- .../Transport/HttpUdsMessageHandler.cs | 20 +-- iothub/device/src/ModuleClient.cs | 23 +-- .../Transport/Amqp/AmqpConnectionHolder.cs | 2 +- .../src/Transport/Amqp/AmqpConnectionPool.cs | 3 +- .../Transport/Amqp/AmqpTransportHandler.cs | 61 +++----- iothub/device/src/Transport/Amqp/AmqpUnit.cs | 8 +- .../src/Transport/Amqp/AmqpUnitManager.cs | 3 +- .../src/Transport/Amqp/IAmqpUnitManager.cs | 3 +- .../Transport/AmqpIoT/AmqpIoTReceivingLink.cs | 91 +++++------ .../src/Transport/Mqtt/MqttIotHubAdapter.cs | 8 +- .../Transport/Mqtt/MqttTransportHandler.cs | 99 ++++++------ iothub/service/src/AmqpServiceClient.cs | 30 +++- iothub/service/src/ServiceClient.cs | 144 +++++++++++------- iothub/service/tests/ServiceClientTests.cs | 17 +++ readme.md | 22 +-- tools/CaptureLogs/readme.md | 2 +- 24 files changed, 364 insertions(+), 365 deletions(-) diff --git a/e2e/test/E2EMsTestBase.cs b/e2e/test/E2EMsTestBase.cs index 9a06aef960..cc8ed9cb28 100644 --- a/e2e/test/E2EMsTestBase.cs +++ b/e2e/test/E2EMsTestBase.cs @@ -23,6 +23,7 @@ namespace Microsoft.Azure.Devices.E2ETests /// public class E2EMsTestBase : IDisposable { + private static readonly string[] s_eventProviders = new string[] { "DotNetty-Default", "Microsoft-Azure-", }; private ConsoleEventListener _listener; // Test specific logger instance @@ -40,7 +41,7 @@ public void TestInitialize() // Note: Events take long and increase run time of the test suite, so only using trace. Logger.Trace($"Starting test - {TestContext.TestName}", SeverityLevel.Information); - _listener = new ConsoleEventListener(); + _listener = new ConsoleEventListener(s_eventProviders); } [TestCleanup] diff --git a/e2e/test/Helpers/ConsoleEventListener.cs b/e2e/test/Helpers/ConsoleEventListener.cs index 0ec251a538..57dddccf4f 100644 --- a/e2e/test/Helpers/ConsoleEventListener.cs +++ b/e2e/test/Helpers/ConsoleEventListener.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -9,47 +8,82 @@ namespace System.Diagnostics.Tracing { public sealed class ConsoleEventListener : EventListener { - // Configure this value to filter all the necessary events when OnEventSourceCreated is called. - // OnEventSourceCreated is triggered as soon as the EventListener is registered and an event source is created. - // So trying to configure this value in the ConsoleEventListener constructor does not work. - // The OnEventSourceCreated can be triggered sooner than the filter is initialized in the ConsoleEventListener constructor. - private static string[] _eventFilters = new string[] { "DotNetty-Default", "Microsoft-Azure-Devices" }; - + private readonly string[] _eventFilters; private readonly object _lock = new object(); - protected override void OnEventSourceCreated(EventSource eventSource) + public ConsoleEventListener(string filter) + { + _eventFilters = new string[1]; + _eventFilters[0] = filter ?? throw new ArgumentNullException(nameof(filter)); + + InitializeEventSources(); + } + + public ConsoleEventListener(string[] filters) + { + _eventFilters = filters ?? throw new ArgumentNullException(nameof(filters)); + if (_eventFilters.Length == 0) + { + throw new ArgumentException("Filters cannot be empty", nameof(filters)); + } + + foreach (string filter in _eventFilters) + { + if (string.IsNullOrWhiteSpace(filter)) + { + throw new ArgumentNullException(nameof(filters)); + } + } + + InitializeEventSources(); + } + + private void InitializeEventSources() { - if (_eventFilters.Any(filter => eventSource.Name.StartsWith(filter, StringComparison.OrdinalIgnoreCase))) + foreach (EventSource source in EventSource.GetSources()) { - base.OnEventSourceCreated(eventSource); - EnableEvents( - eventSource, - EventLevel.LogAlways + EnableEvents(source, EventLevel.LogAlways); + } + } + + protected override void OnEventSourceCreated(EventSource eventSource) + { + base.OnEventSourceCreated(eventSource); + EnableEvents( + eventSource, + EventLevel.LogAlways #if !NET451 , EventKeywords.All #endif ); - } } protected override void OnEventWritten(EventWrittenEventArgs eventData) { + if (_eventFilters == null) + { + return; + } + lock (_lock) { - string eventIdent; + if (_eventFilters.Any(ef => eventData.EventSource.Name.StartsWith(ef, StringComparison.Ordinal))) + { + string eventIdent; #if NET451 // net451 doesn't have EventName, so we'll settle for EventId eventIdent = eventData.EventId.ToString(CultureInfo.InvariantCulture); #else - eventIdent = eventData.EventName; + eventIdent = eventData.EventName; #endif - string text = $"{DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffffff", CultureInfo.InvariantCulture)} [{eventData.EventSource.Name}-{eventIdent}]{(eventData.Payload != null ? $" ({string.Join(", ", eventData.Payload)})." : "")}"; + string text = $"{DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffffff", CultureInfo.InvariantCulture)} [{eventData.EventSource.Name}-{eventIdent}]{(eventData.Payload != null ? $" ({string.Join(", ", eventData.Payload)})." : "")}"; - ConsoleColor origForeground = Console.ForegroundColor; - Console.ForegroundColor = ConsoleColor.DarkYellow; - Console.WriteLine(text); - Debug.WriteLine(text); - Console.ForegroundColor = origForeground; + ConsoleColor origForeground = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.DarkYellow; + Console.WriteLine(text); + Debug.WriteLine(text); + Console.ForegroundColor = origForeground; + } } } } diff --git a/e2e/test/iothub/twin/TwinE2ETests.cs b/e2e/test/iothub/twin/TwinE2ETests.cs index bdea63b787..b4a660bc21 100644 --- a/e2e/test/iothub/twin/TwinE2ETests.cs +++ b/e2e/test/iothub/twin/TwinE2ETests.cs @@ -6,11 +6,11 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.Devices.Client; -using Microsoft.Azure.Devices.Client.Exceptions; using Microsoft.Azure.Devices.E2ETests.Helpers; using Microsoft.Azure.Devices.Shared; using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace Microsoft.Azure.Devices.E2ETests.Twins { @@ -709,12 +709,12 @@ await deviceClient }) .ConfigureAwait(false); } - catch (IotHubException) + catch (Exception) { exceptionThrown = true; } - Assert.IsTrue(exceptionThrown, "IotHubException was expected for updating reported property with an invalid property name, but was not thrown."); + Assert.IsTrue(exceptionThrown, "Exception was expected, but not thrown."); Twin serviceTwin = await registryManager.GetTwinAsync(testDevice.Id).ConfigureAwait(false); Assert.IsFalse(serviceTwin.Properties.Reported.Contains(propName1)); diff --git a/e2e/test/prerequisites/E2ETestsSetup/test-resources.bicep b/e2e/test/prerequisites/E2ETestsSetup/test-resources.bicep index ffb3a2c8cf..5ae37cf3e3 100644 --- a/e2e/test/prerequisites/E2ETestsSetup/test-resources.bicep +++ b/e2e/test/prerequisites/E2ETestsSetup/test-resources.bicep @@ -163,7 +163,7 @@ resource keyVault 'Microsoft.KeyVault/vaults@2018-02-14' = { name: 'standard' family: 'A' } - enableSoftDelete: true + enableSoftDelete: false networkAcls: { defaultAction: 'Allow' bypass: 'AzureServices' diff --git a/e2e/test/prerequisites/E2ETestsSetup/test-resources.json b/e2e/test/prerequisites/E2ETestsSetup/test-resources.json index 2e76516351..5a135a6798 100644 --- a/e2e/test/prerequisites/E2ETestsSetup/test-resources.json +++ b/e2e/test/prerequisites/E2ETestsSetup/test-resources.json @@ -176,7 +176,7 @@ "name": "standard", "family": "A" }, - "enableSoftDelete": true, + "enableSoftDelete": false, "networkAcls": { "defaultAction": "Allow", "bypass": "AzureServices", @@ -342,7 +342,7 @@ "properties": {} }, { - "type": "Microsoft.Security/iotSecuritySolutions", + "type": "Microsoft.Security/IoTSecuritySolutions", "apiVersion": "2019-08-01", "name": "[parameters('SecuritySolutionName')]", "location": "[resourceGroup().location]", @@ -484,7 +484,7 @@ }, "workspaceId": { "type": "string", - "value": "[reference(resourceId('Microsoft.OperationalInsights/workspaces', parameters('OperationalInsightsName')), '2017-03-15-preview').customerId]" + "value": "[format('{0}', reference(resourceId('Microsoft.OperationalInsights/workspaces', parameters('OperationalInsightsName')), '2017-03-15-preview').customerId)]" }, "customAllocationPolicyWebhook": { "type": "string", diff --git a/iothub/device/devdoc/amqpTransportExceptions.md b/iothub/device/devdoc/amqpTransportExceptions.md index 71c6562895..7016dfa8ac 100644 --- a/iothub/device/devdoc/amqpTransportExceptions.md +++ b/iothub/device/devdoc/amqpTransportExceptions.md @@ -41,7 +41,7 @@ Below is the behavior of the SDK on receiving an exception over Amqp transport p | amqp:not-implemented | NotSupportedException | InnerException: AmqpException.Error.Condition = AmqpSymbol.NotImplemented | Inspect the exception details, collect logs and contact service | | amqp:precondition-failed | IotHubException | InnerException: AmqpException.Error.Condition = AmqpSymbol.PreconditionFailed | Inspect the exception details, collect logs and contact service | | amqp:resource-deleted | IotHubException | InnerException: AmqpException.Error.Condition = AmqpSymbol.ResourceDeleted | Inspect the exception details, collect logs and contact service | -| amqp:resource-limit-exceeded | DeviceMaximumQueueDepthExceededException | The correct exception type for this error code is `QuotaExceededException` but it was incorrectly mapped to `DeviceMaximumQueueDepthExceededException`. In order to avoid a breaking change, we now return the correct exception details as an inner exception within the `DeviceMaximumQueueDepthExceededException` thrown. | Upgrade or increase the number of units on your IoT Hub or wait until the next UTC day for the daily quota to refresh and then retry the operation. | +| amqp:resource-limit-exceeded | IotHubException | InnerException: AmqpException.Error.Condition = AmqpSymbol.ResourceLimitExceeded | Inspect the exception details, collect logs and contact service | | amqp:unauthorized-access | UnauthorizedException | InnerException: AmqpException.Error.Condition = AmqpSymbol.UnauthorizedAccess | Inspect your credentials | | com.microsoft:message-lock-lost | DeviceMessageLockLostException | The device client attempted to complete/reject/abandon a received cloud-to-device message, but the lock token was expired (took > 1min after receiving the message to complete/reject/abandon it) | Call `ReceiveAsync()` again to retrieve an updated lock token, and then complete/reject/abandon the message. De-duplication logic wil need to be implemented at the application level | | amqp:transaction :unknown-id | IotHubException | InnerException: AmqpException.Error.Condition = AmqpSymbol.TransactionUnknownId | Inspect the exception details, collect logs and contact service | diff --git a/iothub/device/samples/readme.md b/iothub/device/samples/readme.md index 3908583189..e28b7d4056 100644 --- a/iothub/device/samples/readme.md +++ b/iothub/device/samples/readme.md @@ -2,81 +2,70 @@ This folder contains simple samples showing how to use the various features of Microsoft Azure IoT Hub service, from a device running C# code. ### [Device samples][device-samples] - -- [Reconnection sample][d-message-sample] - - This sample illustrates how to write a device application to handle connection issues, connection-related exceptions, and how to manage the lifetime of the `DeviceClient` - - Includes sending messages and symmetric key failover -- [Method sample][d-method-sample] -- [Receive message sample][d-receive-message-sample] -- [Twin sample][d-twin-sample] -- [File upload sample][d-file-upload-sample] -- [Import/export devices sample][d-import-export-devices-sample] -- [Connect with X509 certificate sample][d-x509-cert-sample] -- [Plug and Play device samples][d-pnp-sample] -- [Xamarin sample][d-xamarin-sample] - -### Module sample - -- [Message sample][m-message-sample] - - This sample illustrates how to write an IoT Hub module to handle connection issues, connection-related exceptions, and how to manage the lifetime of the `ModuleClient` - - Includes sending messages and symmetric key failover +* [Message Sample][d-message-sample] +* [Method Sample][d-method-sample] +* [Twin Sample][d-twin-sample] +* [File Upload Sample][d-file-upload-sample] +* [Keys Rollover Sample][d-keys-rollover-sample] +* [Import Export Devices Sample][d-import-export-devices-sample] +* [Plug And Play Device Sample][d-pnp-sample] +* [Xamarin Sample][d-xamarin-sample] + +### [Module samples][module-samples] +* [Message Sample][m-message-sample] +* [Twin Sample][m-twin-sample] ### Prerequisites - In order to run the device samples on Linux or Windows, you will first need the following prerequisites: - -- [Setup your IoT hub][lnk-setup-iot-hub] -- [Provision your device and get its credentials][lnk-manage-iot-device] +* [Setup your IoT hub][lnk-setup-iot-hub] +* [Provision your device and get its credentials][lnk-manage-iot-device] ### Setup environment The following prerequisite is the minimum requirement to build and run the samples. -- Install the latest .NET Core from +Visual Studio is **not required** to run the samples. -> Visual Studio is **not required** to run the samples. +- Install the latest .NET Core from https://dot.net ### Get and run the samples - You need to clone the repository or download the sample (the one you want to try) project's folder on your device. -#### Build and run the samples +#### Build and run the samples: +1. Preparing the sample application: + 1. Set the following environment variables on the terminal from which you want to run the application. -1. Building the sample application + * IOTHUB_DEVICE_CONNECTION_STRING + +2. Building the sample application: To build the sample application using dotnet, from terminal navigate to the sample folder (where the .csproj file lives). Then execute the following command and check for build errors: - ```console + ``` dotnet build ``` -1. Preparing the sample application: - 1. Many of these samples take parameters. To see the parameters required, type: - - ```console - dotnet run --help - ``` - -1. Running the sample application: +3. Running the sample application: - To run the sample application using dotnet, execute the following command with any required parameters discovered in the previous step. + To run the sample application using dotnet, execute the following command. - ```console + ``` dotnet run ``` [device-samples]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/device -[d-message-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/device/DeviceReconnectionSample -[d-receive-message-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/device/MessageReceiveSample +[d-message-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/device/MessageSample [d-method-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/device/MethodSample [d-twin-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/device/TwinSample [d-file-upload-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/device/FileUploadSample -[d-x509-cert-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/device/X509DeviceCertWithChainSample +[d-keys-rollover-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/device/KeysRolloverSample [d-import-export-devices-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/device/ImportExportDevicesSample [d-pnp-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/device/PnpDeviceSamples [d-xamarin-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/device/XamarinSample -[m-message-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/module/ModuleSample +[module-samples]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/module +[m-message-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/module/MessageSample +[m-twin-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/module/TwinSample [lnk-setup-iot-hub]: https://aka.ms/howtocreateazureiothub [lnk-manage-iot-device]: https://github.com/Azure/azure-iot-device-ecosystem/blob/master/setup_iothub.md#create-new-device-in-the-iot-hub-device-identity-registry \ No newline at end of file diff --git a/iothub/device/src/DeviceClient.cs b/iothub/device/src/DeviceClient.cs index 9ab69f8f3e..43966a49c6 100644 --- a/iothub/device/src/DeviceClient.cs +++ b/iothub/device/src/DeviceClient.cs @@ -15,7 +15,7 @@ namespace Microsoft.Azure.Devices.Client /// Contains methods that a device can use to send messages to and receive from the service. /// /// - public class DeviceClient : IDisposable + public sealed class DeviceClient : IDisposable { /// /// Default operation timeout. @@ -608,26 +608,7 @@ public void SetConnectionStatusChangesHandler(ConnectionStatusChangesHandler sta /// /// Releases the unmanaged resources used by the DeviceClient and optionally disposes of the managed resources. /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Releases the unmanaged resources used by the DeviceClient and allows for any derived class to override and - /// provide custom implementation. - /// - /// Setting to true will release both managed and unmanaged resources. Setting to - /// false will only release the unmanaged resources. - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - InternalClient?.Dispose(); - InternalClient = null; - } - } + public void Dispose() => InternalClient?.Dispose(); /// /// Set a callback that will be called whenever the client receives a state update diff --git a/iothub/device/src/ModernDotNet/HsmAuthentication/Transport/HttpUdsMessageHandler.cs b/iothub/device/src/ModernDotNet/HsmAuthentication/Transport/HttpUdsMessageHandler.cs index b72c87d203..bb68169b85 100644 --- a/iothub/device/src/ModernDotNet/HsmAuthentication/Transport/HttpUdsMessageHandler.cs +++ b/iothub/device/src/ModernDotNet/HsmAuthentication/Transport/HttpUdsMessageHandler.cs @@ -25,7 +25,6 @@ protected override async Task SendAsync(HttpRequestMessage using var stream = new HttpBufferedStream(new NetworkStream(socket, true)); byte[] requestBytes = HttpRequestResponseSerializer.SerializeRequest(request); - #if NET451 || NET472 || NETSTANDARD2_0 await stream.WriteAsync(requestBytes, 0, requestBytes.Length, cancellationToken).ConfigureAwait(false); #else @@ -43,25 +42,10 @@ protected override async Task SendAsync(HttpRequestMessage private async Task GetConnectedSocketAsync() { + var endpoint = new UnixDomainSocketEndPoint(_providerUri.LocalPath); Socket socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); - - // The Edge Agent uses unix sockets for communication with the modules deployed in docker for HSM. - // For netstandard 2.0 there was no implementation for a Unix Domain Socket (UDS) so we used a version - // that was part of a test that was reused in a number of libraries on the internet. - // - // https://github.com/dotnet/corefx/blob/12b51c6bf153cc237b251a4e264d5e7c0ee84a33/src/System.IO.Pipes/src/System/Net/Sockets/UnixDomainSocketEndPoint.cs - // https://github.com/dotnet/corefx/blob/12b51c6bf153cc237b251a4e264d5e7c0ee84a33/src/System.Net.Sockets/tests/FunctionalTests/UnixDomainSocketTest.cs#L248 - // - // Since then the UnixDomainSocketEndpoint has been added to the dotnet framework and there has been considerable work - // around unix sockets in the BCL. For older versions of the framework we will continue to use the existing class since it works - // fine. For netcore 2.1 and greater as well as .NET 5.0 and greater we'll use the native framework version. - -#if NET451 || NET472 || NETSTANDARD2_0 - var endpoint = new Microsoft.Azure.Devices.Client.HsmAuthentication.Transport.UnixDomainSocketEndPoint(_providerUri.LocalPath); -#else - var endpoint = new System.Net.Sockets.UnixDomainSocketEndPoint(_providerUri.LocalPath); -#endif await socket.ConnectAsync(endpoint).ConfigureAwait(false); + return socket; } } diff --git a/iothub/device/src/ModuleClient.cs b/iothub/device/src/ModuleClient.cs index 8970dd1f0d..901a84d57e 100644 --- a/iothub/device/src/ModuleClient.cs +++ b/iothub/device/src/ModuleClient.cs @@ -21,7 +21,7 @@ namespace Microsoft.Azure.Devices.Client /// /// Contains methods that a module can use to send messages to and receive from the service and interact with module twins. /// - public class ModuleClient : IDisposable + public sealed class ModuleClient : IDisposable { private const string ModuleMethodUriFormat = "/twins/{0}/modules/{1}/methods?" + ClientApiVersionHelper.ApiVersionQueryStringLatest; private const string DeviceMethodUriFormat = "/twins/{0}/methods?" + ClientApiVersionHelper.ApiVersionQueryStringLatest; @@ -433,26 +433,7 @@ public void SetConnectionStatusChangesHandler(ConnectionStatusChangesHandler sta /// /// Releases the unmanaged resources used by the ModuleClient and optionally disposes of the managed resources. /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Releases the unmanaged resources used by the ModuleClient and allows for any derived class to override and - /// provide custom implementation. - /// - /// Setting to true will release both managed and unmanaged resources. Setting to - /// false will only release the unmanaged resources. - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - InternalClient?.Dispose(); - InternalClient = null; - } - } + public void Dispose() => InternalClient?.Dispose(); /// /// Set a callback that will be called whenever the client receives a state update diff --git a/iothub/device/src/Transport/Amqp/AmqpConnectionHolder.cs b/iothub/device/src/Transport/Amqp/AmqpConnectionHolder.cs index f3d0eaea15..bc828df3b1 100644 --- a/iothub/device/src/Transport/Amqp/AmqpConnectionHolder.cs +++ b/iothub/device/src/Transport/Amqp/AmqpConnectionHolder.cs @@ -36,7 +36,7 @@ public AmqpConnectionHolder(DeviceIdentity deviceIdentity) public AmqpUnit CreateAmqpUnit( DeviceIdentity deviceIdentity, Func onMethodCallback, - Action twinMessageListener, + Action twinMessageListener, Func onModuleMessageReceivedCallback, Func onDeviceMessageReceivedCallback, Action onUnitDisconnected) diff --git a/iothub/device/src/Transport/Amqp/AmqpConnectionPool.cs b/iothub/device/src/Transport/Amqp/AmqpConnectionPool.cs index 937c7b9544..cef42a9dae 100644 --- a/iothub/device/src/Transport/Amqp/AmqpConnectionPool.cs +++ b/iothub/device/src/Transport/Amqp/AmqpConnectionPool.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using Microsoft.Azure.Devices.Shared; using Microsoft.Azure.Devices.Client.Transport.AmqpIoT; -using Microsoft.Azure.Devices.Client.Exceptions; namespace Microsoft.Azure.Devices.Client.Transport.Amqp { @@ -19,7 +18,7 @@ internal class AmqpConnectionPool : IAmqpUnitManager public AmqpUnit CreateAmqpUnit( DeviceIdentity deviceIdentity, Func onMethodCallback, - Action twinMessageListener, + Action twinMessageListener, Func onModuleMessageReceivedCallback, Func onDeviceMessageReceivedCallback, Action onUnitDisconnected) diff --git a/iothub/device/src/Transport/Amqp/AmqpTransportHandler.cs b/iothub/device/src/Transport/Amqp/AmqpTransportHandler.cs index 6a49418f01..0dae99b25f 100644 --- a/iothub/device/src/Transport/Amqp/AmqpTransportHandler.cs +++ b/iothub/device/src/Transport/Amqp/AmqpTransportHandler.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Microsoft.Azure.Devices.Client.Exceptions; using Microsoft.Azure.Devices.Client.Transport.AmqpIoT; using Microsoft.Azure.Devices.Shared; @@ -325,8 +324,7 @@ public override async Task EnableTwinPatchAsync(CancellationToken cancellationTo try { cancellationToken.ThrowIfCancellationRequested(); - string correlationId = AmqpTwinMessageType.Put + Guid.NewGuid().ToString(); - await _amqpUnit.SendTwinMessageAsync(AmqpTwinMessageType.Put, correlationId, null, _operationTimeout).ConfigureAwait(false); + await _amqpUnit.SendTwinMessageAsync(AmqpTwinMessageType.Put, Guid.NewGuid().ToString(), null, _operationTimeout).ConfigureAwait(false); } finally { @@ -357,7 +355,7 @@ public override async Task SendTwinGetAsync(CancellationToken cancellation try { await EnableTwinPatchAsync(cancellationToken).ConfigureAwait(false); - Twin twin = await RoundTripTwinMessageAsync(AmqpTwinMessageType.Get, null, cancellationToken).ConfigureAwait(false); + Twin twin = await RoundTripTwinMessage(AmqpTwinMessageType.Get, null, cancellationToken).ConfigureAwait(false); if (twin == null) { throw new InvalidOperationException("Service rejected the message"); @@ -377,7 +375,7 @@ public override async Task SendTwinPatchAsync(TwinCollection reportedProperties, try { await EnableTwinPatchAsync(cancellationToken).ConfigureAwait(false); - await RoundTripTwinMessageAsync(AmqpTwinMessageType.Patch, reportedProperties, cancellationToken).ConfigureAwait(false); + Twin twin = await RoundTripTwinMessage(AmqpTwinMessageType.Patch, reportedProperties, cancellationToken).ConfigureAwait(false); } finally { @@ -385,11 +383,11 @@ public override async Task SendTwinPatchAsync(TwinCollection reportedProperties, } } - private async Task RoundTripTwinMessageAsync(AmqpTwinMessageType amqpTwinMessageType, TwinCollection reportedProperties, CancellationToken cancellationToken) + private async Task RoundTripTwinMessage(AmqpTwinMessageType amqpTwinMessageType, TwinCollection reportedProperties, CancellationToken cancellationToken) { - Logging.Enter(this, cancellationToken, $"{nameof(RoundTripTwinMessageAsync)}"); + Logging.Enter(this, cancellationToken, $"{nameof(RoundTripTwinMessage)}"); - string correlationId = amqpTwinMessageType + Guid.NewGuid().ToString(); + string correlationId = Guid.NewGuid().ToString(); Twin response = null; try @@ -401,17 +399,16 @@ private async Task RoundTripTwinMessageAsync(AmqpTwinMessageType amqpTwinM await _amqpUnit.SendTwinMessageAsync(amqpTwinMessageType, correlationId, reportedProperties, _operationTimeout).ConfigureAwait(false); var receivingTask = taskCompletionSource.Task; - if (await Task.WhenAny(receivingTask, Task.Delay(TimeSpan.FromSeconds(ResponseTimeoutInSeconds), cancellationToken)).ConfigureAwait(false) == receivingTask) { - if ((receivingTask.Exception != null) && (receivingTask.Exception.InnerException != null)) - { - throw receivingTask.Exception.InnerException; - } // Task completed within timeout. // Consider that the task may have faulted or been canceled. // We re-await the task so that any exceptions/cancellation is rethrown. response = await receivingTask.ConfigureAwait(false); + if (response == null) + { + throw new InvalidOperationException("Service response is null"); + } } else { @@ -422,7 +419,7 @@ private async Task RoundTripTwinMessageAsync(AmqpTwinMessageType amqpTwinM finally { _twinResponseCompletions.TryRemove(correlationId, out _); - Logging.Exit(this, cancellationToken, $"{nameof(RoundTripTwinMessageAsync)}"); + Logging.Exit(this, cancellationToken, $"{nameof(RoundTripTwinMessage)}"); } return response; @@ -523,37 +520,21 @@ private async Task DisposeMessageAsync(string lockToken, AmqpIoTDisposeActions o #region Helpers - private void TwinMessageListener(Twin twin, string correlationId, TwinCollection twinCollection, IotHubException ex = default) + private void TwinMessageListener(Twin twin, string correlationId, TwinCollection twinCollection) { - if (correlationId == null) + if (correlationId != null) { - // This is desired property updates, so call the callback with TwinCollection. - _onDesiredStatePatchListener(twinCollection); + // It is a GET, just complete the task. + TaskCompletionSource task; + if (_twinResponseCompletions.TryRemove(correlationId, out task)) + { + task.SetResult(twin); + } } else { - if (correlationId.StartsWith(AmqpTwinMessageType.Get.ToString(), StringComparison.OrdinalIgnoreCase) || - correlationId.StartsWith(AmqpTwinMessageType.Patch.ToString(), StringComparison.OrdinalIgnoreCase)) - { - // For Get and Patch, complete the task. - TaskCompletionSource task; - if (_twinResponseCompletions.TryRemove(correlationId, out task)) - { - if(ex == default) - { - task.SetResult(twin); - } - else - { - task.SetException(ex); - } - } - else - { - // This can happen if we received a message from service with correlation Id that was not set by SDK or does not exist in dictionary. - Logging.Info("Could not remove correlation id to complete the task awaiter for a twin operation.", nameof(TwinMessageListener)); - } - } + // It is a PATCH, just call the callback with the TwinCollection + _onDesiredStatePatchListener(twinCollection); } } diff --git a/iothub/device/src/Transport/Amqp/AmqpUnit.cs b/iothub/device/src/Transport/Amqp/AmqpUnit.cs index 4b08a12f94..26c07ab5d9 100644 --- a/iothub/device/src/Transport/Amqp/AmqpUnit.cs +++ b/iothub/device/src/Transport/Amqp/AmqpUnit.cs @@ -19,7 +19,7 @@ internal class AmqpUnit : IDisposable private readonly DeviceIdentity _deviceIdentity; private readonly Func _onMethodCallback; - private readonly Action _twinMessageListener; + private readonly Action _twinMessageListener; private readonly Func _onModuleMessageReceivedCallback; private readonly Func _onDeviceMessageReceivedCallback; private readonly IAmqpConnectionHolder _amqpConnectionHolder; @@ -54,7 +54,7 @@ public AmqpUnit( DeviceIdentity deviceIdentity, IAmqpConnectionHolder amqpConnectionHolder, Func onMethodCallback, - Action twinMessageListener, + Action twinMessageListener, Func onModuleMessageReceivedCallback, Func onDeviceMessageReceivedCallback, Action onUnitDisconnected) @@ -736,13 +736,13 @@ private async Task OpenTwinSenderLinkAsync(AmqpIoTSession amqpIoTSession, string } } - private void OnDesiredPropertyReceived(Twin twin, string correlationId, TwinCollection twinCollection, IotHubException ex = default) + private void OnDesiredPropertyReceived(Twin twin, string correlationId, TwinCollection twinCollection) { Logging.Enter(this, twin, nameof(OnDesiredPropertyReceived)); try { - _twinMessageListener?.Invoke(twin, correlationId, twinCollection, ex); + _twinMessageListener?.Invoke(twin, correlationId, twinCollection); } finally { diff --git a/iothub/device/src/Transport/Amqp/AmqpUnitManager.cs b/iothub/device/src/Transport/Amqp/AmqpUnitManager.cs index ea31509155..aa0904aff2 100644 --- a/iothub/device/src/Transport/Amqp/AmqpUnitManager.cs +++ b/iothub/device/src/Transport/Amqp/AmqpUnitManager.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using Microsoft.Azure.Devices.Shared; using Microsoft.Azure.Devices.Client.Transport.AmqpIoT; -using Microsoft.Azure.Devices.Client.Exceptions; namespace Microsoft.Azure.Devices.Client.Transport.Amqp { @@ -30,7 +29,7 @@ internal static AmqpUnitManager GetInstance() public AmqpUnit CreateAmqpUnit( DeviceIdentity deviceIdentity, Func onMethodCallback, - Action twinMessageListener, + Action twinMessageListener, Func onModuleMessageReceivedCallback, Func onDeviceMessageReceivedCallback, Action onUnitDisconnected) diff --git a/iothub/device/src/Transport/Amqp/IAmqpUnitManager.cs b/iothub/device/src/Transport/Amqp/IAmqpUnitManager.cs index 3e1f5a1de2..882e2eae6e 100644 --- a/iothub/device/src/Transport/Amqp/IAmqpUnitManager.cs +++ b/iothub/device/src/Transport/Amqp/IAmqpUnitManager.cs @@ -3,7 +3,6 @@ using System; using System.Threading.Tasks; -using Microsoft.Azure.Devices.Client.Exceptions; using Microsoft.Azure.Devices.Client.Transport.AmqpIoT; using Microsoft.Azure.Devices.Shared; @@ -14,7 +13,7 @@ internal interface IAmqpUnitManager AmqpUnit CreateAmqpUnit( DeviceIdentity deviceIdentity, Func onMethodCallback, - Action twinMessageListener, + Action twinMessageListener, Func onModuleMessageReceivedCallback, Func onDeviceMessageReceivedCallback, Action onUnitDisconnected); diff --git a/iothub/device/src/Transport/AmqpIoT/AmqpIoTReceivingLink.cs b/iothub/device/src/Transport/AmqpIoT/AmqpIoTReceivingLink.cs index 46e64a8e45..0ed8dc63f3 100644 --- a/iothub/device/src/Transport/AmqpIoT/AmqpIoTReceivingLink.cs +++ b/iothub/device/src/Transport/AmqpIoT/AmqpIoTReceivingLink.cs @@ -23,7 +23,7 @@ internal class AmqpIoTReceivingLink private Action _onEventsReceived; private Action _onDeviceMessageReceived; private Action _onMethodReceived; - private Action _onTwinMessageReceived; + private Action _onDesiredPropertyReceived; public AmqpIoTReceivingLink(ReceivingAmqpLink receivingAmqpLink) { @@ -255,102 +255,91 @@ private void DisposeDelivery(AmqpMessage amqpMessage, bool settled, Accepted acc #region Twin handling - internal void RegisterTwinListener(Action onDesiredPropertyReceived) + internal void RegisterTwinListener(Action onDesiredPropertyReceived) { - _onTwinMessageReceived = onDesiredPropertyReceived; - _receivingAmqpLink.RegisterMessageListener(OnTwinChangesReceived); + _onDesiredPropertyReceived = onDesiredPropertyReceived; + _receivingAmqpLink.RegisterMessageListener(OnDesiredPropertyReceived); } - private void OnTwinChangesReceived(AmqpMessage amqpMessage) + private void OnDesiredPropertyReceived(AmqpMessage amqpMessage) { if (Logging.IsEnabled) { - Logging.Enter(this, amqpMessage, $"{nameof(OnTwinChangesReceived)}"); + Logging.Enter(this, amqpMessage, $"{nameof(OnDesiredPropertyReceived)}"); } try { _receivingAmqpLink.DisposeDelivery(amqpMessage, true, AmqpIoTConstants.AcceptedOutcome); string correlationId = amqpMessage.Properties?.CorrelationId?.ToString(); - int status = GetStatus(amqpMessage); + + if (!VerifyResponseMessage(amqpMessage)) + { + _onDesiredPropertyReceived.Invoke(null, correlationId, null); + } Twin twin = null; TwinCollection twinProperties = null; - if (status >= 400) + if (correlationId != null) { - // Handle failures - if (correlationId.StartsWith(AmqpTwinMessageType.Get.ToString(), StringComparison.OrdinalIgnoreCase) - || correlationId.StartsWith(AmqpTwinMessageType.Patch.ToString(), StringComparison.OrdinalIgnoreCase)) + if (amqpMessage.BodyStream != null) { - string error = null; - using (var reader = new StreamReader(amqpMessage.BodyStream, System.Text.Encoding.UTF8)) + // This a result of a GET TWIN so return (set) the full twin + using (StreamReader reader = new StreamReader(amqpMessage.BodyStream, System.Text.Encoding.UTF8)) { - error = reader.ReadToEnd(); - }; - - // Retry for Http status code request timeout, Too many requests and server errors - var exception = new IotHubException(error, status >= 500 || status == 429 || status == 408); - _onTwinMessageReceived.Invoke(null, correlationId, null, exception); + string body = reader.ReadToEnd(); + var properties = JsonConvert.DeserializeObject(body); + twin = new Twin(properties); + } + } + else + { + // This is a desired property ack from the service + twin = new Twin(); } } else { - if (correlationId == null) + // No correlationId, this is a PATCH sent by the sevice so return (set) the TwinCollection + + using (StreamReader reader = new StreamReader(amqpMessage.BodyStream, System.Text.Encoding.UTF8)) { - // Here we are getting desired property update notifications and want to handle it first - using var reader = new StreamReader(amqpMessage.BodyStream, System.Text.Encoding.UTF8); string patch = reader.ReadToEnd(); twinProperties = JsonConvert.DeserializeObject(patch); } - else if (correlationId.StartsWith(AmqpTwinMessageType.Get.ToString(), StringComparison.OrdinalIgnoreCase)) - { - // This a response of a GET TWIN so return (set) the full twin - using var reader = new StreamReader(amqpMessage.BodyStream, System.Text.Encoding.UTF8); - string body = reader.ReadToEnd(); - var properties = JsonConvert.DeserializeObject(body); - twin = new Twin(properties); - } - else if (correlationId.StartsWith(AmqpTwinMessageType.Patch.ToString(), StringComparison.OrdinalIgnoreCase)) - { - // This can be used to coorelate success response with updating reported properties - // However currently we do not have it as request response style implementation - Logging.Info("Updated twin reported properties successfully", nameof(OnTwinChangesReceived)); - } - else if (correlationId.StartsWith(AmqpTwinMessageType.Put.ToString(), StringComparison.OrdinalIgnoreCase)) - { - // This is an acknowledgement received from service for subscribing to desired property updates - Logging.Info("Subscribed for twin successfully", nameof(OnTwinChangesReceived)); - } - else - { - // This shouldn't happen - Logging.Info("Received a correlation Id for Twin operation that does not match Get, Patch or Put request", nameof(OnTwinChangesReceived)); - } - _onTwinMessageReceived.Invoke(twin, correlationId, twinProperties, null); } + _onDesiredPropertyReceived.Invoke(twin, correlationId, twinProperties); } finally { if (Logging.IsEnabled) { - Logging.Exit(this, amqpMessage, $"{nameof(OnTwinChangesReceived)}"); + Logging.Exit(this, amqpMessage, $"{nameof(OnDesiredPropertyReceived)}"); } } } #endregion Twin handling - internal static int GetStatus(AmqpMessage response) + internal static bool VerifyResponseMessage(AmqpMessage response) { + bool retVal = true; if (response != null) { if (response.MessageAnnotations.Map.TryGetValue(AmqpIoTConstants.ResponseStatusName, out int status)) { - return status; + if (status >= 400) + { + retVal = false; + } } } - return -1; + else + { + retVal = false; + } + return retVal; } } } diff --git a/iothub/device/src/Transport/Mqtt/MqttIotHubAdapter.cs b/iothub/device/src/Transport/Mqtt/MqttIotHubAdapter.cs index 34e445f544..e834d335ab 100644 --- a/iothub/device/src/Transport/Mqtt/MqttIotHubAdapter.cs +++ b/iothub/device/src/Transport/Mqtt/MqttIotHubAdapter.cs @@ -31,8 +31,6 @@ namespace Microsoft.Azure.Devices.Client.Transport.Mqtt // EventLoop. To limit I/O to the EventLoopGroup and keep Netty semantics, we are going to ensure that the // task continuations are executed by this scheduler using ConfigureAwait(true). // - // All awaited calls that happen within dotnetty's pipeline should be ConfigureAwait(true). - // internal sealed class MqttIotHubAdapter : ChannelHandlerAdapter { [Flags] @@ -479,7 +477,7 @@ private static async void PingServerAsync(object ctx) Logging.Info(context, $"Idle time was {idleTime}, so ping request was sent.", nameof(PingServerAsync)); // Wait to capture the ping response semaphore, which is released when a PINGRESP packet is received. - bool receivedPingResponse = await s_pingResponseSemaphore.WaitAsync(s_pingResponseTimeout).ConfigureAwait(true); + bool receivedPingResponse = await s_pingResponseSemaphore.WaitAsync(s_pingResponseTimeout).ConfigureAwait(false); if (!receivedPingResponse) { if (Logging.IsEnabled) @@ -1171,7 +1169,7 @@ public async Task ComposePublishPacketAsync(IChannelHandlerContex int length = (int)streamLength; IByteBuffer buffer = context.Channel.Allocator.Buffer(length, length); - await buffer.WriteBytesAsync(payloadStream, length).ConfigureAwait(true); + await buffer.WriteBytesAsync(payloadStream, length).ConfigureAwait(false); Contract.Assert(buffer.ReadableBytes == length); packet.Payload = buffer; @@ -1376,7 +1374,7 @@ public static async Task WriteMessageAsync(IChannelHandlerContext context, objec { try { - await context.WriteAndFlushAsync(message).ConfigureAwait(true); + await context.WriteAndFlushAsync(message).ConfigureAwait(false); } catch (Exception ex) { diff --git a/iothub/device/src/Transport/Mqtt/MqttTransportHandler.cs b/iothub/device/src/Transport/Mqtt/MqttTransportHandler.cs index caaf8959c4..bf1e8971e7 100644 --- a/iothub/device/src/Transport/Mqtt/MqttTransportHandler.cs +++ b/iothub/device/src/Transport/Mqtt/MqttTransportHandler.cs @@ -35,13 +35,6 @@ namespace Microsoft.Azure.Devices.Client.Transport.Mqtt { - // - // Note on ConfigureAwait: dotNetty is using a custom TaskScheduler that binds Tasks to the corresponding - // EventLoop. To limit I/O to the EventLoopGroup and keep Netty semantics, we are going to ensure that the - // task continuations are executed by this scheduler using ConfigureAwait(true). - // - // All awaited calls that happen within dotnetty's pipeline should be ConfigureAwait(true). - // internal sealed class MqttTransportHandler : TransportHandler, IMqttIotHubEventHandler { private const int ProtocolGatewayPort = 8883; @@ -221,7 +214,7 @@ public override async Task OpenAsync(CancellationToken cancellationToken) EnsureValidState(throwIfNotOpen: false); - await OpenInternalAsync(cancellationToken).ConfigureAwait(false); + await OpenInternalAsync(cancellationToken).ConfigureAwait(true); } finally { @@ -242,7 +235,7 @@ public override async Task SendEventAsync(Message message, CancellationToken can EnsureValidState(); Debug.Assert(_channel != null); - await _channel.WriteAndFlushAsync(message).ConfigureAwait(true); + await _channel.WriteAndFlushAsync(message).ConfigureAwait(false); } finally { @@ -256,7 +249,7 @@ public override async Task SendEventAsync(IEnumerable messages, Cancell foreach (Message message in messages) { cancellationToken.ThrowIfCancellationRequested(); - await SendEventAsync(message, cancellationToken).ConfigureAwait(false); + await SendEventAsync(message, cancellationToken).ConfigureAwait(true); } } @@ -285,11 +278,13 @@ public override async Task ReceiveAsync(CancellationToken cancellationT if (State != TransportState.Receiving) { - await SubscribeCloudToDeviceMessagesAsync().ConfigureAwait(false); + await SubscribeCloudToDeviceMessagesAsync().ConfigureAwait(true); } - await WaitUntilC2dMessageArrivesAsync(cancellationToken).ConfigureAwait(false); - return ProcessMessage(); + bool hasMessage = await ReceiveMessageArrivalAsync(cancellationToken).ConfigureAwait(true); + Message message = ProcessMessage(hasMessage); + + return message; } finally { @@ -321,14 +316,15 @@ public override async Task ReceiveAsync(TimeoutHelper timeoutHelper) if (State != TransportState.Receiving) { - await SubscribeCloudToDeviceMessagesAsync().ConfigureAwait(false); + await SubscribeCloudToDeviceMessagesAsync().ConfigureAwait(true); } TimeSpan timeout = timeoutHelper.GetRemainingTime(); using var cts = new CancellationTokenSource(timeout); + bool hasMessage = await ReceiveMessageArrivalAsync(cts.Token).ConfigureAwait(true); + Message message = ProcessMessage(hasMessage); - await WaitUntilC2dMessageArrivesAsync(cts.Token).ConfigureAwait(false); - return ProcessMessage(); + return message; } finally { @@ -337,25 +333,28 @@ public override async Task ReceiveAsync(TimeoutHelper timeoutHelper) } } - private Message ProcessMessage() + private Message ProcessMessage(bool hasMessage) { Message message = null; try { if (Logging.IsEnabled) - Logging.Enter(this, message, $"Will begin processing received C2D message, queue size={_messageQueue.Count}", nameof(ProcessMessage)); + Logging.Enter(this, message, $"hasMessage={hasMessage}", nameof(ProcessMessage)); - lock (_syncRoot) + if (hasMessage) { - if (_messageQueue.TryDequeue(out message)) + lock (_syncRoot) { - if (_qos == QualityOfService.AtLeastOnce) + if (_messageQueue.TryDequeue(out message)) { - _completionQueue.Enqueue(message.LockToken); - } + if (_qos == QualityOfService.AtLeastOnce) + { + _completionQueue.Enqueue(message.LockToken); + } - message.LockToken = _generationId + message.LockToken; + message.LockToken = _generationId + message.LockToken; + } } } @@ -364,11 +363,11 @@ private Message ProcessMessage() finally { if (Logging.IsEnabled) - Logging.Exit(this, message, $"Processed received C2D message with Id={message?.MessageId}", nameof(ProcessMessage)); + Logging.Exit(this, message, $"hasMessage={hasMessage}", nameof(ProcessMessage)); } } - private async Task WaitUntilC2dMessageArrivesAsync(CancellationToken cancellationToken) + private async Task ReceiveMessageArrivalAsync(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); CancellationToken disconnectToken = _disconnectAwaitersCancellationSource.Token; @@ -376,8 +375,8 @@ private async Task WaitUntilC2dMessageArrivesAsync(CancellationToken cancellatio using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, disconnectToken); - // Wait until either of the linked cancellation tokens have been canceled. - await _receivingSemaphore.WaitAsync(linkedCts.Token).ConfigureAwait(false); + // -1 millisecond represents for SemaphoreSlim to wait indefinitely until either of the linked cancellation tokens have been canceled. + return await _receivingSemaphore.WaitAsync(TimeSpan.FromMilliseconds(-1), linkedCts.Token).ConfigureAwait(true); } public override async Task CompleteAsync(string lockToken, CancellationToken cancellationToken) @@ -413,7 +412,7 @@ public override async Task CompleteAsync(string lockToken, CancellationToken can lockToken.Length != actualLockToken.Length + s_generationPrefixLength) { throw new IotHubException( - $"Client must send PUBACK packets in the order in which the corresponding PUBLISH packets were received (QoS 1 messages) per [MQTT-4.6.0-2]. Expected lock token to end with: '{actualLockToken}'; actual lock token: '{lockToken}'.", + $"Client must send PUBACK packets in the order in which the corresponding PUBLISH packets were received (QoS 1 messages) per [MQTT-4.6.0-2]. Expected lock token: '{actualLockToken}'; actual lock token: '{lockToken}'.", isTransient: false); } @@ -522,7 +521,7 @@ private async Task HandleIncomingTwinPatchAsync(Message message) using var reader = new StreamReader(message.GetBodyStream(), System.Text.Encoding.UTF8); string patch = reader.ReadToEnd(); TwinCollection props = JsonConvert.DeserializeObject(patch); - await Task.Run(() => _onDesiredStatePatchListener(props)).ConfigureAwait(false); + await Task.Run(() => _onDesiredStatePatchListener(props)).ConfigureAwait(true); } } finally @@ -538,7 +537,7 @@ private async Task HandleIncomingMethodPostAsync(Message message) string[] tokens = Regex.Split(message.MqttTopicName, "/", RegexOptions.Compiled, s_regexTimeoutMilliseconds); using var mr = new MethodRequestInternal(tokens[3], tokens[4].Substring(6), message.GetBodyStream(), CancellationToken.None); - await Task.Run(() => _methodListener(mr)).ConfigureAwait(false); + await Task.Run(() => _methodListener(mr)).ConfigureAwait(true); } finally { @@ -555,12 +554,8 @@ private async Task HandleIncomingMessagesAsync() if (Logging.IsEnabled) Logging.Enter(this, "Process C2D message via callback", nameof(HandleIncomingMessagesAsync)); - Message message = ProcessMessage(); - - // We are intentionally not awaiting _deviceMessageReceivedListener callback. - // This is a user-supplied callback that isn't required to be awaited by us. We can simply invoke it and continue. - _ = _deviceMessageReceivedListener?.Invoke(message); - await TaskHelpers.CompletedTask.ConfigureAwait(false); + Message message = ProcessMessage(true); + await (_deviceMessageReceivedListener?.Invoke(message) ?? TaskHelpers.CompletedTask).ConfigureAwait(false); if (Logging.IsEnabled) Logging.Exit(this, "Process C2D message via callback", nameof(HandleIncomingMessagesAsync)); @@ -587,15 +582,15 @@ public async void OnMessageReceived(Message message) } else if (topic.StartsWith(TwinPatchTopicPrefix, StringComparison.OrdinalIgnoreCase)) { - await HandleIncomingTwinPatchAsync(message).ConfigureAwait(false); + await HandleIncomingTwinPatchAsync(message).ConfigureAwait(true); } else if (topic.StartsWith(MethodPostTopicPrefix, StringComparison.OrdinalIgnoreCase)) { - await HandleIncomingMethodPostAsync(message).ConfigureAwait(false); + await HandleIncomingMethodPostAsync(message).ConfigureAwait(true); } else if (topic.StartsWith(_receiveEventMessagePrefix, StringComparison.OrdinalIgnoreCase)) { - await HandleIncomingEventMessageAsync(message).ConfigureAwait(false); + await HandleIncomingEventMessageAsync(message).ConfigureAwait(true); } else if (topic.StartsWith(_deviceboundMessagePrefix, StringComparison.OrdinalIgnoreCase)) { @@ -650,7 +645,7 @@ private async Task HandleIncomingEventMessageAsync(Message message) } } message.LockToken = _generationId + message.LockToken; - await (_moduleMessageReceivedListener?.Invoke(inputName, message) ?? TaskHelpers.CompletedTask).ConfigureAwait(false); + await (_moduleMessageReceivedListener?.Invoke(inputName, message) ?? TaskHelpers.CompletedTask).ConfigureAwait(true); } finally { @@ -763,8 +758,6 @@ public override async Task EnableReceiveMessageAsync(CancellationToken cancellat public override async Task EnsurePendingMessagesAreDeliveredAsync(CancellationToken cancellationToken) { - cancellationToken.ThrowIfCancellationRequested(); - // If the device connects with a CleanSession flag set to false, we will need to deliver the messages // that were sent before the client had subscribed to the C2D message receive topic. if (_retainMessagesAcrossSessions) @@ -873,7 +866,7 @@ public override async Task SendMethodResponseAsync(MethodResponseInternal method MqttTopicName = MethodResponseTopic.FormatInvariant(methodResponse.Status, methodResponse.RequestId) }; - await SendEventAsync(message, cancellationToken).ConfigureAwait(false); + await SendEventAsync(message, cancellationToken).ConfigureAwait(true); } public override async Task EnableTwinPatchAsync(CancellationToken cancellationToken) @@ -924,7 +917,7 @@ public override async Task SendTwinGetAsync(CancellationToken cancellation // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_017: `SendTwinGetAsync` shall wait for a response from the service with a matching $rid value // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_019: If the response is failed, `SendTwinGetAsync` shall return that failure to the caller. // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_020: If the response doesn't arrive within `MqttTransportHandler.TwinTimeout`, `SendTwinGetAsync` shall fail with a timeout error - using Message response = await SendTwinRequestAsync(request, rid, cancellationToken).ConfigureAwait(false); + using Message response = await SendTwinRequestAsync(request, rid, cancellationToken).ConfigureAwait(true); // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_021: If the response contains a success code, `SendTwinGetAsync` shall return success to the caller // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_018: When a response is received, `SendTwinGetAsync` shall return the Twin object to the caller @@ -969,7 +962,7 @@ public override async Task SendTwinPatchAsync(TwinCollection reportedProperties, // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_028: If the response is failed, `SendTwinPatchAsync` shall return that failure to the caller. // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_029: If the response doesn't arrive within `MqttTransportHandler.TwinTimeout`, `SendTwinPatchAsync` shall fail with a timeout error. // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_030: If the response contains a success code, `SendTwinPatchAsync` shall return success to the caller. - await SendTwinRequestAsync(request, rid, cancellationToken).ConfigureAwait(false); + await SendTwinRequestAsync(request, rid, cancellationToken).ConfigureAwait(true); } private async Task OpenInternalAsync(CancellationToken cancellationToken) @@ -990,7 +983,7 @@ private async Task OpenInternalAsync(CancellationToken cancellationToken) #if NET451 _serverAddresses = Dns.GetHostEntry(_hostName).AddressList; #else - _serverAddresses = await Dns.GetHostAddressesAsync(_hostName).ConfigureAwait(false); + _serverAddresses = await Dns.GetHostAddressesAsync(_hostName).ConfigureAwait(true); #endif } @@ -1035,7 +1028,7 @@ private async Task OpenInternalAsync(CancellationToken cancellationToken) }); } - await _connectCompletion.Task.ConfigureAwait(false); + await _connectCompletion.Task.ConfigureAwait(true); // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_031: `OpenAsync` shall subscribe using the '$iothub/twin/res/#' topic filter await SubscribeTwinResponsesAsync().ConfigureAwait(true); @@ -1085,7 +1078,7 @@ await _channel return; } } - await _subscribeCompletionSource.Task.ConfigureAwait(false); + await _subscribeCompletionSource.Task.ConfigureAwait(true); } private Task SubscribeTwinResponsesAsync() @@ -1152,9 +1145,9 @@ private async Task SendTwinRequestAsync(Message request, string rid, Ca { _twinResponseEvent += onTwinResponse; - await SendEventAsync(request, cancellationToken).ConfigureAwait(false); + await SendEventAsync(request, cancellationToken).ConfigureAwait(true); - await responseReceived.WaitAsync(TwinTimeout, cancellationToken).ConfigureAwait(false); + await responseReceived.WaitAsync(TwinTimeout, cancellationToken).ConfigureAwait(true); if (responseException != null) { @@ -1300,7 +1293,7 @@ private Func> CreateWebSocketChannelFactory(Iot #endif using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - await websocket.ConnectAsync(websocketUri, cts.Token).ConfigureAwait(false); + await websocket.ConnectAsync(websocketUri, cts.Token).ConfigureAwait(true); var clientWebSocketChannel = new ClientWebSocketChannel(null, websocket); clientWebSocketChannel @@ -1314,7 +1307,7 @@ private Func> CreateWebSocketChannelFactory(Iot new LoggingHandler(LogLevel.DEBUG), _mqttIotHubAdapterFactory.Create(this, iotHubConnectionString, settings, productInfo, options)); - await s_eventLoopGroup.Value.RegisterAsync(clientWebSocketChannel).ConfigureAwait(true); + await s_eventLoopGroup.Value.RegisterAsync(clientWebSocketChannel).ConfigureAwait(false); return clientWebSocketChannel; }; diff --git a/iothub/service/src/AmqpServiceClient.cs b/iothub/service/src/AmqpServiceClient.cs index 89f7ddcba6..a793b54ee6 100644 --- a/iothub/service/src/AmqpServiceClient.cs +++ b/iothub/service/src/AmqpServiceClient.cs @@ -162,6 +162,12 @@ public async override Task SendAsync(string deviceId, Message message, TimeSpan? } } + // This call is executed over HTTP. + public override Task PurgeMessageQueueAsync(string deviceId) + { + return PurgeMessageQueueAsync(deviceId, CancellationToken.None); + } + // This call is executed over HTTP. public override Task PurgeMessageQueueAsync(string deviceId, CancellationToken cancellationToken) { @@ -199,6 +205,12 @@ public override FileNotificationReceiver GetFileNotificationRe return _fileNotificationReceiver; } + // This call is executed over HTTP. + public override Task GetServiceStatisticsAsync() + { + return GetServiceStatisticsAsync(CancellationToken.None); + } + // This call is executed over HTTP. public override Task GetServiceStatisticsAsync(CancellationToken cancellationToken) { @@ -224,6 +236,12 @@ public override Task GetServiceStatisticsAsync(CancellationTo } } + // This call is executed over HTTP. + public override Task InvokeDeviceMethodAsync(string deviceId, CloudToDeviceMethod cloudToDeviceMethod) + { + return InvokeDeviceMethodAsync(deviceId, cloudToDeviceMethod, CancellationToken.None); + } + // This call is executed over HTTP. public override Task InvokeDeviceMethodAsync(string deviceId, CloudToDeviceMethod cloudToDeviceMethod, @@ -262,6 +280,12 @@ private Task InvokeDeviceMethodAsync(Uri uri, } } + // This call is executed over HTTP. + public override Task InvokeDeviceMethodAsync(string deviceId, string moduleId, CloudToDeviceMethod cloudToDeviceMethod) + { + return InvokeDeviceMethodAsync(deviceId, moduleId, cloudToDeviceMethod, CancellationToken.None); + } + // This call is executed over HTTP. public override Task InvokeDeviceMethodAsync(string deviceId, string moduleId, CloudToDeviceMethod cloudToDeviceMethod, CancellationToken cancellationToken) { @@ -279,7 +303,7 @@ public override Task InvokeDeviceMethodAsync(string d } // This call is executed over AMQP. - public override async Task SendAsync(string deviceId, string moduleId, Message message, TimeSpan? timeout = null) + public override async Task SendAsync(string deviceId, string moduleId, Message message) { Logging.Enter(this, $"Sending message with Id [{message?.MessageId}] for device {deviceId}, module {moduleId}", nameof(SendAsync)); @@ -308,8 +332,6 @@ public override async Task SendAsync(string deviceId, string moduleId, Message m message.ResetBody(); } - timeout ??= OperationTimeout; - using AmqpMessage amqpMessage = MessageConverter.MessageToAmqpMessage(message); amqpMessage.Properties.To = "/devices/" + WebUtility.UrlEncode(deviceId) + "/modules/" + WebUtility.UrlEncode(moduleId) + "/messages/deviceBound"; try @@ -320,7 +342,7 @@ public override async Task SendAsync(string deviceId, string moduleId, Message m amqpMessage, IotHubConnection.GetNextDeliveryTag(ref _sendingDeliveryTag), AmqpConstants.NullBinary, - timeout.Value) + OperationTimeout) .ConfigureAwait(false); Logging.Info(this, $"Outcome was: {outcome?.DescriptorName}", nameof(SendAsync)); diff --git a/iothub/service/src/ServiceClient.cs b/iothub/service/src/ServiceClient.cs index b88e399938..b018c5f555 100644 --- a/iothub/service/src/ServiceClient.cs +++ b/iothub/service/src/ServiceClient.cs @@ -1,7 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Threading; @@ -52,11 +50,11 @@ internal ServiceClient() } /// - /// Create an instance of ServiceClient from the specified IoT Hub connection string. + /// Create ServiceClient from the specified connection string /// - /// Connection string for the IoT Hub. - /// The that allow configuration of the service client instance during initialization. - /// An instance of ServiceClient. + /// Connection string for the IoT Hub + /// The options that allow configuration of the service client instance during initialization. + /// public static ServiceClient CreateFromConnectionString(string connectionString, ServiceClientOptions options = default) { return CreateFromConnectionString(connectionString, TransportType.Amqp, options); @@ -152,25 +150,25 @@ public void Dispose() protected virtual void Dispose(bool disposing) { } /// - /// Create an instance of ServiceClient from the specified IoT Hub connection string using specified Transport Type. + /// Create ServiceClient from the specified connection string using specified Transport Type /// - /// Connection string for the IoT Hub. - /// The used (Amqp or Amqp_WebSocket_Only). - /// The that allow configuration of the service client instance during initialization. - /// An instance of ServiceClient. + /// Connection string for the IoT Hub + /// Specifies whether Amqp or Amqp_WebSocket_Only transport is used + /// The options that allow configuration of the service client instance during initialization. + /// public static ServiceClient CreateFromConnectionString(string connectionString, TransportType transportType, ServiceClientOptions options = default) { return CreateFromConnectionString(connectionString, transportType, new ServiceClientTransportSettings(), options); } /// - /// Create an instance of ServiceClient from the specified IoT Hub connection string using specified Transport Type and transport settings. + /// Create ServiceClient from the specified connection string using specified Transport Type /// - /// Connection string for the IoT Hub. - /// The used (Amqp or Amqp_WebSocket_Only). - /// Specifies the AMQP and HTTP proxy settings for Service Client. - /// The that allow configuration of the service client instance during initialization. - /// An instance of ServiceClient. + /// Connection string for the IoT Hub + /// Specifies whether Amqp or Amqp_WebSocket_Only transport is used + /// Specifies the AMQP and HTTP proxy settings for Service Client + /// The options that allow configuration of the service client instance during initialization. + /// public static ServiceClient CreateFromConnectionString(string connectionString, TransportType transportType, ServiceClientTransportSettings transportSettings, ServiceClientOptions options = default) { if (transportSettings == null) @@ -185,77 +183,111 @@ public static ServiceClient CreateFromConnectionString(string connectionString, } /// - /// Open the ServiceClient instance. + /// Open the ServiceClient instance /// + /// public abstract Task OpenAsync(); /// - /// Close the ServiceClient instance. + /// Close the ServiceClient instance /// + /// public abstract Task CloseAsync(); /// - /// Send a cloud-to-device message to the specified device. + /// Send a one-way notification to the specified device /// - /// The device identifier for the target device. - /// The cloud-to-device message. - /// The operation timeout, which defaults to 1 minute if unspecified. + /// The device identifier for the target device + /// The message containing the notification + /// The operation timeout override. If not used uses OperationTimeout default + /// public abstract Task SendAsync(string deviceId, Message message, TimeSpan? timeout = null); /// - /// Removes all cloud-to-device messages from a device's queue. + /// Removes all messages from a device's queue. /// - /// The device identifier for the target device. - /// A cancellation token to cancel the operation. - public abstract Task PurgeMessageQueueAsync(string deviceId, CancellationToken cancellationToken = default); + /// + /// + public abstract Task PurgeMessageQueueAsync(string deviceId); /// - /// Get the which can deliver acknowledgments for messages sent to a device/module from IoT Hub. - /// For more information see . + /// Removes all messages from a device's queue. /// - /// An instance of . + /// + /// + /// + public abstract Task PurgeMessageQueueAsync(string deviceId, CancellationToken cancellationToken); + + /// + /// Get the FeedbackReceiver + /// + /// An instance of the FeedbackReceiver public abstract FeedbackReceiver GetFeedbackReceiver(); /// - /// Get the which can deliver notifications for file upload operations. - /// For more information see . + /// Get the FeedbackReceiver /// - /// An instance of . + /// An instance of the FeedbackReceiver public abstract FileNotificationReceiver GetFileNotificationReceiver(); /// - /// Gets service statistics for the IoT Hub. + /// Gets service statistics for the Iot Hub. + /// + /// returns ServiceStatistics object containing current service statistics + public abstract Task GetServiceStatisticsAsync(); + + /// + /// Gets service statistics for the Iot Hub. + /// + /// + /// The token which allows the the operation to be cancelled. + /// + /// returns ServiceStatistics object containing current service statistics + public abstract Task GetServiceStatisticsAsync(CancellationToken cancellationToken); + + /// + /// Interactively invokes a method on device + /// + /// Device Id + /// Device method parameters (passthrough to device) + /// Method result + public abstract Task InvokeDeviceMethodAsync(string deviceId, CloudToDeviceMethod cloudToDeviceMethod); + + /// + /// Interactively invokes a method on device /// - /// A cancellation token to cancel the operation. - /// The service statistics that can be retrieved from IoT Hub, eg. the number of devices connected to the hub. - public abstract Task GetServiceStatisticsAsync(CancellationToken cancellationToken = default); + /// Device Id + /// Device method parameters (passthrough to device) + /// Cancellation Token + /// Method result + public abstract Task InvokeDeviceMethodAsync(string deviceId, CloudToDeviceMethod cloudToDeviceMethod, CancellationToken cancellationToken); /// - /// Interactively invokes a method on a device. + /// Interactively invokes a method on device /// - /// The device identifier for the target device. - /// Parameters to execute a direct method on the device. - /// A cancellation token to cancel the operation. - /// The . - public abstract Task InvokeDeviceMethodAsync(string deviceId, CloudToDeviceMethod cloudToDeviceMethod, CancellationToken cancellationToken = default); + /// Device Id + /// Module Id + /// Device method parameters (passthrough to device) + /// Method result + public abstract Task InvokeDeviceMethodAsync(string deviceId, string moduleId, CloudToDeviceMethod cloudToDeviceMethod); /// - /// Interactively invokes a method on a module. + /// Interactively invokes a method on device /// - /// The device identifier for the target device. - /// The module identifier for the target module. - /// Parameters to execute a direct method on the module. - /// A cancellation token to cancel the operation. - /// The . - public abstract Task InvokeDeviceMethodAsync(string deviceId, string moduleId, CloudToDeviceMethod cloudToDeviceMethod, CancellationToken cancellationToken = default); + /// Device Id + /// Module Id + /// Device method parameters (passthrough to device) + /// Cancellation Token + /// Method result + public abstract Task InvokeDeviceMethodAsync(string deviceId, string moduleId, CloudToDeviceMethod cloudToDeviceMethod, CancellationToken cancellationToken); /// - /// Send a cloud-to-device message to the specified module. + /// Send a one-way notification to the specified device module /// - /// The device identifier for the target device. - /// The module identifier for the target module. - /// The cloud-to-module message. - /// The operation timeout, which defaults to 1 minute if unspecified. - public abstract Task SendAsync(string deviceId, string moduleId, Message message, TimeSpan? timeout = null); + /// The device identifier for the target device + /// The module identifier for the target device module + /// The message containing the notification + /// + public abstract Task SendAsync(string deviceId, string moduleId, Message message); } } diff --git a/iothub/service/tests/ServiceClientTests.cs b/iothub/service/tests/ServiceClientTests.cs index 4ba3b86fd1..4ab46f7ffe 100644 --- a/iothub/service/tests/ServiceClientTests.cs +++ b/iothub/service/tests/ServiceClientTests.cs @@ -19,6 +19,23 @@ namespace Microsoft.Azure.Devices.Api.Test [TestCategory("Unit")] public class ServiceClientTests { + [TestMethod] + public async Task PurgeMessageQueueTest() + { + // Arrange Moq + Tuple, AmqpServiceClient, PurgeMessageQueueResult> setupParameters = this.SetupPurgeMessageQueueTests(); + Mock restOpMock = setupParameters.Item1; + AmqpServiceClient serviceClient = setupParameters.Item2; + PurgeMessageQueueResult expectedResult = setupParameters.Item3; + + // Execute method under test + PurgeMessageQueueResult result = await serviceClient.PurgeMessageQueueAsync("TestDevice").ConfigureAwait(false); + + // Verify expected result + Assert.AreSame(expectedResult, result); + restOpMock.VerifyAll(); + } + [TestMethod] public async Task PurgeMessageQueueWithCancellationTokenTest() { diff --git a/readme.md b/readme.md index b4b0f7ea56..1a89f2e50f 100644 --- a/readme.md +++ b/readme.md @@ -103,17 +103,17 @@ For details on OS support see the following resources: :heavy_check_mark: feature available :heavy_multiplication_x: feature planned but not supported :heavy_minus_sign: no support planned -| Features | Support | Transport protocol used underneath | Client to use | Description | -|---------------------------------------------------------------------------------------------------------------|--------------------- |-------------------------| -------|--------------------------------------------------------------------------------------------------------------------------| -| [Identity registry (CRUD)](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-identity-registry) | :heavy_check_mark: | HTTP | RegistryManager | Use your backend app to perform CRUD operation for individual device or in bulk. || -| [Query](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-query-language) | :heavy_check_mark: | HTTP | RegistryManager | Use your backend app to query for information on device twins, module twins, jobs and message routing. | -| [Import/Export jobs](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-jobs) | :heavy_check_mark: | HTTP | RegistryManager | Use your backend app to import or export device identities in bulk. | -| [Scheduled jobs](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-jobs) | :heavy_check_mark: | HTTP | JobsClient | Use your backend app to schedule jobs to update desired properties, update tags and invoke direct methods. -| [Cloud-to-device messaging](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d) | :heavy_check_mark: | AMQP | ServiceClient | Use your backend app to send cloud-to-device messages in AMQP and AMQP-WS, and set up notifications for cloud-to-device message delivery. | -| [Direct Methods operations](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-direct-methods) | :heavy_check_mark: | HTTP | ServiceClient | Use your backend app to invoke direct method on device. | -| [File Upload Notifications](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-file-upload) | :heavy_check_mark: | AMQP | ServiceClient | Use your backend app to receive file upload notifications. -| [IoT Hub Statistics](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-metrics) | :heavy_check_mark: | HTTP | ServiceClient | Use your backend app to get IoT hub identity registry statistics such as total device count for device statistics, and connected device count for service statistics. -| [Digital Twin Operations](https://docs.microsoft.com/en-us/azure/iot-pnp/overview-iot-plug-and-play) | :heavy_check_mark: | HTTP | DigitalTwinClient or RegistryManager | Use your backend app to perform operations on plug and play devices. The operations include get twins, update twins and invoke commands. DigitalTwinClient is the preferred client to use. +| Features | Support | Transport protocol used underneath | Description | +|---------------------------------------------------------------------------------------------------------------|--------------------- |-------------------------|--------------------------------------------------------------------------------------------------------------------------| +| [Identity registry (CRUD)](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-identity-registry) | :heavy_check_mark: | HTTP | Use your backend app to perform CRUD operation for individual device or in bulk. | +| [Cloud-to-device messaging](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d) | :heavy_check_mark: | AMQP | Use your backend app to send cloud-to-device messages in AMQP and AMQP-WS, and set up cloud-to-device message receivers. | +| [Direct Methods operations](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-direct-methods) | :heavy_check_mark: | HTTP | Use your backend app to invoke direct method on device. | +| [Device Twins operations](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-device-twins) | :heavy_check_mark: | HTTP | Use your backend app to perform twin operations. | +| [Query](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-query-language) | :heavy_check_mark: | HTTP | Use your backend app to perform query for information. | +| [Jobs](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-jobs) | :heavy_check_mark: | HTTP | Use your backend app to perform job operation. | +| [File Upload](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-file-upload) | :heavy_check_mark: | AMQP | Set up your backend app to receive file upload notifications. +| [Digital Twin Client](https://docs.microsoft.com/en-us/azure/iot-pnp/overview-iot-plug-and-play) | :heavy_check_mark: | HTTP | Set up your backend app to perform operations on plug and play devices. | +| [IoT Hub Statistics](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-metrics) | :heavy_check_mark: | HTTP | Get IoT Hub identity registry statistics; such as total device count for device statistics, and connected device count for service statistics. | ### Provisioning Device SDK diff --git a/tools/CaptureLogs/readme.md b/tools/CaptureLogs/readme.md index 794c2f4d24..4b64525af5 100644 --- a/tools/CaptureLogs/readme.md +++ b/tools/CaptureLogs/readme.md @@ -9,7 +9,7 @@ On Linux and OSX LTTNG and perfcollect can be used to collect traces. For more i ## Console logging Logging can be added to console. Note that this method will substantially slow down execution. - 1. Add `e2e\test\Helpers\ConsoleEventListener.cs` to your project. + 1. Add `e2e\test\CommonConsoleEventListener.cs` to your project. 2. Instantiate the listener. Add one or more filters (e.g. `Microsoft-Azure-` or `DotNetty-`): ```C#