diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 7e8a502a..b57f73a5 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,22 +3,22 @@ "isRoot": true, "tools": { "powershell": { - "version": "7.4.2", + "version": "7.4.5", "commands": [ "pwsh" ] }, "dotnet-coverage": { - "version": "17.11.0", + "version": "17.12.5", "commands": [ "dotnet-coverage" ] }, "nbgv": { - "version": "3.6.133", + "version": "3.6.143", "commands": [ "nbgv" ] } } -} +} \ No newline at end of file diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 34e56908..9626b31b 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,5 +1,5 @@ # Refer to https://hub.docker.com/_/microsoft-dotnet-sdk for available versions -FROM mcr.microsoft.com/dotnet/sdk:8.0.201-jammy +FROM mcr.microsoft.com/dotnet/sdk:8.0.402-jammy # Installing mono makes `dotnet test` work without errors even for net472. # But installing it takes a long time, so it's excluded by default. diff --git a/.editorconfig b/.editorconfig index 704e66d7..23b8206d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -26,6 +26,7 @@ indent_size = 2 # Xml config files [*.{ruleset,config,nuspec,resx,vsixmanifest,vsct,runsettings}] indent_size = 2 +indent_style = space [*.{js,ts,json}] indent_style = tab @@ -188,5 +189,8 @@ dotnet_diagnostic.DOC202.severity = warning # CA1062: Validate arguments of public methods dotnet_diagnostic.CA1062.severity = warning +# CA2016: Forward the CancellationToken parameter +dotnet_diagnostic.CA2016.severity = warning + [*.sln] indent_style = tab diff --git a/.gitignore b/.gitignore index 69599b87..cc2b1247 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,9 @@ bld/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ +# Jetbrains Rider cache directory +.idea/ + # Visual Studio 2017 auto generated files Generated\ Files/ @@ -352,3 +355,6 @@ MigrationBackup/ # mac-created file to track user view preferences for a directory .DS_Store + +# Analysis results +*.sarif diff --git a/Directory.Build.props b/Directory.Build.props index aa57ea5a..16f48edd 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,7 +6,6 @@ $(RepoRootPath)obj\$([MSBuild]::MakeRelative($(RepoRootPath), $(MSBuildProjectDirectory)))\ $(RepoRootPath)bin\$(MSBuildProjectName)\ $(RepoRootPath)bin\Packages\$(Configuration)\NuGet\ - 12 enable enable latest @@ -64,23 +63,4 @@ - - - false - true - - - - - false - false - false - false - diff --git a/Directory.Build.targets b/Directory.Build.targets index cc8184aa..ecd71a31 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,10 +1,9 @@ - - false + 12 + 16.9 - diff --git a/Directory.Packages.props b/Directory.Packages.props index 24d23067..feb36ddf 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,7 +15,7 @@ - + @@ -28,20 +28,17 @@ - + - + - + - + - + - - - diff --git a/azure-pipelines/Get-LibTemplateBasis.ps1 b/azure-pipelines/Get-LibTemplateBasis.ps1 new file mode 100644 index 00000000..2181c77b --- /dev/null +++ b/azure-pipelines/Get-LibTemplateBasis.ps1 @@ -0,0 +1,25 @@ +<# +.SYNOPSIS + Returns the name of the well-known branch in the Library.Template repository upon which HEAD is based. +#> +[CmdletBinding(SupportsShouldProcess = $true)] +Param( + [switch]$ErrorIfNotRelated +) + +# This list should be sorted in order of decreasing specificity. +$branchMarkers = @( + @{ commit = 'fd0a7b25ccf030bbd16880cca6efe009d5b1fffc'; branch = 'microbuild' }; + @{ commit = '05f49ce799c1f9cc696d53eea89699d80f59f833'; branch = 'main' }; +) + +foreach ($entry in $branchMarkers) { + if (git rev-list HEAD | Select-String -Pattern $entry.commit) { + return $entry.branch + } +} + +if ($ErrorIfNotRelated) { + Write-Error "Library.Template has not been previously merged with this repo. Please review https://github.com/AArnott/Library.Template/tree/main?tab=readme-ov-file#readme for instructions." + exit 1 +} diff --git a/azure-pipelines/build.yml b/azure-pipelines/build.yml index 8e82a1c6..9e3d004a 100644 --- a/azure-pipelines/build.yml +++ b/azure-pipelines/build.yml @@ -35,7 +35,7 @@ jobs: - job: Linux pool: - vmImage: Ubuntu 20.04 + vmImage: Ubuntu-22.04 steps: - checkout: self fetchDepth: 0 # avoid shallow clone so nbgv can do its work. @@ -55,7 +55,7 @@ jobs: - job: macOS condition: ${{ parameters.includeMacOS }} pool: - vmImage: macOS-12 + vmImage: macOS-14 steps: - checkout: self fetchDepth: 0 # avoid shallow clone so nbgv can do its work. diff --git a/azure-pipelines/libtemplate-update.yml b/azure-pipelines/libtemplate-update.yml new file mode 100644 index 00000000..87302b06 --- /dev/null +++ b/azure-pipelines/libtemplate-update.yml @@ -0,0 +1,146 @@ +# This pipeline schedules regular merges of Library.Template into a repo that is based on it. +# Only Azure Repos are supported. GitHub support comes via a GitHub Actions workflow. + +trigger: none +pr: none +schedules: +- cron: "0 3 * * Mon" # Sun @ 8 or 9 PM Mountain Time (depending on DST) + displayName: Weekly trigger + branches: + include: + - main + always: true + +parameters: +- name: AutoComplete + displayName: Auto-complete pull request + type: boolean + default: false + +stages: +- stage: Merge + jobs: + - job: merge + pool: + vmImage: ubuntu-latest + steps: + - checkout: self + fetchDepth: 0 + clean: true + - pwsh: | + $LibTemplateBranch = & ./azure-pipelines/Get-LibTemplateBasis.ps1 -ErrorIfNotRelated + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } + + git fetch https://github.com/aarnott/Library.Template $LibTemplateBranch + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } + $LibTemplateCommit = git rev-parse FETCH_HEAD + + if ((git rev-list FETCH_HEAD ^HEAD --count) -eq 0) { + Write-Host "There are no Library.Template updates to merge." + exit 0 + } + + $UpdateBranchName = 'auto/libtemplateUpdate' + git -c http.extraheader="AUTHORIZATION: bearer $(System.AccessToken)" push origin -f FETCH_HEAD:refs/heads/$UpdateBranchName + + Write-Host "Creating pull request" + $contentType = 'application/json'; + $headers = @{ Authorization = 'Bearer $(System.AccessToken)' }; + $rawRequest = @{ + sourceRefName = "refs/heads/$UpdateBranchName"; + targetRefName = "refs/heads/main"; + title = 'Merge latest Library.Template'; + description = "This merges the latest features and fixes from [Library.Template's $LibTemplateBranch branch](https://github.com/AArnott/Library.Template/tree/$LibTemplateBranch)."; + } + $request = ConvertTo-Json $rawRequest + + $prApiBaseUri = '$(System.TeamFoundationCollectionUri)/$(System.TeamProject)/_apis/git/repositories/$(Build.Repository.ID)/pullrequests' + $prCreationUri = $prApiBaseUri + "?api-version=6.0" + Write-Host "POST $prCreationUri" + Write-Host $request + + $prCreationResult = Invoke-RestMethod -uri $prCreationUri -method POST -Headers $headers -ContentType $contentType -Body $request + $prUrl = "$($prCreationResult.repository.webUrl)/pullrequest/$($prCreationResult.pullRequestId)" + Write-Host "Pull request: $prUrl" + $prApiBaseUri += "/$($prCreationResult.pullRequestId)" + + $SummaryPath = Join-Path '$(Agent.TempDirectory)' 'summary.md' + Set-Content -Path $SummaryPath -Value "[Insertion pull request]($prUrl)" + Write-Host "##vso[task.uploadsummary]$SummaryPath" + + # Tag the PR + $tagUri = "$prApiBaseUri/labels?api-version=7.0" + $rawRequest = @{ + name = 'auto-template-merge'; + } + $request = ConvertTo-Json $rawRequest + Invoke-RestMethod -uri $tagUri -method POST -Headers $headers -ContentType $contentType -Body $request | Out-Null + + # Add properties to the PR that we can programatically parse later. + Function Set-PRProperties($properties) { + $rawRequest = $properties.GetEnumerator() |% { + @{ + op = 'add' + path = "/$($_.key)" + from = $null + value = $_.value + } + } + $request = ConvertTo-Json $rawRequest + $setPrPropertyUri = "$prApiBaseUri/properties?api-version=7.0" + Write-Debug "$request" + $setPrPropertyResult = Invoke-RestMethod -uri $setPrPropertyUri -method PATCH -Headers $headers -ContentType 'application/json-patch+json' -Body $request -StatusCodeVariable setPrPropertyStatus -SkipHttpErrorCheck + if ($setPrPropertyStatus -ne 200) { + Write-Host "##vso[task.logissue type=warning]Failed to set pull request properties. Result: $setPrPropertyStatus. $($setPrPropertyResult.message)" + } + } + Write-Host "Setting pull request properties" + Set-PRProperties @{ + 'AutomatedMerge.SourceBranch' = $LibTemplateBranch + 'AutomatedMerge.SourceCommit' = $LibTemplateCommit + } + + # Add an *active* PR comment to warn users to *merge* the pull request instead of squash it. + $request = ConvertTo-Json @{ + comments = @( + @{ + parentCommentId = 0 + content = "Do **not** squash this pull request when completing it. You must *merge* it." + commentType = 'system' + } + ) + status = 'active' + } + $result = Invoke-RestMethod -uri "$prApiBaseUri/threads?api-version=7.1" -method POST -Headers $headers -ContentType $contentType -Body $request -StatusCodeVariable addCommentStatus -SkipHttpErrorCheck + if ($addCommentStatus -ne 200) { + Write-Host "##vso[task.logissue type=warning]Failed to post comment on pull request. Result: $addCommentStatus. $($result.message)" + } + + # Set auto-complete on the PR + if ('${{ parameters.AutoComplete }}' -eq 'True') { + Write-Host "Setting auto-complete" + $mergeMessage = "Merged PR $($prCreationResult.pullRequestId): " + $commitMessage + $rawRequest = @{ + autoCompleteSetBy = @{ + id = $prCreationResult.createdBy.id + }; + completionOptions = @{ + deleteSourceBranch = $true; + mergeCommitMessage = $mergeMessage; + mergeStrategy = 'noFastForward'; + }; + } + $request = ConvertTo-Json $rawRequest + Write-Host $request + $uri = "$($prApiBaseUri)?api-version=6.0" + $result = Invoke-RestMethod -uri $uri -method PATCH -Headers $headers -ContentType $contentType -Body $request -StatusCodeVariable autoCompleteStatus -SkipHttpErrorCheck + if ($autoCompleteStatus -ne 200) { + Write-Host "##vso[task.logissue type=warning]Failed to set auto-complete on pull request. Result: $autoCompleteStatus. $($result.message)" + } + } + + displayName: Create pull request diff --git a/azure-pipelines/publish-codecoverage.yml b/azure-pipelines/publish-codecoverage.yml index fbb6a39a..8ec94e64 100644 --- a/azure-pipelines/publish-codecoverage.yml +++ b/azure-pipelines/publish-codecoverage.yml @@ -17,9 +17,8 @@ steps: condition: and(succeeded(), ${{ parameters.includeMacOS }}) - powershell: azure-pipelines/Merge-CodeCoverage.ps1 -Path '$(Pipeline.Workspace)' -OutputFile coveragereport/merged.cobertura.xml -Format Cobertura -Verbose displayName: ⚙ Merge coverage -- task: PublishCodeCoverageResults@1 +- task: PublishCodeCoverageResults@2 displayName: 📢 Publish code coverage results to Azure DevOps inputs: - codeCoverageTool: cobertura summaryFileLocation: coveragereport/merged.cobertura.xml failIfCoverageEmpty: true diff --git a/azurepipelines-coverage.yml b/azurepipelines-coverage.yml new file mode 100644 index 00000000..0cd5dad3 --- /dev/null +++ b/azurepipelines-coverage.yml @@ -0,0 +1,6 @@ +# https://learn.microsoft.com/azure/devops/pipelines/test/codecoverage-for-pullrequests?view=azure-devops +coverage: + status: + comments: on # add comment to PRs reporting diff in coverage of modified files + diff: # diff coverage is code coverage only for the lines changed in a pull request. + target: 70% # set this to a desired %. Default is 70% diff --git a/global.json b/global.json index 2565f236..1f0eafb4 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.201", + "version": "8.0.402", "rollForward": "patch", "allowPrerelease": false } diff --git a/src/AssemblyInfo.vb b/src/AssemblyInfo.vb new file mode 100644 index 00000000..34b7cbe1 --- /dev/null +++ b/src/AssemblyInfo.vb @@ -0,0 +1,6 @@ +' Copyright (c) COMPANY-PLACEHOLDER. All rights reserved. +' Licensed under the MIT license. See LICENSE file in the project root for full license information. + +Imports System.Runtime.InteropServices + + diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 07f41346..654f5c6d 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -1,7 +1,8 @@ - + + diff --git a/src/Nerdbank.Streams/NestedStream.cs b/src/Nerdbank.Streams/NestedStream.cs index f29a91fb..4c79d69f 100644 --- a/src/Nerdbank.Streams/NestedStream.cs +++ b/src/Nerdbank.Streams/NestedStream.cs @@ -120,7 +120,7 @@ public override async Task ReadAsync(byte[] buffer, int offset, int count, return 0; } - int bytesRead = await this.underlyingStream.ReadAsync(buffer, offset, count).ConfigureAwaitRunInline(); + int bytesRead = await this.underlyingStream.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwaitRunInline(); this.remainingBytes -= bytesRead; return bytesRead; } diff --git a/src/Nerdbank.Streams/PipeExtensions.cs b/src/Nerdbank.Streams/PipeExtensions.cs index fd266764..d583327c 100644 --- a/src/Nerdbank.Streams/PipeExtensions.cs +++ b/src/Nerdbank.Streams/PipeExtensions.cs @@ -143,42 +143,44 @@ public static PipeWriter UsePipeWriter(this Stream stream, PipeOptions? pipeOpti Requires.Argument(stream.CanWrite, nameof(stream), "Stream must be writable."); var pipe = new Pipe(pipeOptions ?? PipeOptions.Default); - Task.Run(async delegate - { - try + Task.Run( + async delegate { - while (true) + try { - cancellationToken.ThrowIfCancellationRequested(); - ReadResult readResult = await pipe.Reader.ReadAsync(cancellationToken).ConfigureAwait(false); - if (readResult.Buffer.Length > 0) + while (true) { - foreach (ReadOnlyMemory segment in readResult.Buffer) + cancellationToken.ThrowIfCancellationRequested(); + ReadResult readResult = await pipe.Reader.ReadAsync(cancellationToken).ConfigureAwait(false); + if (readResult.Buffer.Length > 0) { - await stream.WriteAsync(segment, cancellationToken).ConfigureAwait(false); - } + foreach (ReadOnlyMemory segment in readResult.Buffer) + { + await stream.WriteAsync(segment, cancellationToken).ConfigureAwait(false); + } - await stream.FlushIfNecessaryAsync(cancellationToken).ConfigureAwait(false); - } + await stream.FlushIfNecessaryAsync(cancellationToken).ConfigureAwait(false); + } - pipe.Reader.AdvanceTo(readResult.Buffer.End); - readResult.ScrubAfterAdvanceTo(); + pipe.Reader.AdvanceTo(readResult.Buffer.End); + readResult.ScrubAfterAdvanceTo(); - if (readResult.IsCompleted) - { - break; + if (readResult.IsCompleted) + { + break; + } } - } - await pipe.Reader.CompleteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - // Propagate the exception to the writer. - await pipe.Reader.CompleteAsync(ex).ConfigureAwait(false); - return; - } - }).Forget(); + await pipe.Reader.CompleteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + // Propagate the exception to the writer. + await pipe.Reader.CompleteAsync(ex).ConfigureAwait(false); + return; + } + }, + CancellationToken.None).Forget(); return pipe.Writer; } @@ -276,41 +278,43 @@ public static PipeReader UsePipeReader(this WebSocket webSocket, int sizeHint = Requires.NotNull(webSocket, nameof(webSocket)); var pipe = new Pipe(pipeOptions ?? PipeOptions.Default); - Task.Run(async delegate - { - while (true) + Task.Run( + async delegate { - Memory memory = pipe.Writer.GetMemory(sizeHint); - try + while (true) { - cancellationToken.ThrowIfCancellationRequested(); + Memory memory = pipe.Writer.GetMemory(sizeHint); + try + { + cancellationToken.ThrowIfCancellationRequested(); #pragma warning disable IDE0008 // Use explicit type - it varies across TFMs so we rely on duck-typing. - var readResult = await webSocket.ReceiveAsync(memory, cancellationToken).ConfigureAwait(false); + var readResult = await webSocket.ReceiveAsync(memory, cancellationToken).ConfigureAwait(false); #pragma warning restore IDE0008 // Use explicit type - if (readResult.Count == 0) + if (readResult.Count == 0) + { + break; + } + + pipe.Writer.Advance(readResult.Count); + } + catch (Exception ex) { - break; + // Propagate the exception to the reader. + await pipe.Writer.CompleteAsync(ex).ConfigureAwait(false); + return; } - pipe.Writer.Advance(readResult.Count); - } - catch (Exception ex) - { - // Propagate the exception to the reader. - await pipe.Writer.CompleteAsync(ex).ConfigureAwait(false); - return; - } - - FlushResult result = await pipe.Writer.FlushAsync().ConfigureAwait(false); - if (result.IsCompleted) - { - break; + FlushResult result = await pipe.Writer.FlushAsync().ConfigureAwait(false); + if (result.IsCompleted) + { + break; + } } - } - // Tell the PipeReader that there's no more data coming - await pipe.Writer.CompleteAsync().ConfigureAwait(false); - }).Forget(); + // Tell the PipeReader that there's no more data coming + await pipe.Writer.CompleteAsync().ConfigureAwait(false); + }, + CancellationToken.None).Forget(); return pipe.Reader; } @@ -450,58 +454,60 @@ internal static Task LinkToAsync(this PipeReader reader, PipeWriter writer, Canc return Task.FromCanceled(cancellationToken); } - return Task.Run(async delegate - { - try + return Task.Run( + async delegate { - if (DuplexPipe.IsDefinitelyCompleted(reader)) - { - await writer.CompleteAsync().ConfigureAwait(false); - return; - } - - while (true) + try { - cancellationToken.ThrowIfCancellationRequested(); - ReadResult result = await reader.ReadAsync(cancellationToken).ConfigureAwait(false); - if (result.IsCanceled) + if (DuplexPipe.IsDefinitelyCompleted(reader)) { - cancellationToken.ThrowIfCancellationRequested(); - throw new OperationCanceledException(Strings.PipeReaderCanceled); + await writer.CompleteAsync().ConfigureAwait(false); + return; } - writer.Write(result.Buffer); - reader.AdvanceTo(result.Buffer.End); - result.ScrubAfterAdvanceTo(); - FlushResult flushResult = await writer.FlushAsync(cancellationToken).ConfigureAwait(false); - - if (flushResult.IsCanceled) + while (true) { cancellationToken.ThrowIfCancellationRequested(); - throw new OperationCanceledException(Strings.PipeWriterFlushCanceled); - } + ReadResult result = await reader.ReadAsync(cancellationToken).ConfigureAwait(false); + if (result.IsCanceled) + { + cancellationToken.ThrowIfCancellationRequested(); + throw new OperationCanceledException(Strings.PipeReaderCanceled); + } - if (flushResult.IsCompleted) - { - // Break out of copy loop. The receiver doesn't care any more. - break; - } + writer.Write(result.Buffer); + reader.AdvanceTo(result.Buffer.End); + result.ScrubAfterAdvanceTo(); + FlushResult flushResult = await writer.FlushAsync(cancellationToken).ConfigureAwait(false); - if (result.IsCompleted) - { - await writer.CompleteAsync().ConfigureAwait(false); - break; + if (flushResult.IsCanceled) + { + cancellationToken.ThrowIfCancellationRequested(); + throw new OperationCanceledException(Strings.PipeWriterFlushCanceled); + } + + if (flushResult.IsCompleted) + { + // Break out of copy loop. The receiver doesn't care any more. + break; + } + + if (result.IsCompleted) + { + await writer.CompleteAsync().ConfigureAwait(false); + break; + } } - } - await reader.CompleteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - await writer.CompleteAsync(ex).ConfigureAwait(false); - await reader.CompleteAsync(ex).ConfigureAwait(false); - } - }); + await reader.CompleteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + await writer.CompleteAsync(ex).ConfigureAwait(false); + await reader.CompleteAsync(ex).ConfigureAwait(false); + } + }, + CancellationToken.None); } /// @@ -585,46 +591,48 @@ private static PipeReader UsePipeReader(this Stream stream, int sizeHint = 0, Pi } #pragma warning restore CS0618 // Type or member is obsolete - Task.Run(async delegate - { - while (!combinedTokenSource.Token.IsCancellationRequested) + Task.Run( + async delegate { - Memory memory = pipe.Writer.GetMemory(sizeHint); - try + while (!combinedTokenSource.Token.IsCancellationRequested) { - int bytesRead = await stream.ReadAsync(memory, combinedTokenSource.Token).ConfigureAwait(false); - if (bytesRead == 0) + Memory memory = pipe.Writer.GetMemory(sizeHint); + try + { + int bytesRead = await stream.ReadAsync(memory, combinedTokenSource.Token).ConfigureAwait(false); + if (bytesRead == 0) + { + break; + } + + pipe.Writer.Advance(bytesRead); + } + catch (OperationCanceledException) { break; } + catch (ObjectDisposedException) + { + break; + } + catch (Exception ex) + { + // Propagate the exception to the reader. + await pipe.Writer.CompleteAsync(ex).ConfigureAwait(false); + return; + } - pipe.Writer.Advance(bytesRead); - } - catch (OperationCanceledException) - { - break; - } - catch (ObjectDisposedException) - { - break; - } - catch (Exception ex) - { - // Propagate the exception to the reader. - await pipe.Writer.CompleteAsync(ex).ConfigureAwait(false); - return; - } - - FlushResult result = await pipe.Writer.FlushAsync().ConfigureAwait(false); - if (result.IsCompleted) - { - break; + FlushResult result = await pipe.Writer.FlushAsync().ConfigureAwait(false); + if (result.IsCompleted) + { + break; + } } - } - // Tell the PipeReader that there's no more data coming - await pipe.Writer.CompleteAsync().ConfigureAwait(false); - }).Forget(); + // Tell the PipeReader that there's no more data coming + await pipe.Writer.CompleteAsync().ConfigureAwait(false); + }, + CancellationToken.None).Forget(); return pipe.Reader; } @@ -644,40 +652,42 @@ private static PipeWriter UsePipeWriter(WebSocket webSocket, PipeOptions? pipeOp Requires.NotNull(webSocket, nameof(webSocket)); var pipe = new Pipe(pipeOptions ?? PipeOptions.Default); - Task.Run(async delegate - { - try + Task.Run( + async delegate { - while (true) + try { - cancellationToken.ThrowIfCancellationRequested(); - ReadResult readResult = await pipe.Reader.ReadAsync(cancellationToken).ConfigureAwait(false); - if (readResult.Buffer.Length > 0) + while (true) { - foreach (ReadOnlyMemory segment in readResult.Buffer) + cancellationToken.ThrowIfCancellationRequested(); + ReadResult readResult = await pipe.Reader.ReadAsync(cancellationToken).ConfigureAwait(false); + if (readResult.Buffer.Length > 0) { - await webSocket.SendAsync(segment, messageType, endOfMessage: true, cancellationToken).ConfigureAwait(false); + foreach (ReadOnlyMemory segment in readResult.Buffer) + { + await webSocket.SendAsync(segment, messageType, endOfMessage: true, cancellationToken).ConfigureAwait(false); + } } - } - pipe.Reader.AdvanceTo(readResult.Buffer.End); - readResult.ScrubAfterAdvanceTo(); + pipe.Reader.AdvanceTo(readResult.Buffer.End); + readResult.ScrubAfterAdvanceTo(); - if (readResult.IsCompleted) - { - break; + if (readResult.IsCompleted) + { + break; + } } - } - await pipe.Reader.CompleteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - // Propagate the exception to the writer. - await pipe.Reader.CompleteAsync(ex).ConfigureAwait(false); - return; - } - }).Forget(); + await pipe.Reader.CompleteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + // Propagate the exception to the writer. + await pipe.Reader.CompleteAsync(ex).ConfigureAwait(false); + return; + } + }, + CancellationToken.None).Forget(); return pipe.Writer; } diff --git a/src/Nerdbank.Streams/StreamPipeWriter.cs b/src/Nerdbank.Streams/StreamPipeWriter.cs index 11a54998..1b8fdd62 100644 --- a/src/Nerdbank.Streams/StreamPipeWriter.cs +++ b/src/Nerdbank.Streams/StreamPipeWriter.cs @@ -112,7 +112,7 @@ public override async ValueTask FlushAsync(CancellationToken cancel cts.Token.ThrowIfCancellationRequested(); System.Buffers.ReadOnlySequence readOnlySeq = this.buffer.AsReadOnlySequence; ReadOnlyMemory segment = readOnlySeq.First; - await this.stream.WriteAsync(segment).ConfigureAwait(false); + await this.stream.WriteAsync(segment, CancellationToken.None).ConfigureAwait(false); this.buffer.AdvanceTo(readOnlySeq.GetPosition(segment.Length)); } diff --git a/src/Nerdbank.Streams/Substream.cs b/src/Nerdbank.Streams/Substream.cs index 8e6b9f39..14e635c6 100644 --- a/src/Nerdbank.Streams/Substream.cs +++ b/src/Nerdbank.Streams/Substream.cs @@ -158,8 +158,8 @@ public override async Task WriteAsync(byte[] buffer, int offset, int count, Canc { int totalCount = this.count + count; await this.WriteLengthHeaderAsync(totalCount, cancellationToken).ConfigureAwait(false); - await this.underlyingStream.WriteAsync(this.buffer, 0, this.count).ConfigureAwait(false); - await this.underlyingStream.WriteAsync(buffer, offset, count).ConfigureAwait(false); + await this.underlyingStream.WriteAsync(this.buffer, 0, this.count, cancellationToken).ConfigureAwait(false); + await this.underlyingStream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); this.count = 0; } } diff --git a/src/Nerdbank.Streams/SubstreamReader.cs b/src/Nerdbank.Streams/SubstreamReader.cs index 1ec7a301..bf4e3948 100644 --- a/src/Nerdbank.Streams/SubstreamReader.cs +++ b/src/Nerdbank.Streams/SubstreamReader.cs @@ -103,7 +103,7 @@ public override async Task ReadAsync(byte[] buffer, int offset, int count, { while (bytesRead < 4) { - int bytesJustRead = await this.underlyingStream.ReadAsync(this.intBuffer, bytesRead, 4 - bytesRead).ConfigureAwait(false); + int bytesJustRead = await this.underlyingStream.ReadAsync(this.intBuffer, bytesRead, 4 - bytesRead, cancellationToken).ConfigureAwait(false); if (bytesJustRead == 0) { throw new EndOfStreamException(); @@ -122,7 +122,7 @@ public override async Task ReadAsync(byte[] buffer, int offset, int count, return 0; } - bytesRead = await this.underlyingStream.ReadAsync(buffer, offset, Math.Min(count, this.count)).ConfigureAwait(false); + bytesRead = await this.underlyingStream.ReadAsync(buffer, offset, Math.Min(count, this.count), cancellationToken).ConfigureAwait(false); this.count -= bytesRead; return bytesRead; } diff --git a/src/Nerdbank.Streams/Utilities.cs b/src/Nerdbank.Streams/Utilities.cs index bd62a05c..975878a7 100644 --- a/src/Nerdbank.Streams/Utilities.cs +++ b/src/Nerdbank.Streams/Utilities.cs @@ -139,7 +139,7 @@ internal static Task FlushIfNecessaryAsync(this Stream stream, CancellationToken return Task.CompletedTask; } - return stream.FlushAsync(); + return stream.FlushAsync(cancellationToken); } /// diff --git a/tools/MergeFrom-Template.ps1 b/tools/MergeFrom-Template.ps1 new file mode 100644 index 00000000..3f721c6a --- /dev/null +++ b/tools/MergeFrom-Template.ps1 @@ -0,0 +1,79 @@ + +<# +.SYNOPSIS + Merges the latest changes from Library.Template into HEAD of this repo. +.PARAMETER LocalBranch + The name of the local branch to create at HEAD and use to merge into from Library.Template. +#> +[CmdletBinding(SupportsShouldProcess = $true)] +Param( + [string]$LocalBranch = "dev/$($env:USERNAME)/libtemplateUpdate" +) + +Function Spawn-Tool($command, $commandArgs, $workingDirectory, $allowFailures) { + if ($workingDirectory) { + Push-Location $workingDirectory + } + try { + if ($env:TF_BUILD) { + Write-Host "$pwd >" + Write-Host "##[command]$command $commandArgs" + } + else { + Write-Host "$command $commandArgs" -ForegroundColor Yellow + } + if ($commandArgs) { + & $command @commandArgs + } else { + Invoke-Expression $command + } + if ((!$allowFailures) -and ($LASTEXITCODE -ne 0)) { exit $LASTEXITCODE } + } + finally { + if ($workingDirectory) { + Pop-Location + } + } +} + +$remoteBranch = & $PSScriptRoot\..\azure-pipelines\Get-LibTemplateBasis.ps1 -ErrorIfNotRelated +if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE +} + +$LibTemplateUrl = 'https://github.com/aarnott/Library.Template' +Spawn-Tool 'git' ('fetch', $LibTemplateUrl, $remoteBranch) +$SourceCommit = Spawn-Tool 'git' ('rev-parse', 'FETCH_HEAD') +$BaseBranch = Spawn-Tool 'git' ('branch', '--show-current') +$SourceCommitUrl = "$LibTemplateUrl/commit/$SourceCommit" + +# To reduce the odds of merge conflicts at this stage, we always move HEAD to the last successful merge. +$basis = Spawn-Tool 'git' ('rev-parse', 'HEAD') # TODO: consider improving this later + +Write-Host "Merging the $remoteBranch branch of Library.Template ($SourceCommit) into local repo $basis" -ForegroundColor Green + +Spawn-Tool 'git' ('checkout', '-b', $LocalBranch, $basis) $null $true +if ($LASTEXITCODE -eq 128) { + Spawn-Tool 'git' ('checkout', $LocalBranch) + Spawn-Tool 'git' ('merge', $basis) +} + +Spawn-Tool 'git' ('merge', 'FETCH_HEAD', '--no-ff', '-m', "Merge the $remoteBranch branch from $LibTemplateUrl`n`nSpecifically, this merges [$SourceCommit from that repo]($SourceCommitUrl).") +if ($LASTEXITCODE -eq 1) { + Write-Error "Merge conflict detected. Manual resolution required." + exit 1 +} +elseif ($LASTEXITCODE -ne 0) { + Write-Error "Merge failed with exit code $LASTEXITCODE." + exit $LASTEXITCODE +} + +$result = New-Object PSObject -Property @{ + BaseBranch = $BaseBranch # The original branch that was checked out when the script ran. + LocalBranch = $LocalBranch # The name of the local branch that was created before the merge. + SourceCommit = $SourceCommit # The commit from Library.Template that was merged in. + SourceBranch = $remoteBranch # The branch from Library.Template that was merged in. +} + +Write-Host $result +Write-Output $result