diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index 1a850444..315ff80a 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -117,7 +117,7 @@ jobs: - name: '📡 Publish Test Results' # https://github.com/marketplace/actions/publish-test-results uses: 'EnricoMi/publish-unit-test-result-action/macos@v2' - if: always() + if: always() # this means: "run even if prior steps fail" if tests fail, the build will fail, but at least we will know exactly what failed with: files: | TestResults/**/TEST-*.xml diff --git a/Laerdal.McuMgr.Bindings.Android/Laerdal.McuMgr.Bindings.Android.csproj b/Laerdal.McuMgr.Bindings.Android/Laerdal.McuMgr.Bindings.Android.csproj index e8923eb5..1f20100e 100644 --- a/Laerdal.McuMgr.Bindings.Android/Laerdal.McuMgr.Bindings.Android.csproj +++ b/Laerdal.McuMgr.Bindings.Android/Laerdal.McuMgr.Bindings.Android.csproj @@ -62,10 +62,10 @@ true - 1.0.1087.0 - 1.0.1087.0 - 1.0.1087.0 - 1.0.1087.0 + 1.0.1092.0 + 1.0.1092.0 + 1.0.1092.0 + 1.0.1092.0 $(PackageId) $(Authors) diff --git a/Laerdal.McuMgr.Bindings.MacCatalyst/Laerdal.McuMgr.Bindings.MacCatalyst.csproj b/Laerdal.McuMgr.Bindings.MacCatalyst/Laerdal.McuMgr.Bindings.MacCatalyst.csproj index 6b802181..8d523370 100644 --- a/Laerdal.McuMgr.Bindings.MacCatalyst/Laerdal.McuMgr.Bindings.MacCatalyst.csproj +++ b/Laerdal.McuMgr.Bindings.MacCatalyst/Laerdal.McuMgr.Bindings.MacCatalyst.csproj @@ -77,10 +77,10 @@ $(AllowedReferenceRelatedFileExtensions);.pdb - 1.0.1177.0 - 1.0.1177.0 - 1.0.1177.0 - 1.0.1177.0 + 1.0.1092.0 + 1.0.1092.0 + 1.0.1092.0 + 1.0.1092.0 $(PackageId) McuMgr Bindings for MacCatalyst - MAUI ready diff --git a/Laerdal.McuMgr.Bindings.NetStandard/Laerdal.McuMgr.Bindings.NetStandard.csproj b/Laerdal.McuMgr.Bindings.NetStandard/Laerdal.McuMgr.Bindings.NetStandard.csproj index f3e3ca9b..db96c965 100644 --- a/Laerdal.McuMgr.Bindings.NetStandard/Laerdal.McuMgr.Bindings.NetStandard.csproj +++ b/Laerdal.McuMgr.Bindings.NetStandard/Laerdal.McuMgr.Bindings.NetStandard.csproj @@ -37,10 +37,10 @@ $(AllowedReferenceRelatedFileExtensions);.pdb - 1.0.1177.0 - 1.0.1177.0 - 1.0.1177.0 - 1.0.1177.0 + 1.0.1092.0 + 1.0.1092.0 + 1.0.1092.0 + 1.0.1092.0 $(PackageId) McuMgr C# Implementation (WIP) diff --git a/Laerdal.McuMgr.Bindings.iOS/Laerdal.McuMgr.Bindings.iOS.csproj b/Laerdal.McuMgr.Bindings.iOS/Laerdal.McuMgr.Bindings.iOS.csproj index dd0dcdf3..0c749715 100644 --- a/Laerdal.McuMgr.Bindings.iOS/Laerdal.McuMgr.Bindings.iOS.csproj +++ b/Laerdal.McuMgr.Bindings.iOS/Laerdal.McuMgr.Bindings.iOS.csproj @@ -74,10 +74,10 @@ $(AllowedReferenceRelatedFileExtensions);.pdb - 1.0.1177.0 - 1.0.1177.0 - 1.0.1177.0 - 1.0.1177.0 + 1.0.1092.0 + 1.0.1092.0 + 1.0.1092.0 + 1.0.1092.0 $(PackageId) McuMgr Bindings for iOS - MAUI ready diff --git a/Laerdal.McuMgr.Tests/DeviceResetter/DeviceResetterTestbed.ResetAsync.ShouldThrowDeviceResetterErroredOutException_GivenBluetoothErrorDuringReset.cs b/Laerdal.McuMgr.Tests/DeviceResetter/DeviceResetterTestbed.ResetAsync.ShouldThrowDeviceResetterErroredOutException_GivenBluetoothErrorDuringReset.cs index d3bf3efd..5d5dc56b 100644 --- a/Laerdal.McuMgr.Tests/DeviceResetter/DeviceResetterTestbed.ResetAsync.ShouldThrowDeviceResetterErroredOutException_GivenBluetoothErrorDuringReset.cs +++ b/Laerdal.McuMgr.Tests/DeviceResetter/DeviceResetterTestbed.ResetAsync.ShouldThrowDeviceResetterErroredOutException_GivenBluetoothErrorDuringReset.cs @@ -1,4 +1,5 @@ using FluentAssertions; +using FluentAssertions.Extensions; using Laerdal.McuMgr.Common.Enums; using Laerdal.McuMgr.Common.Helpers; using Laerdal.McuMgr.DeviceResetter.Contracts.Enums; @@ -23,8 +24,7 @@ public async Task ResetAsync_ShouldThrowDeviceResetterErroredOutException_GivenB // Assert await work - .Should().ThrowExactlyAsync() - .WithTimeoutInMs(500) + .Should().ThrowWithinAsync(500.Milliseconds()) .WithMessage("*bluetooth error blah blah*"); mockedNativeDeviceResetterProxy.DisconnectCalled.Should().BeFalse(); //00 diff --git a/Laerdal.McuMgr.Tests/DeviceResetter/DeviceResetterTestbed.ResetAsync.ShouldThrowDeviceResetterInternalErrorException_GivenErroneousDueToMissingNativeSymbolsNativeDeviceResetterProxy.cs b/Laerdal.McuMgr.Tests/DeviceResetter/DeviceResetterTestbed.ResetAsync.ShouldThrowDeviceResetterInternalErrorException_GivenErroneousDueToMissingNativeSymbolsNativeDeviceResetterProxy.cs index 41bb8f18..6083ecb7 100644 --- a/Laerdal.McuMgr.Tests/DeviceResetter/DeviceResetterTestbed.ResetAsync.ShouldThrowDeviceResetterInternalErrorException_GivenErroneousDueToMissingNativeSymbolsNativeDeviceResetterProxy.cs +++ b/Laerdal.McuMgr.Tests/DeviceResetter/DeviceResetterTestbed.ResetAsync.ShouldThrowDeviceResetterInternalErrorException_GivenErroneousDueToMissingNativeSymbolsNativeDeviceResetterProxy.cs @@ -1,4 +1,5 @@ using FluentAssertions; +using FluentAssertions.Extensions; using Laerdal.McuMgr.Common.Helpers; using Laerdal.McuMgr.DeviceResetter.Contracts.Enums; using Laerdal.McuMgr.DeviceResetter.Contracts.Events; @@ -21,7 +22,10 @@ public async Task ResetAsync_ShouldThrowDeviceResetterInternalErrorException_Giv var work = new Func(() => deviceResetter.ResetAsync()); // Assert - (await work.Should().ThrowExactlyAsync().WithTimeoutInMs(100)).WithInnerExceptionExactly("native symbols not loaded blah blah"); + (await work + .Should() + .ThrowWithinAsync(500.Milliseconds()) + ).WithInnerExceptionExactly("native symbols not loaded blah blah"); mockedNativeDeviceResetterProxy.DisconnectCalled.Should().BeFalse(); //00 mockedNativeDeviceResetterProxy.BeginResetCalled.Should().BeTrue(); diff --git a/Laerdal.McuMgr.Tests/DeviceResetter/DeviceResetterTestbed.ResetAsync.ShouldThrowTimeoutException_GivenTooSmallTimeout.cs b/Laerdal.McuMgr.Tests/DeviceResetter/DeviceResetterTestbed.ResetAsync.ShouldThrowTimeoutException_GivenTooSmallTimeout.cs index bb1e4bc5..c166a162 100644 --- a/Laerdal.McuMgr.Tests/DeviceResetter/DeviceResetterTestbed.ResetAsync.ShouldThrowTimeoutException_GivenTooSmallTimeout.cs +++ b/Laerdal.McuMgr.Tests/DeviceResetter/DeviceResetterTestbed.ResetAsync.ShouldThrowTimeoutException_GivenTooSmallTimeout.cs @@ -22,7 +22,9 @@ public async Task ResetAsync_ShouldThrowTimeoutException_GivenTooSmallTimeout() var work = new Func(() => deviceResetter.ResetAsync(timeoutInMs: 100)); // Assert - await work.Should().ThrowExactlyAsync().WithTimeoutInMs((int)5.Seconds().TotalMilliseconds); + await work + .Should() + .ThrowWithinAsync(5.Seconds()); mockedNativeDeviceResetterProxy.DisconnectCalled.Should().BeFalse(); //00 mockedNativeDeviceResetterProxy.BeginResetCalled.Should().BeTrue(); diff --git a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.MultipleFilesDownloadAsync.ShouldThrowArgumentException_GivenPathCollectionWithErroneousFilesToDownload.cs b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.MultipleFilesDownloadAsync.ShouldThrowArgumentException_GivenPathCollectionWithErroneousFilesToDownload.cs index a764b37f..c5ed6170 100644 --- a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.MultipleFilesDownloadAsync.ShouldThrowArgumentException_GivenPathCollectionWithErroneousFilesToDownload.cs +++ b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.MultipleFilesDownloadAsync.ShouldThrowArgumentException_GivenPathCollectionWithErroneousFilesToDownload.cs @@ -1,4 +1,5 @@ using FluentAssertions; +using FluentAssertions.Extensions; using Laerdal.McuMgr.Common.Helpers; using Laerdal.McuMgr.FileDownloader.Contracts.Enums; using Laerdal.McuMgr.FileDownloader.Contracts.Native; @@ -37,7 +38,7 @@ public async Task MultipleFilesDownloadAsync_ShouldThrowArgumentException_GivenP )); // Assert - await work.Should().ThrowExactlyAsync().WithTimeoutInMs(500); + await work.Should().ThrowWithinAsync(500.Milliseconds()); eventsMonitor.OccurredEvents.Should().HaveCount(0); diff --git a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.MultipleFilesDownloadAsync.ShouldThrowNullArgumentException_GivenNullForFilesToDownload.cs b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.MultipleFilesDownloadAsync.ShouldThrowNullArgumentException_GivenNullForFilesToDownload.cs index bc9f6686..4538c6d0 100644 --- a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.MultipleFilesDownloadAsync.ShouldThrowNullArgumentException_GivenNullForFilesToDownload.cs +++ b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.MultipleFilesDownloadAsync.ShouldThrowNullArgumentException_GivenNullForFilesToDownload.cs @@ -1,4 +1,5 @@ using FluentAssertions; +using FluentAssertions.Extensions; using Laerdal.McuMgr.Common.Helpers; using Laerdal.McuMgr.FileDownloader.Contracts.Enums; using Laerdal.McuMgr.FileDownloader.Contracts.Native; @@ -28,7 +29,7 @@ public async Task MultipleFilesDownloadAsync_ShouldThrowNullArgumentException_Gi )); // Assert - await work.Should().ThrowExactlyAsync().WithTimeoutInMs(500); + await work.Should().ThrowWithinAsync(500.Milliseconds()); eventsMonitor.OccurredEvents.Should().HaveCount(0); diff --git a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowAllDownloadAttemptsFailedException_GivenFatalErrorMidflight.cs b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowAllDownloadAttemptsFailedException_GivenFatalErrorMidflight.cs index 1139edb6..8354715f 100644 --- a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowAllDownloadAttemptsFailedException_GivenFatalErrorMidflight.cs +++ b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowAllDownloadAttemptsFailedException_GivenFatalErrorMidflight.cs @@ -40,9 +40,8 @@ public async Task SingleFileDownloadAsync_ShouldThrowAllDownloadAttemptsFailedEx // Assert await work.Should() - .ThrowExactlyAsync() - .WithMessage("*failed to download*") - .WithTimeoutInMs((int)(maxTriesCount * 3).Seconds().TotalMilliseconds); + .ThrowWithinAsync((maxTriesCount * 3).Seconds()) + .WithMessage("*failed to download*"); mockedNativeFileDownloaderProxy.CancelCalled.Should().BeFalse(); mockedNativeFileDownloaderProxy.DisconnectCalled.Should().BeFalse(); //00 diff --git a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowAllDownloadAttemptsFailedException_GivenRogueNativeErrorMessage.cs b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowAllDownloadAttemptsFailedException_GivenRogueNativeErrorMessage.cs index 2f33cb71..a95ca49a 100644 --- a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowAllDownloadAttemptsFailedException_GivenRogueNativeErrorMessage.cs +++ b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowAllDownloadAttemptsFailedException_GivenRogueNativeErrorMessage.cs @@ -45,8 +45,7 @@ public async Task SingleFileDownloadAsync_ShouldThrowAllDownloadAttemptsFailedEx // Assert await work.Should() - .ThrowExactlyAsync() - .WithTimeoutInMs((int)3.Seconds().TotalMilliseconds); + .ThrowWithinAsync(3.Seconds()); mockedNativeFileDownloaderProxy.CancelCalled.Should().BeFalse(); mockedNativeFileDownloaderProxy.DisconnectCalled.Should().BeFalse(); //00 diff --git a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowArgumentException_GivenEmptyRemoteFilePath.cs b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowArgumentException_GivenEmptyRemoteFilePath.cs index 5a2ef7a1..bff419ed 100644 --- a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowArgumentException_GivenEmptyRemoteFilePath.cs +++ b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowArgumentException_GivenEmptyRemoteFilePath.cs @@ -1,4 +1,5 @@ using FluentAssertions; +using FluentAssertions.Extensions; using Laerdal.McuMgr.Common.Helpers; using Laerdal.McuMgr.FileDownloader.Contracts.Enums; using Laerdal.McuMgr.FileDownloader.Contracts.Native; @@ -29,7 +30,7 @@ public async Task SingleFileDownloadAsync_ShouldThrowArgumentException_GivenEmpt )); // Assert - await work.Should().ThrowExactlyAsync().WithTimeoutInMs(500); + await work.Should().ThrowWithinAsync(500.Milliseconds()); mockedNativeFileDownloaderProxy.CancelCalled.Should().BeFalse(); mockedNativeFileDownloaderProxy.DisconnectCalled.Should().BeFalse(); //00 diff --git a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowDownloadCancelledException_GivenCancellationRequestMidflight.cs b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowDownloadCancelledException_GivenCancellationRequestMidflight.cs index 216db887..2943a358 100644 --- a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowDownloadCancelledException_GivenCancellationRequestMidflight.cs +++ b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowDownloadCancelledException_GivenCancellationRequestMidflight.cs @@ -41,7 +41,7 @@ public async Task SingleFileUploadAsync_ShouldThrowUploadCancelledException_Give )); // Assert - await work.Should().ThrowExactlyAsync().WithTimeoutInMs((int)5.Seconds().TotalMilliseconds); + await work.Should().ThrowWithinAsync(5.Seconds()); mockedNativeFileDownloaderProxy.CancelCalled.Should().BeTrue(); mockedNativeFileDownloaderProxy.DisconnectCalled.Should().BeFalse(); //00 diff --git a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowDownloadTimeoutException_GivenTooSmallTimeout.cs b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowDownloadTimeoutException_GivenTooSmallTimeout.cs index 1752b3c1..f7a2e0b6 100644 --- a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowDownloadTimeoutException_GivenTooSmallTimeout.cs +++ b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowDownloadTimeoutException_GivenTooSmallTimeout.cs @@ -32,7 +32,7 @@ public async Task SingleFileDownloadAsync_ShouldThrowDownloadTimeoutException_Gi )); // Assert - await work.Should().ThrowExactlyAsync().WithTimeoutInMs((int)5.Seconds().TotalMilliseconds); + await work.Should().ThrowWithinAsync(5.Seconds()); mockedNativeFileDownloaderProxy.CancelCalled.Should().BeFalse(); mockedNativeFileDownloaderProxy.DisconnectCalled.Should().BeFalse(); //00 diff --git a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowRemoteFileNotFoundException_GivenNonExistentFilepath.cs b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowRemoteFileNotFoundException_GivenNonExistentFilepath.cs index 1eb9e6b4..7a15801d 100644 --- a/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowRemoteFileNotFoundException_GivenNonExistentFilepath.cs +++ b/Laerdal.McuMgr.Tests/FileDownloader/FileDownloaderTestbed.SingleFileDownloadAsync.ShouldThrowRemoteFileNotFoundException_GivenNonExistentFilepath.cs @@ -46,8 +46,7 @@ public async Task SingleFileDownloadAsync_ShouldThrowRemoteFileNotFoundException // Assert await work.Should() - .ThrowExactlyAsync() - .WithTimeoutInMs((int)3.Seconds().TotalMilliseconds); + .ThrowWithinAsync(3.Seconds()); mockedNativeFileDownloaderProxy.CancelCalled.Should().BeFalse(); mockedNativeFileDownloaderProxy.DisconnectCalled.Should().BeFalse(); //00 diff --git a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.MultipleFilesUploadAsync.ShouldThrowArgumentException_GivenPathCollectionWithErroneousFilesToDownload.cs b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.MultipleFilesUploadAsync.ShouldThrowArgumentException_GivenPathCollectionWithErroneousFilesToDownload.cs index 4a12f1c3..b74b7a8c 100644 --- a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.MultipleFilesUploadAsync.ShouldThrowArgumentException_GivenPathCollectionWithErroneousFilesToDownload.cs +++ b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.MultipleFilesUploadAsync.ShouldThrowArgumentException_GivenPathCollectionWithErroneousFilesToDownload.cs @@ -1,4 +1,5 @@ using FluentAssertions; +using FluentAssertions.Extensions; using Laerdal.McuMgr.Common.Helpers; using Laerdal.McuMgr.FileUploader.Contracts.Enums; using Laerdal.McuMgr.FileUploader.Contracts.Native; @@ -36,7 +37,7 @@ public async Task MultipleFilesUploadAsync_ShouldThrowArgumentException_GivenPat )); // Assert - await work.Should().ThrowAsync().WithTimeoutInMs(500); //dont use throwexactlyasync<> here + await work.Should().ThrowWithinAsync(500.Milliseconds()); //dont use throwexactlyasync<> here eventsMonitor.OccurredEvents.Should().HaveCount(0); diff --git a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.MultipleFilesUploadAsync.ShouldThrowNullArgumentException_GivenNullForFilesToDownload.cs b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.MultipleFilesUploadAsync.ShouldThrowNullArgumentException_GivenNullForFilesToDownload.cs index 1dcfda94..c1dbcc5a 100644 --- a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.MultipleFilesUploadAsync.ShouldThrowNullArgumentException_GivenNullForFilesToDownload.cs +++ b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.MultipleFilesUploadAsync.ShouldThrowNullArgumentException_GivenNullForFilesToDownload.cs @@ -1,4 +1,5 @@ using FluentAssertions; +using FluentAssertions.Extensions; using Laerdal.McuMgr.Common.Helpers; using Laerdal.McuMgr.FileUploader.Contracts.Enums; using Laerdal.McuMgr.FileUploader.Contracts.Native; @@ -28,7 +29,7 @@ public async Task MultipleFilesUploadAsync_ShouldThrowNullArgumentException_Give )); // Assert - await work.Should().ThrowExactlyAsync().WithTimeoutInMs(500); + await work.Should().ThrowWithinAsync(500.Milliseconds()); eventsMonitor.OccurredEvents.Should().HaveCount(0); diff --git a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowAllUploadAttemptsFailedException_GivenFatalErrorMidflight.cs b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowAllUploadAttemptsFailedException_GivenFatalErrorMidflight.cs index eadf067f..e74725d8 100644 --- a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowAllUploadAttemptsFailedException_GivenFatalErrorMidflight.cs +++ b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowAllUploadAttemptsFailedException_GivenFatalErrorMidflight.cs @@ -40,9 +40,8 @@ public async Task SingleFileUploadAsync_ShouldThrowAllUploadAttemptsFailedExcept // Assert await work.Should() - .ThrowExactlyAsync() - .WithMessage("*failed to upload*") - .WithTimeoutInMs((int)((maxTriesCount + 1) * 3).Seconds().TotalMilliseconds); + .ThrowWithinAsync(((maxTriesCount + 1) * 3).Seconds()) + .WithMessage("*failed to upload*"); mockedNativeFileUploaderProxy.CancelCalled.Should().BeFalse(); mockedNativeFileUploaderProxy.DisconnectCalled.Should().BeFalse(); //00 diff --git a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowAllUploadAttemptsFailedException_GivenRogueNativeErrorMessage.cs b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowAllUploadAttemptsFailedException_GivenRogueNativeErrorMessage.cs index 1938baf5..35edf175 100644 --- a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowAllUploadAttemptsFailedException_GivenRogueNativeErrorMessage.cs +++ b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowAllUploadAttemptsFailedException_GivenRogueNativeErrorMessage.cs @@ -43,9 +43,7 @@ public async Task SingleFileUploadAsync_ShouldThrowAllUploadAttemptsFailedExcept )); // Assert - await work.Should() - .ThrowExactlyAsync() - .WithTimeoutInMs((int)3.Seconds().TotalMilliseconds); + await work.Should().ThrowWithinAsync(3.Seconds()); mockedNativeFileUploaderProxy.CancelCalled.Should().BeFalse(); mockedNativeFileUploaderProxy.DisconnectCalled.Should().BeFalse(); //00 diff --git a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowArgumentException_GivenEmptyRemoteFilePath.cs b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowArgumentException_GivenEmptyRemoteFilePath.cs index 2111487b..b23051bc 100644 --- a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowArgumentException_GivenEmptyRemoteFilePath.cs +++ b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowArgumentException_GivenEmptyRemoteFilePath.cs @@ -1,4 +1,5 @@ using FluentAssertions; +using FluentAssertions.Extensions; using Laerdal.McuMgr.Common.Helpers; using Laerdal.McuMgr.FileUploader.Contracts.Enums; using Laerdal.McuMgr.FileUploader.Contracts.Native; @@ -30,7 +31,7 @@ public async Task SingleFileUploadAsync_ShouldThrowArgumentException_GivenEmptyR )); // Assert - await work.Should().ThrowExactlyAsync().WithTimeoutInMs(500); + await work.Should().ThrowWithinAsync(500.Milliseconds()); mockedNativeFileUploaderProxy.CancelCalled.Should().BeFalse(); mockedNativeFileUploaderProxy.DisconnectCalled.Should().BeFalse(); //00 diff --git a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowRemoteFolderNotFoundException_GivenNonExistentFilepath.cs b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowRemoteFolderNotFoundException_GivenNonExistentFilepath.cs index 40dd16f0..a9dc25a7 100644 --- a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowRemoteFolderNotFoundException_GivenNonExistentFilepath.cs +++ b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowRemoteFolderNotFoundException_GivenNonExistentFilepath.cs @@ -52,8 +52,7 @@ int maxTriesCount // Assert await work.Should() - .ThrowExactlyAsync() - .WithTimeoutInMs((int)3.Seconds().TotalMilliseconds); + .ThrowWithinAsync(3.Seconds()); mockedNativeFileUploaderProxy.CancelCalled.Should().BeFalse(); mockedNativeFileUploaderProxy.DisconnectCalled.Should().BeFalse(); //00 diff --git a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowUploadCancelledException_GivenCancellationRequestMidflight.cs b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowUploadCancelledException_GivenCancellationRequestMidflight.cs index a05b76f0..c435d320 100644 --- a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowUploadCancelledException_GivenCancellationRequestMidflight.cs +++ b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowUploadCancelledException_GivenCancellationRequestMidflight.cs @@ -43,7 +43,7 @@ public async Task SingleFileUploadAsync_ShouldThrowUploadCancelledException_Give )); // Assert - await work.Should().ThrowExactlyAsync().WithTimeoutInMs((int)5.Seconds().TotalMilliseconds); + await work.Should().ThrowWithinAsync(5.Seconds()); mockedNativeFileUploaderProxy.CancelCalled.Should().BeTrue(); mockedNativeFileUploaderProxy.CancellationReason.Should().Be(cancellationReason); diff --git a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowUploadTimeoutException_GivenTooSmallTimeout.cs b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowUploadTimeoutException_GivenTooSmallTimeout.cs index 4e0e1ca4..cbf50b8e 100644 --- a/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowUploadTimeoutException_GivenTooSmallTimeout.cs +++ b/Laerdal.McuMgr.Tests/FileUploader/FileUploaderTestbed.SingleFileUploadAsync.ShouldThrowUploadTimeoutException_GivenTooSmallTimeout.cs @@ -33,7 +33,7 @@ public async Task SingleFileUploadAsync_ShouldThrowUploadTimeoutException_GivenT )); // Assert - await work.Should().ThrowExactlyAsync().WithTimeoutInMs((int)5.Seconds().TotalMilliseconds); + await work.Should().ThrowWithinAsync(5.Seconds()); mockedNativeFileUploaderProxy.CancelCalled.Should().BeFalse(); mockedNativeFileUploaderProxy.DisconnectCalled.Should().BeFalse(); //00 diff --git a/Laerdal.McuMgr.Tests/FirmwareEraser/FirmwareEraserTestbed.EraseAsync.ShouldThrowTimeoutException_GivenTooSmallTimeout.cs b/Laerdal.McuMgr.Tests/FirmwareEraser/FirmwareEraserTestbed.EraseAsync.ShouldThrowTimeoutException_GivenTooSmallTimeout.cs index 67277ff5..c2aaa792 100644 --- a/Laerdal.McuMgr.Tests/FirmwareEraser/FirmwareEraserTestbed.EraseAsync.ShouldThrowTimeoutException_GivenTooSmallTimeout.cs +++ b/Laerdal.McuMgr.Tests/FirmwareEraser/FirmwareEraserTestbed.EraseAsync.ShouldThrowTimeoutException_GivenTooSmallTimeout.cs @@ -22,7 +22,7 @@ public async Task EraseAsync_ShouldThrowTimeoutException_GivenTooSmallTimeout() var work = new Func(() => firmwareEraser.EraseAsync(imageIndex: 2, timeoutInMs: 100)); // Assert - await work.Should().ThrowExactlyAsync().WithTimeoutInMs((int)5.Seconds().TotalMilliseconds); + await work.Should().ThrowWithinAsync(5.Seconds()); mockedNativeFirmwareEraserProxy.DisconnectCalled.Should().BeFalse(); //00 mockedNativeFirmwareEraserProxy.BeginErasureCalled.Should().BeTrue(); diff --git a/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowAllFirmwareInstallationAttemptsFailedException_GivenFirmwareUploadFatalErrorMidflight.cs b/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowAllFirmwareInstallationAttemptsFailedException_GivenFirmwareUploadFatalErrorMidflight.cs index 15292531..3f6a030d 100644 --- a/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowAllFirmwareInstallationAttemptsFailedException_GivenFirmwareUploadFatalErrorMidflight.cs +++ b/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowAllFirmwareInstallationAttemptsFailedException_GivenFirmwareUploadFatalErrorMidflight.cs @@ -1,4 +1,5 @@ using FluentAssertions; +using FluentAssertions.Extensions; using Laerdal.McuMgr.Common.Enums; using Laerdal.McuMgr.Common.Helpers; using Laerdal.McuMgr.FirmwareInstaller.Contracts.Enums; @@ -32,9 +33,7 @@ public async Task InstallAsync_ShouldThrowAllFirmwareInstallationAttemptsFailedE // Assert ( - await work.Should() - .ThrowExactlyAsync() - .WithTimeoutInMs(3_000) + await work.Should().ThrowWithinAsync(3_000.Milliseconds()) ).WithInnerException(); mockedNativeFirmwareInstallerProxy.CancelCalled.Should().BeFalse(); diff --git a/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowAllFirmwareInstallationAttemptsFailedException_GivenGenericFatalErrorMidflight.cs b/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowAllFirmwareInstallationAttemptsFailedException_GivenGenericFatalErrorMidflight.cs index 1ddceb23..15c815ea 100644 --- a/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowAllFirmwareInstallationAttemptsFailedException_GivenGenericFatalErrorMidflight.cs +++ b/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowAllFirmwareInstallationAttemptsFailedException_GivenGenericFatalErrorMidflight.cs @@ -1,4 +1,5 @@ using FluentAssertions; +using FluentAssertions.Extensions; using Laerdal.McuMgr.Common.Enums; using Laerdal.McuMgr.Common.Helpers; using Laerdal.McuMgr.FirmwareInstaller.Contracts.Enums; @@ -30,9 +31,9 @@ public async Task InstallAsync_ShouldThrowAllFirmwareInstallationAttemptsFailedE // Assert await work.Should() - .ThrowExactlyAsync() // todo AllFirmwareInstallationAttemptsFailedException - .WithMessage("*fatal error occurred*") - .WithTimeoutInMs(3_000); + .ThrowWithinAsync(3_000.Milliseconds()) + .WithInnerException(typeof(FirmwareInstallationUploadingStageErroredOutException)) + .WithMessage("*fatal error occurred 123*"); mockedNativeFirmwareInstallerProxy.CancelCalled.Should().BeFalse(); mockedNativeFirmwareInstallerProxy.DisconnectCalled.Should().BeFalse(); //00 @@ -43,7 +44,7 @@ await work.Should() eventsMonitor .Should().Raise(nameof(firmwareInstaller.FatalErrorOccurred)) .WithSender(firmwareInstaller) - .WithArgs(args => args.ErrorMessage == "fatal error occurred"); + .WithArgs(args => args.ErrorMessage == "fatal error occurred 123"); eventsMonitor .Should().Raise(nameof(firmwareInstaller.StateChanged)) @@ -103,7 +104,7 @@ public override EFirmwareInstallationVerdict BeginInstallation( await Task.Delay(100); StateChangedAdvertisement(EFirmwareInstallationState.Uploading, EFirmwareInstallationState.Error); // order - FatalErrorOccurredAdvertisement(EFirmwareInstallationState.Confirming, EFirmwareInstallerFatalErrorType.Generic, "fatal error occurred", EGlobalErrorCode.Generic); // order + FatalErrorOccurredAdvertisement(EFirmwareInstallationState.Uploading, EFirmwareInstallerFatalErrorType.Generic, "fatal error occurred 123", EGlobalErrorCode.Generic); // order }); return verdict; diff --git a/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowFirmwareInstallationImageSwapTimeoutException_GivenFatalErrorMidflight.cs b/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowFirmwareInstallationImageSwapTimeoutException_GivenFatalErrorMidflight.cs index c74ed713..62cb4c6f 100644 --- a/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowFirmwareInstallationImageSwapTimeoutException_GivenFatalErrorMidflight.cs +++ b/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowFirmwareInstallationImageSwapTimeoutException_GivenFatalErrorMidflight.cs @@ -1,4 +1,5 @@ using FluentAssertions; +using FluentAssertions.Extensions; using Laerdal.McuMgr.Common.Enums; using Laerdal.McuMgr.Common.Helpers; using Laerdal.McuMgr.FirmwareInstaller.Contracts.Enums; @@ -29,9 +30,7 @@ public async Task InstallAsync_ShouldThrowFirmwareInstallationImageSwapTimeoutEx )); // Assert - await work.Should() - .ThrowExactlyAsync() - .WithTimeoutInMs(3_000); + await work.Should().ThrowWithinAsync(3_000.Milliseconds()); mockedNativeFirmwareInstallerProxy.CancelCalled.Should().BeFalse(); mockedNativeFirmwareInstallerProxy.DisconnectCalled.Should().BeFalse(); //00 diff --git a/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowFirmwareInstallationInternalErrorException_GivenErroneousNativeFirmwareInstaller.cs b/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowFirmwareInstallationInternalErrorException_GivenErroneousNativeFirmwareInstaller.cs index aad3e4d2..a87daa92 100644 --- a/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowFirmwareInstallationInternalErrorException_GivenErroneousNativeFirmwareInstaller.cs +++ b/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowFirmwareInstallationInternalErrorException_GivenErroneousNativeFirmwareInstaller.cs @@ -1,4 +1,5 @@ using FluentAssertions; +using FluentAssertions.Extensions; using Laerdal.McuMgr.Common.Helpers; using Laerdal.McuMgr.FirmwareInstaller.Contracts.Enums; using Laerdal.McuMgr.FirmwareInstaller.Contracts.Exceptions; @@ -26,9 +27,7 @@ public async Task InstallAsync_ShouldThrowFirmwareInstallationInternalErrorExcep // Assert ( - await work.Should() - .ThrowExactlyAsync() - .WithTimeoutInMs(1_000) + await work.Should().ThrowWithinAsync(1_000.Milliseconds()) ).WithInnerExceptionExactly("native symbols not loaded blah blah"); mockedNativeFirmwareInstallerProxy.CancelCalled.Should().BeFalse(); diff --git a/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowFirmwareInstallationTimeoutException_GivenTooSmallTimeout.cs b/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowFirmwareInstallationTimeoutException_GivenTooSmallTimeout.cs index f2b3895f..d0aaed7d 100644 --- a/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowFirmwareInstallationTimeoutException_GivenTooSmallTimeout.cs +++ b/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowFirmwareInstallationTimeoutException_GivenTooSmallTimeout.cs @@ -31,9 +31,7 @@ public async Task InstallAsync_ShouldThrowFirmwareInstallationTimeoutException_G )); // Assert - await work.Should() - .ThrowExactlyAsync() - .WithTimeoutInMs((int)2.Seconds().TotalMilliseconds); + await work.Should().ThrowWithinAsync(2.Seconds()); mockedNativeFirmwareInstallerProxy.CancelCalled.Should().BeFalse(); mockedNativeFirmwareInstallerProxy.DisconnectCalled.Should().BeFalse(); //00 diff --git a/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowInstallationCancelledException_GivenCancellationRequestMidflight.cs b/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowInstallationCancelledException_GivenCancellationRequestMidflight.cs index f5b81077..b499723e 100644 --- a/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowInstallationCancelledException_GivenCancellationRequestMidflight.cs +++ b/Laerdal.McuMgr.Tests/FirmwareInstaller/FirmwareInstallerTestbed.InstallAsync.ShouldThrowInstallationCancelledException_GivenCancellationRequestMidflight.cs @@ -40,9 +40,7 @@ public async Task InstallAsync_ShouldThrowFirmwareInstallationCancelledException )); // Assert - await work.Should() - .ThrowExactlyAsync() - .WithTimeoutInMs((int)5.Seconds().TotalMilliseconds); + await work.Should().ThrowWithinAsync(5.Seconds()); mockedNativeFirmwareInstallerProxy.CancelCalled.Should().BeTrue(); mockedNativeFirmwareInstallerProxy.DisconnectCalled.Should().BeFalse(); //00 diff --git a/Laerdal.McuMgr.slnx.DotSettings.user b/Laerdal.McuMgr.slnx.DotSettings.user index e71543da..8e5f73b5 100644 --- a/Laerdal.McuMgr.slnx.DotSettings.user +++ b/Laerdal.McuMgr.slnx.DotSettings.user @@ -1,8 +1,10 @@  ForceIncluded + ForceIncluded + ForceIncluded - <SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> - <Solution /> + <SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <Solution /> </SessionState> diff --git a/Laerdal.McuMgr/Laerdal.McuMgr.csproj b/Laerdal.McuMgr/Laerdal.McuMgr.csproj index 66691306..ed535d2b 100644 --- a/Laerdal.McuMgr/Laerdal.McuMgr.csproj +++ b/Laerdal.McuMgr/Laerdal.McuMgr.csproj @@ -11,16 +11,16 @@ - $(TargetFrameworks)netstandard2.1; + $(TargetFrameworks)net8.0; $(TargetFrameworks)net8.0-android; $(TargetFrameworks)net8.0-ios11; $(TargetFrameworks)net8.0-maccatalyst - true - true - true - true - true + true + true + true + true + true true true @@ -71,10 +71,10 @@ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb - 1.0.1177.0 - 1.0.1177.0 - 1.0.1177.0 - 1.0.1177.0 + 1.0.1092.0 + 1.0.1092.0 + 1.0.1092.0 + 1.0.1092.0 $(PackageId) $(Authors) @@ -159,7 +159,7 @@ - + @@ -167,21 +167,21 @@ - + - + - + - - + + diff --git a/Laerdal.McuMgr/Shared/Common/Helpers/TaskCompletionSourceExtensions.cs b/Laerdal.McuMgr/Shared/Common/Helpers/TaskCompletionSourceExtensions.cs new file mode 100644 index 00000000..d2457270 --- /dev/null +++ b/Laerdal.McuMgr/Shared/Common/Helpers/TaskCompletionSourceExtensions.cs @@ -0,0 +1,502 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Laerdal.McuMgr.Common.Helpers +{ + /// + /// A family of extension methods that ensure that the is properly cleaned up in the case of a timeout either + /// from the cancellation token or the timeout specified.

