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