+ /// + /// Always bear in mind that if the is never completed, its task will never complete. Even though the underlying + /// Task is not actually in a "scheduler" (since TCS tasks are Promise Tasks) + /// never completing tasks, of any type, is generally considered a bug. + ///
+ public static class TaskCompletionSourceExtensions + { + /// + /// Sets up a timeout-monitor on the given task. This is essentially a wrapper around + /// with two major differences:

+ /// - If the timeout is zero or negative then it gets interpreted as "wait forever" and the method will just return the task itself.

+ /// - Most importantly, in the case of a TimeoutException this method makes sure to properly cleanup + /// (cancel) the task itself so that you won't have to (it's so easy to forget this and it's a common source of memory-leaks and zombie-promise-tasks + /// that can cripple the system!). + ///
+ /// The task to monitor with a timeout. + /// The timeout in milliseconds. If zero or negative it gets interpreted as "wait forever" and the method will + /// just return the task itself. + /// (optional) The cancellation token that co-monitors the waiting mechanism. + /// Thrown when the task didn't complete within the specified timeout. + /// The hybridized task that you can await on if you have provided a positive timeout. The task itself otherwise. + /// + /// // per https://devblogs.microsoft.com/premier-developer/the-danger-of-taskcompletionsourcet-class/ + /// var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + /// + /// try + /// { + /// PropertyChanged += MyEventHandler_; + /// await tcs.WaitTaskWithOptionalTimeoutAsync(timeout); + /// } + /// finally + /// { + /// PropertyChanged -= MyEventHandler_; //this is needed in the case of a timeout + /// } + /// + /// return; + /// + /// void MyEventHandler_(object? _, SomeEventArgs ea_) + /// { + /// try + /// { + /// // ... logic here ... + /// + /// PropertyChanged -= BindableObject_PropertyChanged_; //best to unwire the listener as soon as we can + /// tcs.TrySetResult(); + /// } + /// catch (Exception ex) + /// { + /// PropertyChanged -= BindableObject_PropertyChanged_; //best to unwire the listener as soon as we can + /// tcs.TrySetException(ex); //vital we need to ensure that the task gets completed one way or another + /// } + /// } + /// + public static Task WaitTaskWithOptionalTimeoutAsync(this TaskCompletionSource tcs, int timeoutInMilliseconds, CancellationToken cancellationToken = default) + { + return timeoutInMilliseconds <= 0 + ? tcs.Task + : tcs.WaitTaskWithTimeoutAsync(TimeSpan.FromMilliseconds(timeoutInMilliseconds), cancellationToken); + } + + /// + /// Sets up a timeout-monitor on the given task. This is essentially a wrapper around + /// with two major differences:

+ /// - If the timeout is zero or negative then it gets interpreted as "wait forever" and the method will just return the task itself.

+ /// - Most importantly, in the case of a TimeoutException this method makes sure to properly cleanup + /// (cancel) the task itself so that you won't have to (it's so easy to forget this and it's a common source of memory-leaks and zombie-promise-tasks + /// that can cripple the system!) + ///
+ /// The task to monitor with a timeout. + /// The amount of time to wait for before throwing a . If zero or negative it gets interpreted + /// as "wait forever" and the method will just return the task itself. + /// (optional) The cancellation token that co-monitors the waiting mechanism. + /// Thrown when the task didn't complete within the specified timeout. + /// The hybridized task that you can await on if you have provided a positive timeout. The task itself otherwise. + /// + /// // per https://devblogs.microsoft.com/premier-developer/the-danger-of-taskcompletionsourcet-class/ + /// var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + /// + /// try + /// { + /// PropertyChanged += MyEventHandler_; + /// await tcs.WaitTaskWithOptionalTimeoutAsync(timeout); + /// } + /// finally + /// { + /// PropertyChanged -= MyEventHandler_; //this is needed in the case of a timeout + /// } + /// + /// return; + /// + /// void MyEventHandler_(object? _, SomeEventArgs ea_) + /// { + /// try + /// { + /// // ... logic here ... + /// + /// PropertyChanged -= BindableObject_PropertyChanged_; //best to unwire the listener as soon as we can + /// tcs.TrySetResult(); + /// } + /// catch (Exception ex) + /// { + /// PropertyChanged -= BindableObject_PropertyChanged_; //best to unwire the listener as soon as we can + /// tcs.TrySetException(ex); //vital we need to ensure that the task gets completed one way or another + /// } + /// } + /// + public static Task WaitTaskWithOptionalTimeoutAsync(this TaskCompletionSource tcs, TimeSpan timespan, CancellationToken cancellationToken = default) + { + return timespan <= TimeSpan.Zero + ? tcs.Task + : tcs.WaitTaskWithTimeoutAsync(timespan, cancellationToken); + } + + /// + /// Sets up a timeout-monitor on the given task. This is essentially a wrapper around + /// with two major differences:

+ /// - If the timeout is negative you will get an thrown (WaitAsync() allows -1 as a special-case for "wait forever" + /// but this method doesn't allow that special case)

+ /// - Most importantly, in the case of a TimeoutException this method makes sure to properly cleanup + /// (cancel) the task itself so that you won't have to (it's so easy to forget this and it's a common source of memory-leaks and zombie-promise-tasks + /// that can cripple the system!). + ///
+ /// The task to monitor with a timeout. + /// The timeout in milliseconds. If zero or negative it gets interpreted as "wait forever" and the method will + /// just return the task itself. + /// (optional) The cancellation token that co-monitors the waiting mechanism. + /// Thrown when the task didn't complete within the specified timeout. + /// The hybridized task that you can await on. + /// + /// // per https://devblogs.microsoft.com/premier-developer/the-danger-of-taskcompletionsourcet-class/ + /// var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + /// + /// try + /// { + /// PropertyChanged += MyEventHandler_; + /// await tcs.WaitTaskWithOptionalTimeoutAsync(timeout); + /// } + /// finally + /// { + /// PropertyChanged -= MyEventHandler_; //this is needed in the case of a timeout + /// } + /// + /// return; + /// + /// void MyEventHandler_(object? _, SomeEventArgs ea_) + /// { + /// try + /// { + /// // ... logic here ... + /// + /// PropertyChanged -= BindableObject_PropertyChanged_; //best to unwire the listener as soon as we can + /// tcs.TrySetResult(); + /// } + /// catch (Exception ex) + /// { + /// PropertyChanged -= BindableObject_PropertyChanged_; //best to unwire the listener as soon as we can + /// tcs.TrySetException(ex); //vital we need to ensure that the task gets completed one way or another + /// } + /// } + /// + public static Task WaitTaskWithTimeoutAsync(this TaskCompletionSource tcs, int timeoutInMilliseconds, CancellationToken cancellationToken = default) + { + return tcs.WaitTaskWithTimeoutAsync(TimeSpan.FromMilliseconds(timeoutInMilliseconds), cancellationToken); + } + + /// + /// Sets up a timeout-monitor on the given task. This is essentially a wrapper around + /// with two major differences:

+ /// - If the timeout is negative you will get an thrown (WaitAsync() allows -1 as a special-case for "wait forever" + /// but this method doesn't allow that special case)

+ /// - Most importantly, in the case of a TimeoutException this method makes sure to properly cleanup + /// (cancel) the task itself so that you won't have to (it's so easy to forget this and it's a common source of memory-leaks and zombie-promise-tasks + /// that can cripple the system!). + ///
+ /// The task to monitor with a timeout. + /// The amount of time to wait for before throwing a . If zero or negative it gets interpreted + /// as "wait forever" and the method will just return the task itself. + /// (optional) The cancellation token that co-monitors the waiting mechanism. + /// Thrown when the task didn't complete within the specified timeout. + /// The hybridized task that you can await on. + /// + /// // per https://devblogs.microsoft.com/premier-developer/the-danger-of-taskcompletionsourcet-class/ + /// var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + /// + /// try + /// { + /// PropertyChanged += MyEventHandler_; + /// await tcs.WaitTaskWithTimeoutAsync(timeout); + /// } + /// finally + /// { + /// PropertyChanged -= MyEventHandler_; //this is needed in the case of a timeout + /// } + /// + /// return; + /// + /// void MyEventHandler_(object? _, SomeEventArgs ea_) + /// { + /// try + /// { + /// // ... logic here ... + /// + /// PropertyChanged -= BindableObject_PropertyChanged_; //best to unwire the listener as soon as we can + /// tcs.TrySetResult(); + /// } + /// catch (Exception ex) + /// { + /// PropertyChanged -= BindableObject_PropertyChanged_; //best to unwire the listener as soon as we can + /// tcs.TrySetException(ex); //vital we need to ensure that the task gets completed one way or another + /// } + /// } + /// + public async static Task WaitTaskWithTimeoutAsync(this TaskCompletionSource tcs, TimeSpan timespan, CancellationToken cancellationToken = default) + { + if (timespan < TimeSpan.Zero) //note that this deviates from the behaviour of .WaitAsync() which does accept -1 milliseconds which means "wait forever" + { + var exception = new ArgumentException("Timeout must be zero or positive", nameof(timespan)); + tcs.TrySetException(exception); //vital we need to ensure that the task gets completed one way or another + + throw exception; + } + + try + { + await tcs.Task.WaitAsync(timespan, cancellationToken); + } + catch (Exception ex) when (ex is TimeoutException or TaskCanceledException) //taskcanceledexception can come from cancellation-token timeouts + { + var isCancellationSuccessful = tcs.TrySetCanceled(cancellationToken); //00 vital + if (isCancellationSuccessful) + throw; + + if (tcs.Task.IsCompletedSuccessfully) //10 barely completed in time + return; //micro-optimization to avoid the overhead of await + + await tcs.Task; + } + + //00 it is absolutely vital to trash the tcs and ensure it will not pester the system as a zombie promise-task + // waitasync() does not take care of this aspect because quite simply it cannot even if it tried + // all waitasync() does is to simply unwire the continuation from task.Task and leave it be + // + //10 if the cancellation fails it means one of the following two things: + // + // 1. that the task barely managed to complete in time on its own at the nick of time we prefer to give + // it the benefit of the doubt and let it complete normally + // + // 2. that the task itself threw a timeout-exception and in this case we prefer to honor the exception that + // the task itself threw and let it be propagated to the caller + } + + /// + /// Sets up a timeout-monitor on the given task. This is essentially a wrapper around + /// with two major differences:

+ /// - If the timeout is zero or negative then it gets interpreted as "wait forever" and the method will just return the task itself.

+ /// - Most importantly, in the case of a TimeoutException this method makes sure to properly cleanup + /// (cancel) the task itself so that you won't have to (it's so easy to forget this and it's a common source of memory-leaks and zombie-promise-tasks + /// that can cripple the system!). + ///
+ /// The task to monitor with a timeout. + /// The timeout in milliseconds. If zero or negative it gets interpreted as "wait forever" and the method + /// will just return the task itself. + /// (optional) The cancellation token that co-monitors the waiting mechanism. + /// Thrown when the task didn't complete within the specified timeout. + /// The hybridized task that you can await on if you have provided a positive timeout. The task itself otherwise. + /// + /// // per https://devblogs.microsoft.com/premier-developer/the-danger-of-taskcompletionsourcet-class/ + /// var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + /// + /// try + /// { + /// PropertyChanged += MyEventHandler_; + /// await tcs.WaitTaskWithTimeoutAsync<int>(timeout); + /// } + /// finally + /// { + /// PropertyChanged -= MyEventHandler_; //this is needed in the case of a timeout + /// } + /// + /// return; + /// + /// void MyEventHandler_(object? _, SomeEventArgs ea_) + /// { + /// try + /// { + /// // ... logic here ... + /// + /// PropertyChanged -= BindableObject_PropertyChanged_; //best to unwire the listener as soon as we can + /// tcs.TrySetResult(123); + /// } + /// catch (Exception ex) + /// { + /// PropertyChanged -= BindableObject_PropertyChanged_; //best to unwire the listener as soon as we can + /// tcs.TrySetException(ex); //vital we need to ensure that the task gets completed one way or another + /// } + /// } + /// + public static Task WaitTaskWithOptionalTimeoutAsync(this TaskCompletionSource tcs, int timeoutInMilliseconds, CancellationToken cancellationToken = default) + { + return timeoutInMilliseconds <= 0 + ? tcs.Task + : tcs.WaitTaskWithTimeoutAsync(TimeSpan.FromMilliseconds(timeoutInMilliseconds), cancellationToken); + } + + /// + /// Sets up a timeout-monitor on the given task. This is essentially a wrapper around + /// with two major differences:

+ /// - If the timeout is zero or negative then it gets interpreted as "wait forever" and the method will just return the task itself.

+ /// - Most importantly, in the case of a TimeoutException this method makes sure to properly cleanup + /// (cancel) the task itself so that you won't have to (it's so easy to forget this and it's a common source of memory-leaks and zombie-promise-tasks + /// that can cripple the system!). + ///
+ /// The task to monitor with a timeout. + /// The amount of time to wait for before throwing a . If zero or negative it gets interpreted + /// as "wait forever" and the method will just return the task itself. + /// (optional) The cancellation token that co-monitors the waiting mechanism. + /// Thrown when the task didn't complete within the specified timeout. + /// The hybridized task that you can await on if you have provided a positive timeout. The task itself otherwise. + /// + /// // per https://devblogs.microsoft.com/premier-developer/the-danger-of-taskcompletionsourcet-class/ + /// var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + /// + /// try + /// { + /// PropertyChanged += MyEventHandler_; + /// await tcs.WaitTaskWithOptionalTimeoutAsync<int>(timeout); + /// } + /// finally + /// { + /// PropertyChanged -= MyEventHandler_; //this is needed in the case of a timeout + /// } + /// + /// return; + /// + /// void MyEventHandler_(object? _, SomeEventArgs ea_) + /// { + /// try + /// { + /// // ... logic here ... + /// + /// PropertyChanged -= BindableObject_PropertyChanged_; //best to unwire the listener as soon as we can + /// tcs.TrySetResult(123); + /// } + /// catch (Exception ex) + /// { + /// PropertyChanged -= BindableObject_PropertyChanged_; //best to unwire the listener as soon as we can + /// tcs.TrySetException(ex); //vital we need to ensure that the task gets completed one way or another + /// } + /// } + /// + public static Task WaitTaskWithOptionalTimeoutAsync(this TaskCompletionSource tcs, TimeSpan timespan, CancellationToken cancellationToken = default) + { + return timespan <= TimeSpan.Zero + ? tcs.Task + : tcs.WaitTaskWithTimeoutAsync(timespan, cancellationToken); + } + + /// + /// Sets up a timeout-monitor on the given task. This is essentially a wrapper around + /// with two major differences:

+ /// - If the timeout is negative you will get an thrown (WaitAsync() allows -1 as a special-case for "wait forever" + /// but this method doesn't allow that special case)

+ /// - Most importantly, in the case of a TimeoutException this method makes sure to properly cleanup + /// (cancel) the task itself so that you won't have to (it's so easy to forget this and it's a common source of memory-leaks and zombie-promise-tasks + /// that can cripple the system!). + ///
+ /// The task to monitor with a timeout. + /// The timeout in milliseconds. If zero or negative it gets interpreted as "wait forever" and the method will + /// just return the task itself. + /// (optional) The cancellation token that co-monitors the waiting mechanism. + /// Thrown when the task didn't complete within the specified timeout. + /// + /// // per https://devblogs.microsoft.com/premier-developer/the-danger-of-taskcompletionsourcet-class/ + /// var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + /// + /// try + /// { + /// PropertyChanged += MyEventHandler_; + /// await tcs.WaitTaskWithTimeoutAsync<int>(timeout); + /// } + /// finally + /// { + /// PropertyChanged -= MyEventHandler_; //this is needed in the case of a timeout + /// } + /// + /// return; + /// + /// void MyEventHandler_(object? _, SomeEventArgs ea_) + /// { + /// try + /// { + /// // ... logic here ... + /// + /// PropertyChanged -= BindableObject_PropertyChanged_; //best to unwire the listener as soon as we can + /// tcs.TrySetResult(123); + /// } + /// catch (Exception ex) + /// { + /// PropertyChanged -= BindableObject_PropertyChanged_; //best to unwire the listener as soon as we can + /// tcs.TrySetException(ex); //vital we need to ensure that the task gets completed one way or another + /// } + /// } + /// + public static Task WaitTaskWithTimeoutAsync(this TaskCompletionSource tcs, int timeoutInMilliseconds, CancellationToken cancellationToken = default) + { + return tcs.WaitTaskWithTimeoutAsync(TimeSpan.FromMilliseconds(timeoutInMilliseconds), cancellationToken); + } + + /// + /// Sets up a timeout-monitor on the given task. This is essentially a wrapper around + /// with two major differences:

+ /// - If the timeout is negative you will get an thrown (WaitAsync() allows -1 as a special-case for "wait forever" + /// but this method doesn't allow that special case)

+ /// - Most importantly, in the case of a TimeoutException this method makes sure to properly cleanup + /// (cancel) the task itself so that you won't have to (it's so easy to forget this and it's a common source of memory-leaks and zombie-promise-tasks + /// that can cripple the system!). + ///
+ /// The task to monitor with a timeout. + /// The amount of time to wait for before throwing a . If zero or negative it gets interpreted + /// as "wait forever" and the method will just return the task itself. + /// (optional) The cancellation token that co-monitors the waiting mechanism. + /// Thrown when the task didn't complete within the specified timeout. + /// + /// // per https://devblogs.microsoft.com/premier-developer/the-danger-of-taskcompletionsourcet-class/ + /// var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + /// + /// try + /// { + /// PropertyChanged += MyEventHandler_; + /// await tcs.WaitTaskWithTimeoutAsync<int>(timeout); + /// } + /// finally + /// { + /// PropertyChanged -= MyEventHandler_; //this is needed in the case of a timeout + /// } + /// + /// return; + /// + /// void MyEventHandler_(object? _, SomeEventArgs ea_) + /// { + /// try + /// { + /// // ... logic here ... + /// + /// PropertyChanged -= BindableObject_PropertyChanged_; //best to unwire the listener as soon as we can + /// tcs.TrySetResult(123); + /// } + /// catch (Exception ex) + /// { + /// PropertyChanged -= BindableObject_PropertyChanged_; //best to unwire the listener as soon as we can + /// tcs.TrySetException(ex); //vital we need to ensure that the task gets completed one way or another + /// } + /// } + /// + public async static Task WaitTaskWithTimeoutAsync(this TaskCompletionSource tcs, TimeSpan timespan, CancellationToken cancellationToken = default) + { + if (timespan < TimeSpan.Zero) //note that this deviates from the behaviour of .WaitAsync() which does accept -1 milliseconds which means "wait forever" + { + var exception = new ArgumentException("Timeout must be zero or positive", nameof(timespan)); + tcs.TrySetException(exception); //vital we need to ensure that the task gets completed one way or another + + throw exception; + } + + try + { + return await tcs.Task.WaitAsync(timespan, cancellationToken); + } + catch (Exception ex) when (ex is TimeoutException or TaskCanceledException) //taskcanceledexception can come from cancellation-token timeouts + { + var isCancellationSuccessful = tcs.TrySetCanceled(cancellationToken); //00 vital + if (isCancellationSuccessful) + throw; + + return tcs.Task.IsCompletedSuccessfully //10 barely completed in time + ? tcs.Task.Result //micro-optimization to avoid the overhead of await + : await tcs.Task; //this means the task itself faulted + } + + //00 it is absolutely vital to trash the tcs and ensure it will not pester the system as a zombie promise-task + // waitasync() does not take care of this aspect because quite simply it cannot even if it tried + // all waitasync() does is to simply unwire the continuation from task.Task and leave it be + // + //10 if the cancellation fails it means one of the following two things: + // + // 1. that the task barely managed to complete in time on its own at the nick of time we prefer to give + // it the benefit of the doubt and let it complete normally + // + // 2. that the task itself threw a timeout-exception and in this case we prefer to honor the exception that + // the task itself threw and let it be propagated to the caller + } + } +} \ No newline at end of file diff --git a/Laerdal.McuMgr/Shared/Common/Helpers/TaskExtensions.cs b/Laerdal.McuMgr/Shared/Common/Helpers/TaskExtensions.cs deleted file mode 100644 index 67e54ba0..00000000 --- a/Laerdal.McuMgr/Shared/Common/Helpers/TaskExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Laerdal.McuMgr.Common.Helpers -{ - static internal class TaskExtensions - { - static public async Task WithTimeoutInMs(this Task task, int timeout) - { - if (await Task.WhenAny(task, Task.Delay(timeout)) == task) - return await task; - - throw new TimeoutException(); - } - } -} \ No newline at end of file diff --git a/Laerdal.McuMgr/Shared/DeviceResetter/DeviceResetter.cs b/Laerdal.McuMgr/Shared/DeviceResetter/DeviceResetter.cs index ff40e62f..78f98c07 100644 --- a/Laerdal.McuMgr/Shared/DeviceResetter/DeviceResetter.cs +++ b/Laerdal.McuMgr/Shared/DeviceResetter/DeviceResetter.cs @@ -109,9 +109,7 @@ public async Task ResetAsync(int timeoutInMs = -1) if (verdict != EDeviceResetterInitializationVerdict.Success) throw new ArgumentException(verdict.ToString()); - _ = timeoutInMs <= 0 - ? await taskCompletionSource.Task - : await taskCompletionSource.Task.WithTimeoutInMs(timeout: timeoutInMs); + await taskCompletionSource.WaitTaskWithOptionalTimeoutAsync(timeoutInMs); } catch (TimeoutException ex) { diff --git a/Laerdal.McuMgr/Shared/FileDownloader/FileDownloader.cs b/Laerdal.McuMgr/Shared/FileDownloader/FileDownloader.cs index 737be86a..61df02e5 100644 --- a/Laerdal.McuMgr/Shared/FileDownloader/FileDownloader.cs +++ b/Laerdal.McuMgr/Shared/FileDownloader/FileDownloader.cs @@ -285,10 +285,7 @@ public async Task DownloadAsync( if (verdict != EFileDownloaderVerdict.Success) throw new ArgumentException(verdict.ToString()); - result = timeoutForDownloadInMs < 0 - ? await taskCompletionSource.Task - : await taskCompletionSource.Task.WithTimeoutInMs(timeout: timeoutForDownloadInMs); - + result = await taskCompletionSource.WaitTaskWithOptionalTimeoutAsync(timeoutForDownloadInMs); break; } catch (TimeoutException ex) diff --git a/Laerdal.McuMgr/Shared/FileUploader/FileUploader.cs b/Laerdal.McuMgr/Shared/FileUploader/FileUploader.cs index cf822540..7ab52403 100644 --- a/Laerdal.McuMgr/Shared/FileUploader/FileUploader.cs +++ b/Laerdal.McuMgr/Shared/FileUploader/FileUploader.cs @@ -352,10 +352,7 @@ public async Task UploadAsync( if (verdict != EFileUploaderVerdict.Success) throw new ArgumentException(verdict.ToString()); - _ = timeoutForUploadInMs < 0 - ? await taskCompletionSource.Task - : await taskCompletionSource.Task.WithTimeoutInMs(timeout: timeoutForUploadInMs); //order - + await taskCompletionSource.WaitTaskWithOptionalTimeoutAsync(timeoutForUploadInMs); //order break; } catch (TimeoutException ex) diff --git a/Laerdal.McuMgr/Shared/FirmwareEraser/FirmwareEraser.cs b/Laerdal.McuMgr/Shared/FirmwareEraser/FirmwareEraser.cs index b53d2d2d..ba9d5ba4 100644 --- a/Laerdal.McuMgr/Shared/FirmwareEraser/FirmwareEraser.cs +++ b/Laerdal.McuMgr/Shared/FirmwareEraser/FirmwareEraser.cs @@ -125,9 +125,7 @@ public async Task EraseAsync(int imageIndex = 1, int timeoutInMs = -1) if (verdict != EFirmwareErasureInitializationVerdict.Success) throw new ArgumentException(verdict.ToString()); - _ = timeoutInMs <= 0 - ? await taskCompletionSource.Task - : await taskCompletionSource.Task.WithTimeoutInMs(timeout: timeoutInMs); + await taskCompletionSource.WaitTaskWithOptionalTimeoutAsync(timeoutInMs); } catch (TimeoutException ex) { diff --git a/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/Exceptions/FirmwareInstallationUploadingStageErroredOutException.cs b/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/Exceptions/FirmwareInstallationUploadingStageErroredOutException.cs index 4d728360..d15254bd 100644 --- a/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/Exceptions/FirmwareInstallationUploadingStageErroredOutException.cs +++ b/Laerdal.McuMgr/Shared/FirmwareInstaller/Contracts/Exceptions/FirmwareInstallationUploadingStageErroredOutException.cs @@ -4,8 +4,8 @@ namespace Laerdal.McuMgr.FirmwareInstaller.Contracts.Exceptions { public class FirmwareInstallationUploadingStageErroredOutException : FirmwareInstallationErroredOutException, IFirmwareInstallationException { - public FirmwareInstallationUploadingStageErroredOutException(EGlobalErrorCode globalErrorCode) - : base("An error occurred while uploading the firmware", globalErrorCode) + public FirmwareInstallationUploadingStageErroredOutException(string internalErrorMessage, EGlobalErrorCode globalErrorCode) + : base($"An error occurred while uploading the firmware: {internalErrorMessage}", globalErrorCode) { } } diff --git a/Laerdal.McuMgr/Shared/FirmwareInstaller/FirmwareInstaller.cs b/Laerdal.McuMgr/Shared/FirmwareInstaller/FirmwareInstaller.cs index b80a7254..f4d558b0 100644 --- a/Laerdal.McuMgr/Shared/FirmwareInstaller/FirmwareInstaller.cs +++ b/Laerdal.McuMgr/Shared/FirmwareInstaller/FirmwareInstaller.cs @@ -266,9 +266,7 @@ public async Task InstallAsync( if (verdict != EFirmwareInstallationVerdict.Success) throw new ArgumentException(verdict.ToString()); - _ = timeoutInMs <= 0 - ? await taskCompletionSource.Task - : await taskCompletionSource.Task.WithTimeoutInMs(timeout: timeoutInMs); + await taskCompletionSource.WaitTaskWithOptionalTimeoutAsync(timeoutInMs); } catch (TimeoutException ex) { @@ -373,7 +371,7 @@ void FirmwareInstaller_FatalErrorOccurred_(object _, FatalErrorOccurredEventArgs => new FirmwareInstallationConfirmationStageTimeoutException(estimatedSwapTimeInMilliseconds, ea_.GlobalErrorCode), { FatalErrorType: EFirmwareInstallerFatalErrorType.FirmwareUploadingErroredOut } or { State: EFirmwareInstallationState.Uploading } - => new FirmwareInstallationUploadingStageErroredOutException(ea_.GlobalErrorCode), + => new FirmwareInstallationUploadingStageErroredOutException(ea_.ErrorMessage, ea_.GlobalErrorCode), _ => new FirmwareInstallationErroredOutException($"{ea_.ErrorMessage} (state={ea_.State})", ea_.GlobalErrorCode) }); diff --git a/Laerdal.Scripts/Laerdal.GenerateSignAndUploadSbom.sh b/Laerdal.Scripts/Laerdal.GenerateSignAndUploadSbom.sh index f073f71d..3d918ad4 100644 --- a/Laerdal.Scripts/Laerdal.GenerateSignAndUploadSbom.sh +++ b/Laerdal.Scripts/Laerdal.GenerateSignAndUploadSbom.sh @@ -2,6 +2,9 @@ # set -x +declare host_os="" +declare host_os_and_architecture="" + declare project_name="" declare project_version="" @@ -13,6 +16,8 @@ declare csproj_classifier="" declare output_directory_path="" declare output_sbom_file_name="" declare sbom_signing_key_file_path="" +declare skip_installing_cyclonedx_cli_tool="no" +declare skip_installing_cyclonedx_dotnet_extension="no" declare dependency_tracker_url="" declare dependency_tracker_api_key_file_path="" @@ -21,17 +26,17 @@ function parse_arguments() { while [[ $# -gt 0 ]]; do case $1 in - + --project-name) project_name="$2" shift ;; - + --project-version) project_version="$2" shift ;; - + --parent-project-name) parent_project_name="$2" shift @@ -56,7 +61,7 @@ function parse_arguments() { output_directory_path="$2" shift ;; - + --output-sbom-file-name) output_sbom_file_name="$2" shift @@ -67,6 +72,16 @@ function parse_arguments() { shift ;; + --skip-installing-cyclonedx-cli-tool) + skip_installing_cyclonedx_cli_tool="yes" + # shift # no need to shift here + ;; + + --skip-installing-cyclonedx-dotnet-extension) + skip_installing_cyclonedx_dotnet_extension="yes" + # shift # no need to shift here + ;; + --dependency-tracker-url) dependency_tracker_url="$2" shift @@ -104,11 +119,11 @@ function parse_arguments() { # if [[ -z ${parent_project_version} ]]; then this is optional # ... - + # if [[ -n ${parent_project_name} && -z ${parent_project_version} ]]; then # nah better not to enforce this # echo "Specifying --parent-project-version is mandatory when --parent-project-name has been used!" # usage - # exit 1 + # exit 1 # fi if [[ -z ${csproj_file_path} ]]; then @@ -134,13 +149,13 @@ function parse_arguments() { usage exit 1 fi - + if [[ -z ${sbom_signing_key_file_path} ]]; then echo "Specifying --sbom-signing-key-file-path is mandatory!" usage exit 1 fi - + if [[ -z ${dependency_tracker_url} ]]; then echo "Specifying --dependency-tracker-url is mandatory!" usage @@ -157,125 +172,234 @@ function parse_arguments() { function usage() { local -r script_name=$(basename "$0") - echo "Usage: ${script_name} --project-name --project-version [--parent-project-name --parent-project-version ] --csproj-file-path --csproj-file-path --output-directory-path --output-sbom-file-name --sbom-signing-key-file-path --dependency-tracker-url --dependency-tracker-api-key-file-path " + echo "Usage: ${script_name} --project-name --project-version [--skip-installing-cyclonedx-cli-tool] [--skip-installing-cyclonedx-dotnet-extension] [--parent-project-name --parent-project-version ] --csproj-file-path --csproj-file-path --output-directory-path --output-sbom-file-name --sbom-signing-key-file-path --dependency-tracker-url --dependency-tracker-api-key-file-path " } -function install_tools() { +function sniff_and_validate_host_os_and_architecture() { + host_os="$(uname -s)" + case "${host_os}" in + Linux*) host_os="Linux" ;; + MINGW*) host_os="Windows" ;; + Darwin*) host_os="Mac" ;; + CYGWIN*) host_os="Windows" ;; + MSYS_NT*) host_os="Windows" ;; + esac + + declare architecture=$(uname -m) + case ${architecture} in + i386) architecture="x86" ;; + i686) architecture="x86" ;; + x64) architecture="x64" ;; # shouldnt happen but just in case + x86_64) architecture="x64" ;; + + armv7l) architecture="arm" ;; + arm64) architecture="arm64" ;; + aarch64) architecture="arm64" ;; + arm) dpkg --print-architecture | grep -q "arm64" && architecture="arm64" || architecture="arm" ;; + esac + + host_os_and_architecture="${host_os}-${architecture}" # e.g. Linux-x64 + + if [[ ${host_os} == "Mac" ]] || + [[ ${host_os} == "Linux" ]] || + [[ ${host_os_and_architecture} == "Windows-x86" ]] || + [[ ${host_os_and_architecture} == "Windows-x64" ]] || + [[ ${host_os_and_architecture} == "Windows-arm" ]] || + [[ ${host_os_and_architecture} == "Windows-arm64" ]]; then + return # host os is supported + fi + + echo "Unsupported host OS '${host_os_and_architecture}' - don't know how to install the CycloneDX tool on this platform." + exit 1 +} + +function install_dotnet_cyclonedx() { + if [[ ${skip_installing_cyclonedx_dotnet_extension} == "yes" ]]; then + return # the calling environment might have τηε cyclonedx extension for dotnet preinstalled + fi echo echo "** Installing CycloneDX as a dotnet tool:" - dotnet tool \ - install \ - --global CycloneDX + dotnet tool \ + install \ + --global CycloneDX declare exitCode=$? if [ $exitCode != 0 ]; then - echo "##vso[task.logissue type=error]Something went wrong with the CycloneDX tool for dotnet." + echo "Something went wrong with the CycloneDX tool for dotnet." exit 10 fi echo echo "** CycloneDX:" - which dotnet-CycloneDX && dotnet-CycloneDX --version + which dotnet-CycloneDX && dotnet cyclonedx --version declare exitCode=$? if [ $exitCode != 0 ]; then - echo "##vso[task.logissue type=error]Something's wrong with 'dotnet-CycloneDX'." + echo "Something's wrong with 'dotnet-CycloneDX'." exit 12 fi +} - # we need to install the CycloneDX tool too in order to sign the artifacts - curl --output cyclonedx --url https://github.com/CycloneDX/cyclonedx-cli/releases/download/v0.26.0/cyclonedx-osx-arm64 \ - && chmod +x cyclonedx - declare exitCode=$? - if [ $exitCode != 0 ]; then - echo "##vso[task.logissue type=error]Failed to install 'cyclonedx'." - exit 13 +function install_cyclonedx_standalone() { # we need to install the CycloneDX tool too in order to sign the artifacts + if [[ ${skip_installing_cyclonedx_cli_tool} == "yes" ]]; then + return # the calling environment might have cyclonedx preinstalled fi + sniff_and_validate_host_os_and_architecture + + echo "** Installing cyclonedx cli tool for '${host_os}'" + if [[ ${host_os} == "Mac" ]] || [[ ${host_os} == "Linux" ]]; then + brew install cyclonedx/cyclonedx/cyclonedx-cli # both the macos and linux vmimages support brew so we can use it + declare exitCode=$? + if [ $exitCode != 0 ]; then + echo "Failed to install 'cyclonedx'." + exit 1 + fi + + return + fi + + if [[ ${host_os_and_architecture} == "Windows-x86" ]]; then # windows does not support brew and chocolatey does not have a cyclonedx-cli package as of Q3 2024 + curl --output cyclonedx --url https://github.com/CycloneDX/cyclonedx-cli/releases/download/v0.27.1/cyclonedx-win-x86.exe && + chmod +x cyclonedx + declare exitCode=$? + if [ $exitCode != 0 ]; then + echo "Failed to install 'cyclonedx'." + exit 1 + fi + + return + fi + + if [[ ${host_os_and_architecture} == "Windows-x64" ]]; then + curl --output cyclonedx --url https://github.com/CycloneDX/cyclonedx-cli/releases/download/v0.27.1/cyclonedx-win-x64.exe && + chmod +x cyclonedx + declare exitCode=$? + if [ $exitCode != 0 ]; then + echo "Failed to install 'cyclonedx'." + exit 1 + fi + + return + fi + + if [[ ${host_os_and_architecture} == "Windows-arm" ]]; then + curl --output cyclonedx --url https://github.com/CycloneDX/cyclonedx-cli/releases/download/v0.27.1/cyclonedx-win-arm.exe && + chmod +x cyclonedx + declare exitCode=$? + if [ $exitCode != 0 ]; then + echo "Failed to install 'cyclonedx'." + exit 10 + fi + + return + fi + + if [[ ${host_os_and_architecture} == "Windows-arm64" ]]; then + curl --output cyclonedx --url https://github.com/CycloneDX/cyclonedx-cli/releases/download/v0.27.1/cyclonedx-win-arm64.exe && + chmod +x cyclonedx + declare exitCode=$? + if [ $exitCode != 0 ]; then + echo "Failed to install 'cyclonedx'." + exit 10 + fi + + return + fi + + echo "Unsupported host OS '${host_os_and_architecture}' - cannot install 'cyclonedx-cli'." + exit 1 +} + +function install_tools() { + install_dotnet_cyclonedx # order + install_cyclonedx_standalone # order } function generate_sign_and_upload_sbom() { # set -x - # GENERATE SBOM we intentionally disable package restore because the packages are already restored at this point - dotnet-CycloneDX "${csproj_file_path}" \ - --exclude-dev \ - --disable-package-restore \ - --include-project-references \ - \ - --output "${output_directory_path}" \ - --set-type "${csproj_classifier}" \ - --set-version "${project_version}" \ - \ - --filename "${output_sbom_file_name}" + # GENERATE SBOM + dotnet cyclonedx "${csproj_file_path}" \ + --exclude-dev \ + --include-project-references \ + \ + --output "${output_directory_path}" \ + --set-type "${csproj_classifier}" \ + --set-version "${project_version}" \ + \ + --filename "${output_sbom_file_name}" declare exitCode=$? if [ ${exitCode} != 0 ]; then - echo "##vso[task.logissue type=error]Failed to generate the SBOM!" + echo "Failed to generate the SBOM!" exit 20 fi - - # SIGN SBOM todo figure out why this doesnt actually sign anything on windows even though on macos it works as intended declare -r bom_file_path="${output_directory_path}/${output_sbom_file_name}" - ./cyclonedx sign bom \ - "${bom_file_path}" \ - --key-file "${sbom_signing_key_file_path}" + + declare cyclonedxCliFilepath="./cyclonedx" # we prioritize the local installation of the cyclonedx cli tool but + if [[ ! -f "${cyclonedxCliFilepath}" ]]; then # if the local installation of the cyclonedx cli tool is not available + cyclonedxCliFilepath="cyclonedx" # then and only then we opt to use the global installation of the tool + fi + + "${cyclonedxCliFilepath}" sign bom \ + "${bom_file_path}" \ + --key-file "${sbom_signing_key_file_path}" declare exitCode=$? if [ ${exitCode} != 0 ]; then - echo "##vso[task.logissue type=error]Singing the SBOM failed!" + echo "Singing the SBOM failed!" exit 30 fi # echo -e "\n\n" # tail "${bom_file_path}" # echo -e "\n\n" - - # UPLOAD SBOM declare optional_parent_project_name_parameter="" if [[ -n ${parent_project_name} ]]; then - optional_parent_project_name_parameter="--form parentName=${parent_project_name}" + optional_parent_project_name_parameter="--form parentName=${parent_project_name}" fi - + declare optional_parent_project_version_parameter="" if [[ -n ${parent_project_version} ]]; then - optional_parent_project_version_parameter="--form parentVersion=${parent_project_version}" + optional_parent_project_version_parameter="--form parentVersion=${parent_project_version}" fi - declare -r http_response_code=$( \ - curl "${dependency_tracker_url}" \ - --location \ - --request "POST" \ - \ - --header "Content-Type: multipart/form-data" \ - --header "X-API-Key: $(cat "${dependency_tracker_api_key_file_path}")" \ - \ - --form "bom=@${bom_file_path}" \ - --form "autoCreate=true" \ - \ - --form "projectName=${project_name}" \ - --form "projectVersion=${project_version}" \ - \ - ${optional_parent_project_name_parameter} \ - ${optional_parent_project_version_parameter} \ - \ - -w "%{http_code}" \ + declare -r http_response_code=$( + curl "${dependency_tracker_url}" \ + --location \ + --request "POST" \ + \ + --header "Content-Type: multipart/form-data" \ + --header "X-API-Key: $(cat "${dependency_tracker_api_key_file_path}")" \ + \ + --form "bom=@${bom_file_path}" \ + --form "autoCreate=true" \ + \ + --form "projectName=${project_name}" \ + --form "projectVersion=${project_version}" \ + \ + ${optional_parent_project_name_parameter} \ + ${optional_parent_project_version_parameter} \ + \ + -o /dev/null \ + -w "%{http_code}" ) - declare exitCode=$? - set +x - + # declare exitCode=$? # no point to get the exit code because we use a subshell to capture the http response code + echo "** Curl sbom-uploading HTTP Response Code: ${http_response_code}" - - if [ ${exitCode} != 0 ]; then - echo "##vso[task.logissue type=error]SBOM Uploading failed!" + + if [[ -z ${http_response_code} ]] || [[ ${http_response_code} -gt 299 ]]; then + echo "SBOM Uploading failed!" exit 40 fi } function main() { - parse_arguments "$@" - install_tools - generate_sign_and_upload_sbom + parse_arguments "$@" # order + install_tools # order + generate_sign_and_upload_sbom # order } main "$@" +# set +x \ No newline at end of file