mirror of
https://github.com/docker/cli.git
synced 2026-11-09 22:42:57 -04:00
Compare commits
74
Commits
v29.9.0-rc.2
...
v29.9.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a82e528901 | ||
|
|
e35bc1b6ae | ||
|
|
baf6c13889 | ||
|
|
4051f75f69 | ||
|
|
76321a699d | ||
|
|
b82456c62f | ||
|
|
a98becc3e3 | ||
|
|
dfc520dcc2 | ||
|
|
a3a2bb6118 | ||
|
|
c11295afc4 | ||
|
|
010e125d8b | ||
|
|
5cdf229230 | ||
|
|
66f6797582 | ||
|
|
cb8870a537 | ||
|
|
529f66d2f9 | ||
|
|
4d6ff7d646 | ||
|
|
b997c5b7aa | ||
|
|
1822903c1d | ||
|
|
37013cb262 | ||
|
|
61ee187552 | ||
|
|
433b13a249 | ||
|
|
5c8bfb2844 | ||
|
|
528a2ada2e | ||
|
|
de8f4032ad | ||
|
|
e4078058de | ||
|
|
8da8b00555 | ||
|
|
067d069f0c | ||
|
|
02fbbf3eb0 | ||
|
|
890976a13b | ||
|
|
23c66ccd61 | ||
|
|
3e686a8b59 | ||
|
|
f415da838b | ||
|
|
45c2317672 | ||
|
|
8308969eda | ||
|
|
cf809998ab | ||
|
|
730c880b35 | ||
|
|
e63e4fe498 | ||
|
|
e7df01fe6e | ||
|
|
2e51107855 | ||
|
|
949bf401dd | ||
|
|
9db9802127 | ||
|
|
63445d6b70 | ||
|
|
fdb4ee3e75 | ||
|
|
4ae247756e | ||
|
|
eabc65846b | ||
|
|
b873f57cd0 | ||
|
|
c8a849c2f9 | ||
|
|
f4efc1b596 | ||
|
|
2747e2e3bf | ||
|
|
329cdf1a9d | ||
|
|
0c7e2e7203 | ||
|
|
dfc4efb1e2 | ||
|
|
dba867594c | ||
|
|
d9aefe565a | ||
|
|
9182b351c1 | ||
|
|
0ae55a3e6c | ||
|
|
029a4b21b3 | ||
|
|
e596fea454 | ||
|
|
0c72f456aa | ||
|
|
26f2736a2b | ||
|
|
ac4c4dfabd | ||
|
|
1245054535 | ||
|
|
f72fb0caf0 | ||
|
|
e4af2bf0d3 | ||
|
|
49a1af6aaa | ||
|
|
c2be9ccfc3 | ||
|
|
0da6a5121f | ||
|
|
6b3ca8fc04 | ||
|
|
f47603c290 | ||
|
|
9709c8fe08 | ||
|
|
af45a23fa9 | ||
|
|
bc97f5ac19 | ||
|
|
6d71967120 | ||
|
|
9b51892b80 |
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended",
|
||||
// https://docs.renovatebot.com/modules/manager/github-actions/
|
||||
// https://docs.renovatebot.com/presets-helpers/
|
||||
"helpers:pinGitHubActionDigestsToSemver",
|
||||
":gitSignOff"
|
||||
],
|
||||
"gomod": {
|
||||
"managerFilePatterns": ["/(^|/)vendor\\.mod$/"]
|
||||
},
|
||||
"postUpdateOptions": ["gomodTidy"],
|
||||
"minimumReleaseAge": "3 days",
|
||||
"internalChecksFilter": "strict",
|
||||
"packageRules": [
|
||||
{
|
||||
"matchManagers": ["gomod"],
|
||||
"matchFileNames": ["vendor.mod"],
|
||||
"semanticCommits": "disabled",
|
||||
"commitMessagePrefix": "vendor:"
|
||||
},
|
||||
{
|
||||
"matchManagers": ["gomod"],
|
||||
"matchFileNames": ["cmd/docker-trust/go.mod"],
|
||||
// The trust module does not vendor dependencies; vendor/ belongs to the root module.
|
||||
"postUpdateOptions": ["gomodSkipVendor"],
|
||||
"semanticCommits": "disabled",
|
||||
"commitMessagePrefix": "cmd/docker-trust:"
|
||||
},
|
||||
{
|
||||
"matchManagers": ["gomod"],
|
||||
"matchFileNames": ["vendor.mod"],
|
||||
"groupName": "golang.org/x packages",
|
||||
"groupSlug": "golang-x",
|
||||
"matchDepNames": ["/^golang\\.org\\/x\\//"]
|
||||
},
|
||||
{
|
||||
"matchManagers": ["github-actions"],
|
||||
"minimumReleaseAge": "7 days",
|
||||
"addLabels": ["area/ci", "status/2-code-review"]
|
||||
},
|
||||
{
|
||||
"matchManagers": ["github-actions"],
|
||||
"groupName": "docker GitHub Actions",
|
||||
"groupSlug": "docker-actions",
|
||||
"matchDepNames": ["/^docker\\//"]
|
||||
},
|
||||
{
|
||||
// Keep all the google.golang.org/genproto modules at the same version.
|
||||
"matchManagers": ["gomod"],
|
||||
"matchFileNames": ["vendor.mod"],
|
||||
"groupName": "google.golang.org/genproto packages",
|
||||
"groupSlug": "google-genproto",
|
||||
"matchDepNames": ["/^google\\.golang\\.org\\/genproto(\\/|$)/"],
|
||||
"schedule": ["every month"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
-
|
||||
name: Build
|
||||
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
PLATFORM: ${{ matrix.platform }}
|
||||
-
|
||||
name: Upload artifacts
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: ${{ env.ARTIFACT_NAME }}
|
||||
path: /tmp/out/*
|
||||
@@ -156,7 +156,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
-
|
||||
name: Build
|
||||
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
|
||||
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
name: Update Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version: "1.26.8"
|
||||
go-version: "1.26.10"
|
||||
cache: false
|
||||
-
|
||||
name: Initialize CodeQL
|
||||
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
docker info
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
-
|
||||
name: Run ${{ matrix.target }}
|
||||
run: |
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload context
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: pr-review-context
|
||||
path: context/
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
-
|
||||
name: Test
|
||||
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version: "1.26.8"
|
||||
go-version: "1.26.10"
|
||||
cache: false
|
||||
-
|
||||
name: Test
|
||||
|
||||
@@ -101,7 +101,7 @@ jobs:
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version: "1.26.8"
|
||||
go-version: "1.26.10"
|
||||
cache: false
|
||||
-
|
||||
name: Run gocompat check
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ run:
|
||||
# which causes it to fallback to go1.17 semantics.
|
||||
#
|
||||
# TODO(thaJeztah): update "usetesting" settings to enable go1.24 features once our minimum version is go1.24
|
||||
go: "1.26.8"
|
||||
go: "1.26.10"
|
||||
|
||||
timeout: 5m
|
||||
|
||||
|
||||
+4
-4
@@ -8,11 +8,11 @@ ARG BASE_VARIANT=alpine
|
||||
ARG ALPINE_VERSION=3.23
|
||||
ARG BASE_DEBIAN_DISTRO=bookworm
|
||||
|
||||
ARG GO_VERSION=1.26.8
|
||||
ARG GO_VERSION=1.26.10
|
||||
|
||||
# XX_VERSION specifies the version of the xx utility to use.
|
||||
# It must be a valid tag in the docker.io/tonistiigi/xx image repository.
|
||||
ARG XX_VERSION=1.9.0
|
||||
ARG XX_VERSION=1.10.0
|
||||
|
||||
# GOVERSIONINFO_VERSION is the version of GoVersionInfo to install.
|
||||
# It must be a valid tag from https://github.com/josephspurrier/goversioninfo
|
||||
@@ -25,12 +25,12 @@ ARG GOTESTSUM_VERSION=v1.13.0
|
||||
# BUILDX_VERSION sets the version of buildx to use for the e2e tests.
|
||||
# It must be a tag in the docker.io/docker/buildx-bin image repository
|
||||
# on Docker Hub.
|
||||
ARG BUILDX_VERSION=0.34.1
|
||||
ARG BUILDX_VERSION=0.38.0
|
||||
|
||||
# COMPOSE_VERSION is the version of compose to install in the dev container.
|
||||
# It must be a tag in the docker.io/docker/compose-bin image repository
|
||||
# on Docker Hub.
|
||||
ARG COMPOSE_VERSION=v5.1.3
|
||||
ARG COMPOSE_VERSION=v5.6.0
|
||||
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
|
||||
|
||||
|
||||
@@ -30,7 +30,11 @@ package hooks
|
||||
type ResponseType int
|
||||
|
||||
const (
|
||||
// NextSteps renders suggestions under a shared "What's next:" header.
|
||||
NextSteps ResponseType = 0
|
||||
// GenericMessage renders before NextSteps, without a header or indentation.
|
||||
// CLI versions that do not recognize this type ignore the response.
|
||||
GenericMessage ResponseType = 1
|
||||
)
|
||||
|
||||
// Request is the type representing the information
|
||||
|
||||
@@ -7,17 +7,42 @@ const (
|
||||
indent = " "
|
||||
)
|
||||
|
||||
// PrintNextSteps renders list of [NextSteps] messages and writes them
|
||||
// to out. It is a no-op if messages is empty.
|
||||
func PrintNextSteps(out io.Writer, messages []string) {
|
||||
if len(messages) == 0 {
|
||||
return
|
||||
// PrintMessages groups messages by type and writes them to out. Generic messages
|
||||
// appear first, without a header or indentation, followed by next steps under a
|
||||
// shared "What's next:" header. Order within each type is preserved.
|
||||
// Messages with no lines or an unknown type produce no output.
|
||||
func PrintMessages(out io.Writer, messages []Response, evaluate func(Response) []string) {
|
||||
grouped := make(map[ResponseType][]string)
|
||||
for _, message := range messages {
|
||||
lines := evaluate(message)
|
||||
grouped[message.Type] = append(grouped[message.Type], lines...)
|
||||
}
|
||||
for _, responseType := range []ResponseType{GenericMessage, NextSteps} {
|
||||
lines := grouped[responseType]
|
||||
if len(lines) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
_, _ = io.WriteString(out, whatsNext)
|
||||
for _, msg := range messages {
|
||||
_, _ = io.WriteString(out, indent)
|
||||
_, _ = io.WriteString(out, msg)
|
||||
_, _ = io.WriteString(out, "\n")
|
||||
header, prefix := "\n", ""
|
||||
if responseType == NextSteps {
|
||||
header, prefix = whatsNext, indent
|
||||
}
|
||||
|
||||
_, _ = io.WriteString(out, header)
|
||||
for _, line := range lines {
|
||||
_, _ = io.WriteString(out, prefix)
|
||||
_, _ = io.WriteString(out, line)
|
||||
_, _ = io.WriteString(out, "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PrintNextSteps renders list of [NextSteps] messages and writes them
|
||||
// to out. It is a no-op if messages is empty.
|
||||
//
|
||||
// Deprecated: use [PrintMessages] instead.
|
||||
func PrintNextSteps(out io.Writer, messages []string) {
|
||||
PrintMessages(out, []Response{{Type: NextSteps}}, func(Response) []string {
|
||||
return messages
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,7 +8,85 @@ import (
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestPrintHookMessages(t *testing.T) {
|
||||
func TestPrintMessages(t *testing.T) {
|
||||
const header = "\n\x1b[1mWhat's next:\x1b[0m\n"
|
||||
|
||||
for _, tc := range []struct {
|
||||
doc string
|
||||
messages []hooks.Response
|
||||
expected string
|
||||
}{
|
||||
{doc: "no messages"},
|
||||
{
|
||||
doc: "no lines",
|
||||
messages: []hooks.Response{
|
||||
{Type: hooks.GenericMessage},
|
||||
{Type: hooks.NextSteps},
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "generic message",
|
||||
messages: []hooks.Response{
|
||||
{Type: hooks.GenericMessage, Template: "Session active"},
|
||||
},
|
||||
expected: "\nSession active\n",
|
||||
},
|
||||
{
|
||||
doc: "preserve message formatting",
|
||||
messages: []hooks.Response{
|
||||
{Type: hooks.GenericMessage, Template: "Session active\n\n Details"},
|
||||
},
|
||||
expected: "\nSession active\n\n Details\n",
|
||||
},
|
||||
{
|
||||
doc: "next steps",
|
||||
messages: []hooks.Response{
|
||||
{Type: hooks.NextSteps, Template: "Try another command"},
|
||||
},
|
||||
expected: header + " Try another command\n",
|
||||
},
|
||||
{
|
||||
doc: "group interleaved types and preserve order within each type",
|
||||
messages: []hooks.Response{
|
||||
{Type: hooks.NextSteps, Template: "First suggestion"},
|
||||
{Type: hooks.GenericMessage, Template: "Session active"},
|
||||
{Type: hooks.NextSteps, Template: "Second suggestion\nThird suggestion"},
|
||||
{Type: hooks.GenericMessage, Template: "Another status"},
|
||||
},
|
||||
expected: "\nSession active\nAnother status\n" + header + " First suggestion\n Second suggestion\n Third suggestion\n",
|
||||
},
|
||||
{
|
||||
doc: "unknown type",
|
||||
messages: []hooks.Response{
|
||||
{Type: hooks.ResponseType(99), Template: "Ignored"},
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "unknown type does not affect known messages",
|
||||
messages: []hooks.Response{
|
||||
{Type: hooks.ResponseType(99), Template: "Ignored"},
|
||||
{Type: hooks.GenericMessage, Template: "Session active"},
|
||||
{Type: hooks.NextSteps, Template: "Try another command"},
|
||||
},
|
||||
expected: "\nSession active\n" + header + " Try another command\n",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
var w strings.Builder
|
||||
hooks.PrintMessages(&w, tc.messages, func(message hooks.Response) []string {
|
||||
if message.Template == "" {
|
||||
return nil
|
||||
}
|
||||
lines, err := hooks.ParseTemplate(message.Template, nil)
|
||||
assert.NilError(t, err)
|
||||
return lines
|
||||
})
|
||||
assert.Equal(t, w.String(), tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintNextSteps(t *testing.T) {
|
||||
const header = "\n\x1b[1mWhat's next:\x1b[0m\n"
|
||||
|
||||
tests := []struct {
|
||||
@@ -34,6 +112,11 @@ func TestPrintHookMessages(t *testing.T) {
|
||||
" Foo\n" +
|
||||
" bar\n",
|
||||
},
|
||||
{
|
||||
doc: "preserve empty and indented lines",
|
||||
messages: []string{"", " Details", ""},
|
||||
expectedOutput: header + " \n Details\n \n",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
@@ -43,7 +44,16 @@ func ParseTemplate(hookTemplate string, cmd *cobra.Command) ([]string, error) {
|
||||
if n := strings.Count(out, "\n"); n > maxMessages {
|
||||
return nil, fmt.Errorf("hook template contains too many messages (%d): maximum is %d", n, maxMessages)
|
||||
}
|
||||
return strings.SplitN(out, "\n", maxMessages), nil
|
||||
lines := strings.SplitN(out, "\n", maxMessages)
|
||||
|
||||
// Skip messages containing only whitespace, but preserve empty lines
|
||||
// in messages that have content (e.g., for paragraph spacing).
|
||||
if !slices.ContainsFunc(lines, func(line string) bool {
|
||||
return strings.TrimSpace(line) != ""
|
||||
}) {
|
||||
return nil, nil
|
||||
}
|
||||
return lines, nil
|
||||
}
|
||||
|
||||
var ErrHookTemplateParse = errors.New("failed to parse hook template")
|
||||
|
||||
@@ -33,7 +33,17 @@ func TestParseTemplate(t *testing.T) {
|
||||
{
|
||||
doc: "empty template",
|
||||
template: "",
|
||||
expectedOutput: []string{""},
|
||||
expectedOutput: nil,
|
||||
},
|
||||
{
|
||||
doc: "only empty lines",
|
||||
template: "\n\n",
|
||||
expectedOutput: nil,
|
||||
},
|
||||
{
|
||||
doc: "whitespace-only lines",
|
||||
template: " \n\t\n ",
|
||||
expectedOutput: nil,
|
||||
},
|
||||
{
|
||||
doc: "plain message",
|
||||
|
||||
@@ -49,11 +49,18 @@ func RunPluginHooks(ctx context.Context, dockerCLI config.Provider, rootCmd, sub
|
||||
}
|
||||
|
||||
func runHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd, subCommand *cobra.Command, invokedCommand string, flags map[string]string, cmdErrorMessage string) {
|
||||
nextSteps := invokeAndCollectHooks(ctx, cfg, rootCmd, subCommand, invokedCommand, flags, cmdErrorMessage)
|
||||
hooks.PrintNextSteps(subCommand.ErrOrStderr(), nextSteps)
|
||||
messages := invokeAndCollectHooks(ctx, cfg, rootCmd, invokedCommand, flags, cmdErrorMessage)
|
||||
hooks.PrintMessages(subCommand.ErrOrStderr(), messages, func(message hooks.Response) []string {
|
||||
lines, err := hooks.ParseTemplate(message.Template, subCommand)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Debug("Failed to evaluate hook template")
|
||||
return nil
|
||||
}
|
||||
return lines
|
||||
})
|
||||
}
|
||||
|
||||
func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd, subCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []string {
|
||||
func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []hooks.Response {
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -64,17 +71,17 @@ func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, root
|
||||
}
|
||||
|
||||
pluginDirs := getPluginDirs(cfg)
|
||||
nextSteps := make([]string, 0, len(pluginsCfg))
|
||||
var collected []hooks.Response
|
||||
|
||||
tryInvokeHook := func(pluginName string, pluginCfg map[string]string) (messages []string, ok bool, err error) {
|
||||
tryInvokeHook := func(pluginName string, pluginCfg map[string]string) (hooks.Response, error) {
|
||||
match, matched := pluginMatch(pluginCfg, subCmdStr, cmdErrorMessage)
|
||||
if !matched {
|
||||
return nil, false, nil
|
||||
return hooks.Response{}, nil
|
||||
}
|
||||
|
||||
p, err := getPlugin(pluginName, pluginDirs, rootCmd)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
return hooks.Response{}, err
|
||||
}
|
||||
|
||||
resp, err := p.RunHook(ctx, hooks.Request{
|
||||
@@ -83,29 +90,23 @@ func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, root
|
||||
CommandError: cmdErrorMessage,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
return hooks.Response{}, err
|
||||
}
|
||||
|
||||
var message hooks.Response
|
||||
if err := json.Unmarshal(resp, &message); err != nil {
|
||||
return nil, false, fmt.Errorf("failed to unmarshal hook response (%q): %w", string(resp), err)
|
||||
return hooks.Response{}, fmt.Errorf("failed to unmarshal hook response (%q): %w", string(resp), err)
|
||||
}
|
||||
|
||||
// currently the only hook type
|
||||
if message.Type != hooks.NextSteps {
|
||||
return nil, false, errors.New("unexpected hook response type: " + strconv.Itoa(int(message.Type)))
|
||||
if message.Type != hooks.NextSteps && message.Type != hooks.GenericMessage {
|
||||
return hooks.Response{}, errors.New("unexpected hook response type: " + strconv.Itoa(int(message.Type)))
|
||||
}
|
||||
|
||||
messages, err = hooks.ParseTemplate(message.Template, subCmd)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
return messages, true, nil
|
||||
return message, nil
|
||||
}
|
||||
|
||||
for pluginName, pluginCfg := range pluginsCfg {
|
||||
messages, ok, err := tryInvokeHook(pluginName, pluginCfg)
|
||||
message, err := tryInvokeHook(pluginName, pluginCfg)
|
||||
if err != nil {
|
||||
// skip misbehaving plugins, but don't halt execution
|
||||
logrus.WithFields(logrus.Fields{
|
||||
@@ -114,38 +115,13 @@ func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, root
|
||||
}).Debug("Plugin hook invocation failed")
|
||||
continue
|
||||
}
|
||||
if !ok {
|
||||
if message.Template == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
var appended bool
|
||||
nextSteps, appended = appendNextSteps(nextSteps, messages)
|
||||
if !appended {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"plugin": pluginName,
|
||||
}).Debug("Plugin responded with an empty hook message; ignoring")
|
||||
}
|
||||
collected = append(collected, message)
|
||||
}
|
||||
return nextSteps
|
||||
}
|
||||
|
||||
// appendNextSteps appends the processed hook output to the nextSteps slice.
|
||||
// If the processed hook output is empty, it is not appended.
|
||||
// Empty lines are not stripped if there's at least one non-empty line.
|
||||
func appendNextSteps(nextSteps []string, processed []string) ([]string, bool) {
|
||||
empty := true
|
||||
for _, l := range processed {
|
||||
if strings.TrimSpace(l) != "" {
|
||||
empty = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if empty {
|
||||
return nextSteps, false
|
||||
}
|
||||
|
||||
return append(nextSteps, processed...), true
|
||||
return collected
|
||||
}
|
||||
|
||||
// pluginMatch takes a plugin configuration and a string representing the
|
||||
|
||||
@@ -2,6 +2,11 @@ package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
@@ -258,38 +263,6 @@ func TestMatchHookConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendNextSteps(t *testing.T) {
|
||||
testCases := []struct {
|
||||
processed []string
|
||||
expectedOut []string
|
||||
}{
|
||||
{
|
||||
processed: []string{},
|
||||
expectedOut: []string{},
|
||||
},
|
||||
{
|
||||
processed: []string{"", ""},
|
||||
expectedOut: []string{},
|
||||
},
|
||||
{
|
||||
processed: []string{"Some hint", "", "Some other hint"},
|
||||
expectedOut: []string{"Some hint", "", "Some other hint"},
|
||||
},
|
||||
{
|
||||
processed: []string{"Hint 1", "Hint 2"},
|
||||
expectedOut: []string{"Hint 1", "Hint 2"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
got, appended := appendNextSteps([]string{}, tc.processed)
|
||||
assert.Check(t, is.DeepEqual(got, tc.expectedOut))
|
||||
assert.Check(t, is.Equal(appended, len(got) > 0))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPluginHooksPassesErrorMessage(t *testing.T) {
|
||||
cfg := configfile.New("")
|
||||
cfg.Plugins = map[string]map[string]string{
|
||||
@@ -336,7 +309,7 @@ func TestInvokeAndCollectHooksErrorHooksSkippedOnSuccess(t *testing.T) {
|
||||
// On success, error-hooks should not match, so the plugin
|
||||
// binary is never looked up and no results are returned.
|
||||
result := invokeAndCollectHooks(
|
||||
context.Background(), cfg, root, sub,
|
||||
context.Background(), cfg, root,
|
||||
"build", map[string]string{}, "",
|
||||
)
|
||||
assert.Check(t, is.Len(result, 0))
|
||||
@@ -349,7 +322,7 @@ func TestInvokeAndCollectHooksNoPlugins(t *testing.T) {
|
||||
root.AddCommand(sub)
|
||||
|
||||
result := invokeAndCollectHooks(
|
||||
context.Background(), cfg, root, sub,
|
||||
context.Background(), cfg, root,
|
||||
"build", map[string]string{}, "some error",
|
||||
)
|
||||
assert.Check(t, is.Len(result, 0))
|
||||
@@ -368,8 +341,108 @@ func TestInvokeAndCollectHooksCancelledContext(t *testing.T) {
|
||||
cancel() // cancel immediately
|
||||
|
||||
result := invokeAndCollectHooks(
|
||||
ctx, cfg, root, sub,
|
||||
ctx, cfg, root,
|
||||
"build", map[string]string{}, "exit status 1",
|
||||
)
|
||||
assert.Check(t, is.Nil(result))
|
||||
}
|
||||
|
||||
func TestRunHooksMessages(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("test plugins require a POSIX shell")
|
||||
}
|
||||
|
||||
const header = "\n\x1b[1mWhat's next:\x1b[0m\n"
|
||||
for _, tc := range []struct {
|
||||
doc string
|
||||
responses map[string]string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
doc: "generic message preserves formatting and expands templates",
|
||||
responses: map[string]string{
|
||||
"status": `{"Type":1,"Template":"Status for {{command}}\n\n Details"}`,
|
||||
},
|
||||
expected: "\nStatus for build\n\n Details\n",
|
||||
},
|
||||
{
|
||||
doc: "omitted type defaults to next steps",
|
||||
responses: map[string]string{
|
||||
"suggestion": `{"Template":"Try another command"}`,
|
||||
},
|
||||
expected: header + " Try another command\n",
|
||||
},
|
||||
{
|
||||
doc: "generic messages precede shared next steps header",
|
||||
responses: map[string]string{
|
||||
"statusone": `{"Type":1,"Template":"Session active"}`,
|
||||
"statustwo": `{"Type":1,"Template":"Session active"}`,
|
||||
"suggestionone": `{"Type":0,"Template":"Try another command"}`,
|
||||
"suggestiontwo": `{"Type":0,"Template":"Try another command"}`,
|
||||
},
|
||||
expected: "\nSession active\nSession active\n" + header + " Try another command\n Try another command\n",
|
||||
},
|
||||
{
|
||||
doc: "empty responses produce no output",
|
||||
responses: map[string]string{
|
||||
"status": `{"Type":1,"Template":" \n\t"}`,
|
||||
"suggestion": `{"Type":0,"Template":""}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "empty generic message does not affect next steps",
|
||||
responses: map[string]string{
|
||||
"status": `{"Type":1,"Template":""}`,
|
||||
"suggestion": `{"Type":0,"Template":"Try another command"}`,
|
||||
},
|
||||
expected: header + " Try another command\n",
|
||||
},
|
||||
{
|
||||
doc: "invalid responses do not affect other plugins",
|
||||
responses: map[string]string{
|
||||
"unknown": `{"Type":99,"Template":"Ignored"}`,
|
||||
"invalid": `{"Type":1,"Template":"{{"}`,
|
||||
"status": `{"Type":1,"Template":"Session active"}`,
|
||||
"suggestion": `{"Type":0,"Template":"Try another command"}`,
|
||||
},
|
||||
expected: "\nSession active\n" + header + " Try another command\n",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
pluginDir := t.TempDir()
|
||||
cfg := configfile.New("")
|
||||
cfg.CLIPluginsExtraDirs = []string{pluginDir}
|
||||
cfg.Plugins = make(map[string]map[string]string)
|
||||
for name, response := range tc.responses {
|
||||
plugin := fmt.Sprintf(`#!/bin/sh
|
||||
case "$1" in
|
||||
docker-cli-plugin-metadata)
|
||||
printf '%%s\n' '{"SchemaVersion":"0.1.0","Vendor":"test"}'
|
||||
;;
|
||||
*)
|
||||
test "$2" = docker-cli-plugin-hooks || exit 1
|
||||
printf 'called\n' >> "$0.calls"
|
||||
printf '%%s\n' '%s'
|
||||
;;
|
||||
esac
|
||||
`, response)
|
||||
assert.NilError(t, os.WriteFile(filepath.Join(pluginDir, "docker-"+name), []byte(plugin), 0o755))
|
||||
cfg.Plugins[name] = map[string]string{"hooks": "build"}
|
||||
}
|
||||
|
||||
root := &cobra.Command{Use: "docker"}
|
||||
sub := &cobra.Command{Use: "build"}
|
||||
root.AddCommand(sub)
|
||||
var output strings.Builder
|
||||
sub.SetErr(&output)
|
||||
|
||||
runHooks(context.Background(), cfg, root, sub, "build", nil, "")
|
||||
assert.Equal(t, output.String(), tc.expected)
|
||||
for name := range tc.responses {
|
||||
calls, err := os.ReadFile(filepath.Join(pluginDir, "docker-"+name+".calls"))
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, string(calls), "called\n", "each plugin hook must run exactly once")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,8 +260,11 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions)
|
||||
contextDir = tempDir
|
||||
case build.ContextTypeRemote:
|
||||
buildCtx, relDockerfile, err = build.GetContextFromURL(progBuff, options.context, options.dockerfileName)
|
||||
if err != nil && options.quiet {
|
||||
_, _ = fmt.Fprintln(dockerCli.Err(), progBuff)
|
||||
if err != nil {
|
||||
if options.quiet {
|
||||
_, _ = fmt.Fprintln(dockerCli.Err(), progBuff)
|
||||
}
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unable to prepare context: path %q not found", options.context)
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
@@ -172,6 +174,35 @@ RUN echo hello world
|
||||
assert.DeepEqual(t, fakeBuild.filenames(t), []string{"Dockerfile"})
|
||||
}
|
||||
|
||||
// TestRunBuildRemoteContextDownloadError verifies that remote context
|
||||
// download errors are propagated.
|
||||
//
|
||||
// regression test for https://github.com/docker/cli/issues/7372
|
||||
func TestRunBuildRemoteContextDownloadError(t *testing.T) {
|
||||
t.Setenv("DOCKER_BUILDKIT", "0")
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
imageBuildCalled := false
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
imageBuildFunc: func(ctx context.Context, buildContext io.Reader, options client.ImageBuildOptions) (client.ImageBuildResult, error) {
|
||||
imageBuildCalled = true
|
||||
return client.ImageBuildResult{Body: io.NopCloser(bytes.NewReader(nil))}, nil
|
||||
},
|
||||
})
|
||||
|
||||
options := newBuildOptions()
|
||||
options.context = server.URL
|
||||
|
||||
err := runBuild(context.Background(), cli, options)
|
||||
|
||||
assert.ErrorContains(t, err, "unable to download remote context")
|
||||
assert.Assert(t, !imageBuildCalled, "ImageBuild should not be called when downloading the remote context fails")
|
||||
}
|
||||
|
||||
type fakeBuild struct {
|
||||
context *tar.Reader
|
||||
options client.ImageBuildOptions
|
||||
|
||||
+10
-10
@@ -5,13 +5,13 @@ go 1.26.0
|
||||
require (
|
||||
github.com/containerd/errdefs v1.0.0
|
||||
github.com/distribution/reference v0.6.0
|
||||
github.com/docker/cli v29.7.2+incompatible
|
||||
github.com/docker/cli v29.8.2+incompatible
|
||||
github.com/docker/cli-docs-tool v0.11.0
|
||||
github.com/docker/distribution v2.8.3+incompatible
|
||||
github.com/docker/go-connections v0.8.1
|
||||
github.com/docker/go-connections v0.8.2
|
||||
github.com/fvbommel/sortorder v1.2.0
|
||||
github.com/moby/moby/api v1.55.0
|
||||
github.com/moby/moby/client v0.5.1
|
||||
github.com/moby/moby/api v1.56.1
|
||||
github.com/moby/moby/client v0.6.2
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/sirupsen/logrus v1.10.2
|
||||
github.com/spf13/cobra v1.10.2
|
||||
@@ -22,7 +22,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
@@ -66,11 +66,11 @@ require (
|
||||
go.opentelemetry.io/otel/sdk/metric v1.46.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.46.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.56.0 // indirect
|
||||
golang.org/x/net v0.58.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/term v0.45.0 // indirect
|
||||
golang.org/x/text v0.41.0 // indirect
|
||||
golang.org/x/crypto v0.57.0 // indirect
|
||||
golang.org/x/net v0.60.0 // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
golang.org/x/term v0.46.0 // indirect
|
||||
golang.org/x/text v0.42.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect
|
||||
google.golang.org/grpc v1.83.2 // indirect
|
||||
|
||||
+20
-21
@@ -1,5 +1,5 @@
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4 h1:m2xcjRABtG288vnlV3U/PKh/HnPURxZp50RKNUwBxM8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4/go.mod h1:MTUE7ZuZO89FAtAwozGg6AWD83pcdefnEq5gF82MB8s=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
@@ -42,8 +42,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/cli v29.7.2+incompatible h1:dlkwallR8XqfeVnA2ELEhdwvb4lsSwuB4IgsG8Q9cLY=
|
||||
github.com/docker/cli v29.7.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v29.8.2+incompatible h1:2zgdFuoFst2T80oS42vhKGxkd0JRo305eIEKTsxR7pQ=
|
||||
github.com/docker/cli v29.8.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli-docs-tool v0.11.0 h1:7d8QARFb7QEobizqxmEM7fOteZEHwH/zWgHQtHZEcfE=
|
||||
github.com/docker/cli-docs-tool v0.11.0/go.mod h1:ma8BKiisUo8D6W05XEYIh3oa1UbgrZhi1nowyKFJa8Q=
|
||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
@@ -54,8 +54,8 @@ github.com/docker/docker-credential-helpers v0.9.9/go.mod h1:v1S+hepowrQXITkEfw6
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUExb29/M=
|
||||
github.com/docker/go-connections v0.8.1/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||
github.com/docker/go-connections v0.8.2 h1:9d9DEokx5W7qlr0shkFXIxEA7+TTD+llwzcykSlxMNc=
|
||||
github.com/docker/go-connections v0.8.2/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||
github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI=
|
||||
github.com/docker/go-metrics v0.1.0 h1:r76KPNpstz+IvQKSWpYegSkkyzex0V3A1ZGVx6bhGlY=
|
||||
github.com/docker/go-metrics v0.1.0/go.mod h1:PciI3sONtB051kXALN1JoIlpcu54E1FuPh+4DuqEzyw=
|
||||
@@ -133,10 +133,10 @@ github.com/mitchellh/mapstructure v1.0.0 h1:vVpGvMXJPqSDh2VYHF7gsfQj8Ncx+Xw5Y1KH
|
||||
github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc=
|
||||
github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
|
||||
github.com/moby/moby/client v0.5.1 h1:tYNaJno4c0HXz12y5BiqEDy0rVTYkWzI26lGvnTMiJw=
|
||||
github.com/moby/moby/client v0.5.1/go.mod h1:odLstlZ6uSnfvAgVxMpvgmb8SUdd+siH2T0GBuxVAlM=
|
||||
github.com/moby/moby/api v1.56.1 h1:PpWkvVPB7Fr/No8w+TfyJ/I6rWZ2YPZhT3JorIg+06c=
|
||||
github.com/moby/moby/api v1.56.1/go.mod h1:sZ+THbVWkjOmBPPfbnzdD/G1LuIexWhqlSHHPTDQ1Uk=
|
||||
github.com/moby/moby/client v0.6.2 h1:ekSPlMccrZGcw6Scb8T9D39qE0mB+XupU/yS0zNw9N4=
|
||||
github.com/moby/moby/client v0.6.2/go.mod h1:DGcnO7qmPJrqobx/ggQgQ/AUk+2wgVdQv7uxB0/mEsc=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8=
|
||||
@@ -236,27 +236,26 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
|
||||
golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
|
||||
golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
|
||||
golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
||||
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
|
||||
@@ -571,11 +571,15 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error {
|
||||
return nil
|
||||
}
|
||||
if !errdefs.IsNotFound(err) {
|
||||
// For plugin not found we fall through to
|
||||
// cmd.Execute() which deals with reporting
|
||||
// "command not found" in a consistent way.
|
||||
return err
|
||||
}
|
||||
if args[0] == "offload" {
|
||||
// Report this before cmd.ExecuteContext(): plugin-specific flags such as
|
||||
// --cpus otherwise fail parsing before the root command's RunE.
|
||||
return errors.New("docker: to use 'docker offload', install the plugin.\nSee https://docs.docker.com/go/cloud-flag/")
|
||||
}
|
||||
// Other missing or invalid plugins fall through to cmd.ExecuteContext(),
|
||||
// which reports the standard unknown-command error.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.26.8
|
||||
ARG GO_VERSION=1.26.10
|
||||
|
||||
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
|
||||
# It must be a supported tag in the docker.io/library/alpine image repository
|
||||
@@ -10,7 +10,7 @@ ARG ALPINE_VERSION=3.23
|
||||
# BUILDX_VERSION sets the version of buildx to install in the dev container.
|
||||
# It must be a valid tag in the docker.io/docker/buildx-bin image repository
|
||||
# on Docker Hub.
|
||||
ARG BUILDX_VERSION=0.34.1
|
||||
ARG BUILDX_VERSION=0.38.0
|
||||
FROM docker/buildx-bin:${BUILDX_VERSION} AS buildx
|
||||
|
||||
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS golang
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.26.8
|
||||
ARG GO_VERSION=1.26.10
|
||||
|
||||
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
|
||||
# It must be a supported tag in the docker.io/library/alpine image repository
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.26.8
|
||||
ARG GO_VERSION=1.26.10
|
||||
|
||||
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
|
||||
# It must be a supported tag in the docker.io/library/alpine image repository
|
||||
|
||||
+6
-6
@@ -22,7 +22,7 @@ require (
|
||||
github.com/docker/cli-docs-tool v0.11.0
|
||||
github.com/docker/distribution v2.8.3+incompatible
|
||||
github.com/docker/docker-credential-helpers v0.9.9
|
||||
github.com/docker/go-connections v0.8.1
|
||||
github.com/docker/go-connections v0.8.2
|
||||
github.com/docker/go-units v0.5.0
|
||||
github.com/fvbommel/sortorder v1.2.0
|
||||
github.com/go-jose/go-jose/v4 v4.1.5
|
||||
@@ -31,10 +31,10 @@ require (
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/mattn/go-runewidth v0.0.30
|
||||
github.com/mattn/go-runewidth v0.0.31
|
||||
github.com/moby/go-archive v0.3.3
|
||||
github.com/moby/moby/api v1.56.1
|
||||
github.com/moby/moby/client v0.6.1
|
||||
github.com/moby/moby/client v0.6.2
|
||||
github.com/moby/patternmatcher v0.6.1
|
||||
github.com/moby/swarmkit/v2 v2.1.2
|
||||
github.com/moby/sys/atomicwriter v0.1.0
|
||||
@@ -70,8 +70,8 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20260917205352-e937bb47801a // indirect
|
||||
github.com/Microsoft/go-winio v0.6.3-0.20260930231756-f19d9717deb0 // indirect; see https://github.com/microsoft/hcsshim/pull/2545
|
||||
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.3 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
@@ -102,7 +102,7 @@ require (
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.11.0 // indirect
|
||||
golang.org/x/mod v0.41.0 // indirect
|
||||
golang.org/x/net v0.59.0 // indirect
|
||||
golang.org/x/net v0.60.0 // indirect
|
||||
golang.org/x/time v0.16.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect
|
||||
|
||||
+12
-13
@@ -2,10 +2,10 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20260917205352-e937bb47801a h1:4hxax9ktNjSDoFn1tZSeL6gXMTRMpO0FzjdKfT01jgY=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20260917205352-e937bb47801a/go.mod h1:3EWSSOZ50kb+arhww0qIaEXHToib/3FjBj9Jj5lcP5g=
|
||||
github.com/Microsoft/go-winio v0.6.3-0.20260930231756-f19d9717deb0 h1:71VU8kW/LCxJkx41+YYys1EgQO+AUnPa1TrxTSyYUCU=
|
||||
github.com/Microsoft/go-winio v0.6.3-0.20260930231756-f19d9717deb0/go.mod h1:ma0QpxizD4fyyzJ1C7MIblymTD2Nxz5Mho/3CepwIZg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4 h1:m2xcjRABtG288vnlV3U/PKh/HnPURxZp50RKNUwBxM8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4/go.mod h1:MTUE7ZuZO89FAtAwozGg6AWD83pcdefnEq5gF82MB8s=
|
||||
github.com/Microsoft/go-winio v0.6.3 h1:M/52tsoyTfwaxajLnXZm7PqkbMGRH8iSWP+8fcg9MQ4=
|
||||
github.com/Microsoft/go-winio v0.6.3/go.mod h1:ma0QpxizD4fyyzJ1C7MIblymTD2Nxz5Mho/3CepwIZg=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
@@ -37,8 +37,8 @@ github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBi
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker-credential-helpers v0.9.9 h1:BkydjIgZ46JnDbqyM2p2fc63KMw6y+KHL3Em/2AGJ7w=
|
||||
github.com/docker/docker-credential-helpers v0.9.9/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
|
||||
github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUExb29/M=
|
||||
github.com/docker/go-connections v0.8.1/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||
github.com/docker/go-connections v0.8.2 h1:9d9DEokx5W7qlr0shkFXIxEA7+TTD+llwzcykSlxMNc=
|
||||
github.com/docker/go-connections v0.8.2/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||
github.com/docker/go-events v0.1.0 h1:J8VX4H7Ta8mmBhv/K/24x1kosKOrEEnYmGjcdFHTOW0=
|
||||
github.com/docker/go-events v0.1.0/go.mod h1:jwVwMgySJX4PiukIIRrind1wPGU8QhPa8aTEPkreUj4=
|
||||
github.com/docker/go-metrics v0.1.0 h1:r76KPNpstz+IvQKSWpYegSkkyzex0V3A1ZGVx6bhGlY=
|
||||
@@ -82,16 +82,16 @@ github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLM
|
||||
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/mattn/go-runewidth v0.0.30 h1:+KUuiDA4fF0R1p5FeueHefjDm+GIM+kWfFnDjybOPgk=
|
||||
github.com/mattn/go-runewidth v0.0.30/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
|
||||
github.com/mattn/go-runewidth v0.0.31 h1:1oCV4ReTlosDBc6rYRw5cL8S/4y8iv5oRl4SJ5IBgwo=
|
||||
github.com/mattn/go-runewidth v0.0.31/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/go-archive v0.3.3 h1:OxxR9paxsluYi+zDUEXTTaIxtkK3viymW+Ka7vRhhME=
|
||||
github.com/moby/go-archive v0.3.3/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE=
|
||||
github.com/moby/moby/api v1.56.1 h1:PpWkvVPB7Fr/No8w+TfyJ/I6rWZ2YPZhT3JorIg+06c=
|
||||
github.com/moby/moby/api v1.56.1/go.mod h1:sZ+THbVWkjOmBPPfbnzdD/G1LuIexWhqlSHHPTDQ1Uk=
|
||||
github.com/moby/moby/client v0.6.1 h1:gvfKmdcWw+9MzXYP0vAUKQtBTGu1L8475K2nsmL7O98=
|
||||
github.com/moby/moby/client v0.6.1/go.mod h1:XHgTFqz9NCgS/VuoxXMmEDVmVgXQ4vFEc15wsCIwb24=
|
||||
github.com/moby/moby/client v0.6.2 h1:ekSPlMccrZGcw6Scb8T9D39qE0mB+XupU/yS0zNw9N4=
|
||||
github.com/moby/moby/client v0.6.2/go.mod h1:DGcnO7qmPJrqobx/ggQgQ/AUk+2wgVdQv7uxB0/mEsc=
|
||||
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
|
||||
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||
github.com/moby/swarmkit/v2 v2.1.2 h1:1WDZAI6HVYNKdCG4zlXnTAPyLsLwuhRGWlHoOUf5Z6I=
|
||||
@@ -193,8 +193,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
|
||||
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||
golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
|
||||
golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -203,7 +203,6 @@ golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
|
||||
+25
-7
@@ -3,8 +3,9 @@
|
||||
package winio
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -20,16 +21,33 @@ import (
|
||||
//sys setFileCompletionNotificationModes(h windows.Handle, flags uint8) (err error) = SetFileCompletionNotificationModes
|
||||
//sys wsaGetOverlappedResult(h windows.Handle, o *windows.Overlapped, bytes *uint32, wait bool, flags *uint32) (err error) = ws2_32.WSAGetOverlappedResult
|
||||
|
||||
var (
|
||||
ErrFileClosed = errors.New("file has already been closed")
|
||||
ErrTimeout = &timeoutError{}
|
||||
)
|
||||
// ErrFileClosed is returned when an operation is performed on a closed file.
|
||||
//
|
||||
// Deprecated: use [os.ErrClosed] instead.
|
||||
var ErrFileClosed = os.ErrClosed
|
||||
|
||||
// ErrTimeout is returned when an I/O operation exceeds its deadline.
|
||||
//
|
||||
// It matches both [context.DeadlineExceeded] and [os.ErrDeadlineExceeded]
|
||||
// with [errors.Is].
|
||||
var ErrTimeout error = &timeoutError{}
|
||||
|
||||
// timeoutError preserves the historical "i/o timeout" error while matching
|
||||
// both context.DeadlineExceeded and os.ErrDeadlineExceeded through errors.Is.
|
||||
// This mirrors the behavior of net.errTimeout:
|
||||
// https://github.com/golang/go/blob/go1.27.1/src/net/net.go
|
||||
//
|
||||
// TODO: consider replacing ErrTimeout with os.ErrDeadlineExceeded if os.ErrDeadlineExceeded is changed to match context.DeadlineExceeded.
|
||||
type timeoutError struct{}
|
||||
|
||||
func (*timeoutError) Error() string { return "i/o timeout" }
|
||||
func (*timeoutError) Timeout() bool { return true }
|
||||
func (*timeoutError) Temporary() bool { return true }
|
||||
func (*timeoutError) Unwrap() error { return os.ErrDeadlineExceeded }
|
||||
|
||||
func (*timeoutError) Is(err error) bool {
|
||||
return err == context.DeadlineExceeded
|
||||
}
|
||||
|
||||
type timeoutChan chan struct{}
|
||||
|
||||
@@ -143,7 +161,7 @@ func (f *win32File) prepareIO() (*ioOperation, error) {
|
||||
f.wgLock.RLock()
|
||||
if f.closing.Load() {
|
||||
f.wgLock.RUnlock()
|
||||
return nil, ErrFileClosed
|
||||
return nil, os.ErrClosed
|
||||
}
|
||||
f.wg.Add(1)
|
||||
f.wgLock.RUnlock()
|
||||
@@ -192,7 +210,7 @@ func (f *win32File) asyncIO(c *ioOperation, d *deadlineHandler, bytes uint32, er
|
||||
err = r.err
|
||||
if err == windows.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno
|
||||
if f.closing.Load() {
|
||||
err = ErrFileClosed
|
||||
err = os.ErrClosed
|
||||
}
|
||||
} else if err != nil && f.socket {
|
||||
// err is from Win32. Query the overlapped structure to get the winsock error.
|
||||
|
||||
+1
-1
@@ -441,7 +441,7 @@ func canRedial(err error) bool {
|
||||
|
||||
func (conn *HvsockConn) opErr(op string, err error) error {
|
||||
// translate from "file closed" to "socket closed"
|
||||
if errors.Is(err, ErrFileClosed) {
|
||||
if errors.Is(err, os.ErrClosed) {
|
||||
err = socket.ErrSocketClosed
|
||||
}
|
||||
return &net.OpError{Op: op, Net: "hvsock", Source: &conn.local, Addr: &conn.remote, Err: err}
|
||||
|
||||
+11
-12
@@ -102,12 +102,10 @@ func (status ntStatus) Err() error {
|
||||
return rtlNtStatusToDosError(status)
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrPipeListenerClosed is returned for pipe operations on listeners that have been closed.
|
||||
ErrPipeListenerClosed = net.ErrClosed
|
||||
|
||||
errPipeWriteClosed = errors.New("pipe has been closed for write")
|
||||
)
|
||||
// ErrPipeListenerClosed is returned for pipe operations on listeners that have been closed.
|
||||
//
|
||||
// Deprecated: use [net.ErrClosed] instead.
|
||||
var ErrPipeListenerClosed = net.ErrClosed
|
||||
|
||||
type win32Pipe struct {
|
||||
*win32File
|
||||
@@ -144,9 +142,10 @@ func (f *win32Pipe) Disconnect() error {
|
||||
}
|
||||
|
||||
// CloseWrite closes the write side of a message pipe in byte mode.
|
||||
// It returns [io.ErrClosedPipe] if the write side is already closed.
|
||||
func (f *win32MessageBytePipe) CloseWrite() error {
|
||||
if f.writeClosed {
|
||||
return errPipeWriteClosed
|
||||
return io.ErrClosedPipe
|
||||
}
|
||||
err := f.win32File.Flush()
|
||||
if err != nil {
|
||||
@@ -161,10 +160,10 @@ func (f *win32MessageBytePipe) CloseWrite() error {
|
||||
}
|
||||
|
||||
// Write writes bytes to a message pipe in byte mode. Zero-byte writes are ignored, since
|
||||
// they are used to implement CloseWrite().
|
||||
// they are used to implement CloseWrite. It returns [io.ErrClosedPipe] if the write side is closed.
|
||||
func (f *win32MessageBytePipe) Write(b []byte) (int, error) {
|
||||
if f.writeClosed {
|
||||
return 0, errPipeWriteClosed
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return 0, nil
|
||||
@@ -454,7 +453,7 @@ func (l *win32PipeListener) makeConnectedServerPipe() (*win32File, error) {
|
||||
_ = p.Close()
|
||||
p = nil
|
||||
<-ch
|
||||
err = ErrPipeListenerClosed
|
||||
err = net.ErrClosed
|
||||
}
|
||||
return p, err
|
||||
}
|
||||
@@ -479,7 +478,7 @@ func (l *win32PipeListener) listenerRoutine() {
|
||||
}
|
||||
}
|
||||
responseCh <- acceptResponse{p, err}
|
||||
closed = err == ErrPipeListenerClosed //nolint:errorlint // err is Errno
|
||||
closed = err == net.ErrClosed //nolint:errorlint // err is Errno
|
||||
}
|
||||
}
|
||||
windows.Close(l.firstHandle)
|
||||
@@ -571,7 +570,7 @@ func (l *win32PipeListener) Accept() (net.Conn, error) {
|
||||
}
|
||||
return &win32Pipe{win32File: response.f, path: l.path}, nil
|
||||
case <-l.doneCh:
|
||||
return nil, ErrPipeListenerClosed
|
||||
return nil, net.ErrClosed
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-1
@@ -9,13 +9,17 @@ import (
|
||||
// NewTCPSocket creates a TCP socket listener with the specified address and
|
||||
// the specified tls configuration. If TLSConfig is set, will encapsulate the
|
||||
// TCP listener inside a TLS one.
|
||||
// If no ALPN protocols are configured, it defaults to HTTP/1.1.
|
||||
func NewTCPSocket(addr string, tlsConfig *tls.Config) (net.Listener, error) {
|
||||
l, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tlsConfig != nil {
|
||||
tlsConfig.NextProtos = []string{"http/1.1"}
|
||||
if len(tlsConfig.NextProtos) == 0 {
|
||||
tlsConfig = tlsConfig.Clone()
|
||||
tlsConfig.NextProtos = []string{"http/1.1"}
|
||||
}
|
||||
l = tls.NewListener(l, tlsConfig)
|
||||
}
|
||||
return l, nil
|
||||
|
||||
+5
@@ -15,6 +15,11 @@ Usage
|
||||
runewidth.StringWidth("つのだ☆HIRO") == 12
|
||||
```
|
||||
|
||||
NOTE: Latin letters that Unicode gives ambiguous East Asian width, such as
|
||||
`ü`, `é` and `ß`, are measured as one cell even when `EastAsianWidth` is
|
||||
set, as terminals draw them in CJK locales too. Other ambiguous characters
|
||||
such as `±`, `×` and `○` still take two cells there.
|
||||
|
||||
|
||||
Author
|
||||
------
|
||||
|
||||
+100
-22
@@ -361,9 +361,11 @@ var private = table{
|
||||
|
||||
var nonprint = table{
|
||||
{0x0000, 0x001F}, {0x007F, 0x009F}, {0x00AD, 0x00AD},
|
||||
{0x070F, 0x070F}, {0x180B, 0x180E}, {0x200B, 0x200F},
|
||||
{0x2028, 0x202E}, {0x206A, 0x206F}, {0xD800, 0xDFFF},
|
||||
{0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFB}, {0xFFFE, 0xFFFF},
|
||||
{0x061C, 0x061C}, {0x070F, 0x070F}, {0x180B, 0x180E},
|
||||
{0x200B, 0x200F}, {0x2028, 0x202E}, {0x2060, 0x2064},
|
||||
{0x2066, 0x206F}, {0xD800, 0xDFFF}, {0xFEFF, 0xFEFF},
|
||||
{0xFFF9, 0xFFFB}, {0xFFFE, 0xFFFF}, {0x13430, 0x1343F},
|
||||
{0x1BCA0, 0x1BCA3}, {0x1D173, 0x1D17A}, {0xE0001, 0xE0001},
|
||||
}
|
||||
|
||||
// Condition have flag EastAsianWidth whether the current locale is CJK or not.
|
||||
@@ -514,19 +516,77 @@ func isJoiner(r rune) bool {
|
||||
return inTable(r, joiner)
|
||||
}
|
||||
|
||||
// clusterText returns s to segment in place of s. When s ends in a lead
|
||||
// byte short of the bytes it promises, the segmenter gives up there and
|
||||
// glues everything from it onto the cluster before, where the two-cell cap
|
||||
// hides it, although the same bytes followed by anything else form
|
||||
// clusters of their own. Such a string gets NULs after it, enough for any
|
||||
// lead byte to be decoded; as controls they are clusters of their own, and
|
||||
// the loops over the clusters stop at len(s) before reaching them.
|
||||
func clusterText(s string) string {
|
||||
if endsShort(s) {
|
||||
return s + "\x00\x00\x00"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// endsShort reports whether one of the last three bytes of s is a lead
|
||||
// byte that needs more bytes than are left, which is where the segmenter
|
||||
// stops decoding. It only looks at the length the lead byte announces, not
|
||||
// at whether the bytes after it could continue it, as the segmenter does.
|
||||
func endsShort(s string) bool {
|
||||
for i := max(len(s)-3, 0); i < len(s); i++ {
|
||||
b, left := s[i], len(s)-i
|
||||
if b >= 0xC2 && b < 0xE0 && left < 2 ||
|
||||
b >= 0xE0 && b < 0xF0 && left < 3 ||
|
||||
b >= 0xF0 && b < 0xF8 && left < 4 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// graphemeWidth returns the width of a single grapheme cluster: the sum of
|
||||
// the widths of its runes, capped at 2 cells. The cap keeps multi-rune
|
||||
// sequences that render as a single glyph (ZWJ emoji, flags, Hangul jamo)
|
||||
// from being counted wider than the two cells terminals give them.
|
||||
// the widths of its runes, with the part from the first rune that starts a
|
||||
// single glyph (an emoji, a regional indicator, a Hangul jamo or syllable)
|
||||
// capped at 2 cells. The cap keeps multi-rune sequences that render as one
|
||||
// glyph (ZWJ emoji, skin tones, flags, Hangul jamo) from being counted
|
||||
// wider than the two cells terminals give them. What comes before it, such
|
||||
// as a Prepend rune, and a cluster with no such rune, like a letter with a
|
||||
// skin tone modifier after it, are shown rune by rune and summed.
|
||||
func (c *Condition) graphemeWidth(cluster string) int {
|
||||
width := 0
|
||||
if r, size := utf8.DecodeRuneInString(cluster); size == len(cluster) {
|
||||
return c.RuneWidth(r) // one rune, never over the cap
|
||||
}
|
||||
width, glyph := 0, -1
|
||||
for _, r := range cluster {
|
||||
width += c.RuneWidth(r)
|
||||
w := c.RuneWidth(r)
|
||||
if glyph < 0 && r >= 0x1100 && startsGlyph(r) {
|
||||
glyph = 0
|
||||
}
|
||||
if glyph >= 0 {
|
||||
glyph += w
|
||||
} else {
|
||||
width += w
|
||||
}
|
||||
}
|
||||
if width > 2 {
|
||||
width = 2
|
||||
return width + min(max(glyph, 0), 2)
|
||||
}
|
||||
|
||||
// startsGlyph reports whether r begins a sequence that a terminal draws as
|
||||
// a single glyph however many runes it has.
|
||||
func startsGlyph(r rune) bool {
|
||||
switch {
|
||||
case 0x1100 <= r && r <= 0x11FF, // Hangul jamo
|
||||
0xA960 <= r && r <= 0xA97F, // Hangul jamo extended-A
|
||||
0xAC00 <= r && r <= 0xD7FF, // Hangul syllables, jamo extended-B
|
||||
0x1F1E6 <= r && r <= 0x1F1FF: // regional indicators
|
||||
return true
|
||||
case r < 0x203C, 0x3299 < r && r < 0x1F004:
|
||||
// Outside the emoji table: spares CJK text the search.
|
||||
return false
|
||||
}
|
||||
return width
|
||||
return inTable(r, emoji)
|
||||
}
|
||||
|
||||
// StringWidth return width as you can see
|
||||
@@ -536,6 +596,11 @@ func (c *Condition) StringWidth(s string) (width int) {
|
||||
if b < 0x20 || b == 0x7F {
|
||||
return 0
|
||||
}
|
||||
if b >= 0x80 {
|
||||
// Not UTF-8 on its own: measure it as the U+FFFD every other
|
||||
// path decodes it to, which is ambiguous width.
|
||||
return c.RuneWidth(utf8.RuneError)
|
||||
}
|
||||
return 1
|
||||
}
|
||||
if len(s) > 0 && len(s) <= utf8.UTFMax {
|
||||
@@ -565,8 +630,8 @@ graphemes:
|
||||
return
|
||||
}
|
||||
width = 0
|
||||
g := graphemes.FromString(s)
|
||||
for g.Next() {
|
||||
g := graphemes.FromString(clusterText(s))
|
||||
for g.Next() && g.Start() < len(s) {
|
||||
width += c.graphemeWidth(g.Value())
|
||||
}
|
||||
return
|
||||
@@ -597,8 +662,8 @@ func (c *Condition) Truncate(s string, w int, tail string) string {
|
||||
}
|
||||
var width int
|
||||
pos := len(s)
|
||||
g := graphemes.FromString(s)
|
||||
for g.Next() {
|
||||
g := graphemes.FromString(clusterText(s))
|
||||
for g.Next() && g.Start() < len(s) {
|
||||
chWidth := c.graphemeWidth(g.Value())
|
||||
if width+chWidth > w {
|
||||
pos = g.Start()
|
||||
@@ -651,8 +716,8 @@ func (c *Condition) TruncateLeft(s string, w int, prefix string) string {
|
||||
var width int
|
||||
pos := len(s)
|
||||
|
||||
g := graphemes.FromString(s)
|
||||
for g.Next() {
|
||||
g := graphemes.FromString(clusterText(s))
|
||||
for g.Next() && g.Start() < len(s) {
|
||||
chWidth := c.graphemeWidth(g.Value())
|
||||
|
||||
if width+chWidth > w {
|
||||
@@ -714,8 +779,8 @@ func (c *Condition) TruncatePrefix(s string, w int, prefix string) string {
|
||||
}
|
||||
var width int
|
||||
var pos int
|
||||
g := graphemes.FromString(s)
|
||||
for g.Next() {
|
||||
g := graphemes.FromString(clusterText(s))
|
||||
for g.Next() && g.Start() < len(s) {
|
||||
chWidth := c.graphemeWidth(g.Value())
|
||||
if sw-(width+chWidth) <= w {
|
||||
pos = g.End()
|
||||
@@ -791,6 +856,14 @@ func (c *Condition) Wrap(s string, w int) string {
|
||||
width = 0
|
||||
continue
|
||||
}
|
||||
// CRLF is one cluster that ends the line, as on the
|
||||
// segmenter, so it is never broken before.
|
||||
if b == '\r' && i+1 < len(s) && s[i+1] == '\n' {
|
||||
out.WriteString("\r\n")
|
||||
width = 0
|
||||
i++
|
||||
continue
|
||||
}
|
||||
// Same rule as the StringWidth fast path: no ASCII byte is
|
||||
// wide or ambiguous, so the flags in c cannot change this.
|
||||
cw := 0
|
||||
@@ -815,8 +888,8 @@ func (c *Condition) Wrap(s string, w int) string {
|
||||
width := 0
|
||||
var out strings.Builder
|
||||
out.Grow(len(s) + len(s)/max(w, 1) + 1)
|
||||
g := graphemes.FromString(s)
|
||||
for g.Next() {
|
||||
g := graphemes.FromString(clusterText(s))
|
||||
for g.Next() && g.Start() < len(s) {
|
||||
cluster := g.Value()
|
||||
// LF and CRLF are each a single cluster
|
||||
if strings.HasSuffix(cluster, "\n") {
|
||||
@@ -862,8 +935,13 @@ func RuneWidth(r rune) int {
|
||||
}
|
||||
|
||||
// IsAmbiguousWidth returns whether is ambiguous width or not.
|
||||
//
|
||||
// NOTE: The Latin letters of U+0080..U+017F that Unicode gives ambiguous
|
||||
// width (ü, é, ß, æ, ø and so on) are reported as ambiguous here, but
|
||||
// RuneWidth measures them as one cell even with EastAsianWidth, since
|
||||
// terminals draw them narrow in CJK locales too.
|
||||
func IsAmbiguousWidth(r rune) bool {
|
||||
return inTable(r, private) || inTable(r, ambiguous)
|
||||
return inTable(r, private) || inTable(r, ambiguous) || inTable(r, ambiguousLatin)
|
||||
}
|
||||
|
||||
// IsCombiningWidth returns whether is combining width or not.
|
||||
|
||||
+35
-8
@@ -14,9 +14,11 @@ func mblen(charset string) int {
|
||||
return 6
|
||||
case "jis":
|
||||
return 8
|
||||
case "euctw", "gb18030":
|
||||
return 4
|
||||
case "eucjp":
|
||||
return 3
|
||||
case "euckr", "euccn", "sjis", "cp932", "cp51932", "cp936", "cp949", "cp950", "big5", "gbk", "gb2312":
|
||||
case "euckr", "euccn", "sjis", "shiftjis", "cp932", "cp51932", "cp936", "cp949", "cp950", "big5", "big5hkscs", "gbk", "gb2312":
|
||||
return 2
|
||||
}
|
||||
return 1
|
||||
@@ -45,9 +47,35 @@ func localeCharset(locale string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// isCJKLanguage reports whether the language code at the start of locale is
|
||||
// Japanese, Korean or Chinese.
|
||||
func isCJKLanguage(locale string) bool {
|
||||
n := 0
|
||||
for n < len(locale) && locale[n] >= 'a' && locale[n] <= 'z' {
|
||||
n++
|
||||
}
|
||||
switch locale[:n] {
|
||||
case "ja", "ko", "zh":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// normalizeCharset drops the hyphens and underscores that charset names are
|
||||
// spelled with or without, so that "EUC-JP" and "eucjp" compare equal.
|
||||
func normalizeCharset(charset string) string {
|
||||
return strings.Map(func(r rune) rune {
|
||||
if r == '-' || r == '_' {
|
||||
return -1
|
||||
}
|
||||
return r
|
||||
}, charset)
|
||||
}
|
||||
|
||||
func isEastAsian(locale string) bool {
|
||||
charset := strings.ToLower(locale)
|
||||
if cs := localeCharset(locale); cs != "" {
|
||||
cs := localeCharset(locale)
|
||||
if cs != "" {
|
||||
charset = strings.ToLower(cs)
|
||||
}
|
||||
|
||||
@@ -61,14 +89,13 @@ func isEastAsian(locale string) bool {
|
||||
break
|
||||
}
|
||||
}
|
||||
max := mblen(charset)
|
||||
if max > 1 && (charset[0] != 'u' ||
|
||||
strings.HasPrefix(locale, "ja") ||
|
||||
strings.HasPrefix(locale, "ko") ||
|
||||
strings.HasPrefix(locale, "zh")) {
|
||||
cjk := isCJKLanguage(locale)
|
||||
if cs == "" && cjk {
|
||||
// A CJK locale without a charset, such as "ja_JP" on macOS.
|
||||
return true
|
||||
}
|
||||
return false
|
||||
max := mblen(normalizeCharset(charset))
|
||||
return max > 1 && (charset[0] != 'u' || cjk)
|
||||
}
|
||||
|
||||
// IsEastAsian return true if the current locale is CJK
|
||||
|
||||
+58
-52
@@ -173,65 +173,71 @@ var doublewidth = table{
|
||||
|
||||
var ambiguous = table{
|
||||
{0x00A1, 0x00A1}, {0x00A4, 0x00A4}, {0x00A7, 0x00A8},
|
||||
{0x00AA, 0x00AA}, {0x00AD, 0x00AE}, {0x00B0, 0x00B4},
|
||||
{0x00B6, 0x00BA}, {0x00BC, 0x00BF}, {0x00C6, 0x00C6},
|
||||
{0x00D0, 0x00D0}, {0x00D7, 0x00D8}, {0x00DE, 0x00E1},
|
||||
{0x00AD, 0x00AE}, {0x00B0, 0x00B4}, {0x00B6, 0x00B9},
|
||||
{0x00BC, 0x00BF}, {0x00D7, 0x00D7}, {0x00F7, 0x00F7},
|
||||
{0x01CE, 0x01CE}, {0x01D0, 0x01D0}, {0x01D2, 0x01D2},
|
||||
{0x01D4, 0x01D4}, {0x01D6, 0x01D6}, {0x01D8, 0x01D8},
|
||||
{0x01DA, 0x01DA}, {0x01DC, 0x01DC}, {0x0251, 0x0251},
|
||||
{0x0261, 0x0261}, {0x02C4, 0x02C4}, {0x02C7, 0x02C7},
|
||||
{0x02C9, 0x02CB}, {0x02CD, 0x02CD}, {0x02D0, 0x02D0},
|
||||
{0x02D8, 0x02DB}, {0x02DD, 0x02DD}, {0x02DF, 0x02DF},
|
||||
{0x0300, 0x036F}, {0x0391, 0x03A1}, {0x03A3, 0x03A9},
|
||||
{0x03B1, 0x03C1}, {0x03C3, 0x03C9}, {0x0401, 0x0401},
|
||||
{0x0410, 0x044F}, {0x0451, 0x0451}, {0x2010, 0x2010},
|
||||
{0x2013, 0x2016}, {0x2018, 0x2019}, {0x201C, 0x201D},
|
||||
{0x2020, 0x2022}, {0x2024, 0x2027}, {0x2030, 0x2030},
|
||||
{0x2032, 0x2033}, {0x2035, 0x2035}, {0x203B, 0x203B},
|
||||
{0x203E, 0x203E}, {0x2074, 0x2074}, {0x207F, 0x207F},
|
||||
{0x2081, 0x2084}, {0x20AC, 0x20AC}, {0x2103, 0x2103},
|
||||
{0x2105, 0x2105}, {0x2109, 0x2109}, {0x2113, 0x2113},
|
||||
{0x2116, 0x2116}, {0x2121, 0x2122}, {0x2126, 0x2126},
|
||||
{0x212B, 0x212B}, {0x2153, 0x2154}, {0x215B, 0x215E},
|
||||
{0x2160, 0x216B}, {0x2170, 0x2179}, {0x2189, 0x2189},
|
||||
{0x2190, 0x2199}, {0x21B8, 0x21B9}, {0x21D2, 0x21D2},
|
||||
{0x21D4, 0x21D4}, {0x21E7, 0x21E7}, {0x2200, 0x2200},
|
||||
{0x2202, 0x2203}, {0x2207, 0x2208}, {0x220B, 0x220B},
|
||||
{0x220F, 0x220F}, {0x2211, 0x2211}, {0x2215, 0x2215},
|
||||
{0x221A, 0x221A}, {0x221D, 0x2220}, {0x2223, 0x2223},
|
||||
{0x2225, 0x2225}, {0x2227, 0x222C}, {0x222E, 0x222E},
|
||||
{0x2234, 0x2237}, {0x223C, 0x223D}, {0x2248, 0x2248},
|
||||
{0x224C, 0x224C}, {0x2252, 0x2252}, {0x2260, 0x2261},
|
||||
{0x2264, 0x2267}, {0x226A, 0x226B}, {0x226E, 0x226F},
|
||||
{0x2282, 0x2283}, {0x2286, 0x2287}, {0x2295, 0x2295},
|
||||
{0x2299, 0x2299}, {0x22A5, 0x22A5}, {0x22BF, 0x22BF},
|
||||
{0x2312, 0x2312}, {0x2460, 0x24E9}, {0x24EB, 0x254B},
|
||||
{0x2550, 0x2573}, {0x2580, 0x258F}, {0x2592, 0x2595},
|
||||
{0x25A0, 0x25A1}, {0x25A3, 0x25A9}, {0x25B2, 0x25B3},
|
||||
{0x25B6, 0x25B7}, {0x25BC, 0x25BD}, {0x25C0, 0x25C1},
|
||||
{0x25C6, 0x25C8}, {0x25CB, 0x25CB}, {0x25CE, 0x25D1},
|
||||
{0x25E2, 0x25E5}, {0x25EF, 0x25EF}, {0x2605, 0x2606},
|
||||
{0x2609, 0x2609}, {0x260E, 0x260F}, {0x261C, 0x261C},
|
||||
{0x261E, 0x261E}, {0x2640, 0x2640}, {0x2642, 0x2642},
|
||||
{0x2660, 0x2661}, {0x2663, 0x2665}, {0x2667, 0x266A},
|
||||
{0x266C, 0x266D}, {0x266F, 0x266F}, {0x269E, 0x269F},
|
||||
{0x26BF, 0x26BF}, {0x26C6, 0x26CD}, {0x26CF, 0x26D3},
|
||||
{0x26D5, 0x26E1}, {0x26E3, 0x26E3}, {0x26E8, 0x26E9},
|
||||
{0x26EB, 0x26F1}, {0x26F4, 0x26F4}, {0x26F6, 0x26F9},
|
||||
{0x26FB, 0x26FC}, {0x26FE, 0x26FF}, {0x273D, 0x273D},
|
||||
{0x2776, 0x277F}, {0x2B56, 0x2B59}, {0x3248, 0x324F},
|
||||
{0xE000, 0xF8FF}, {0xFFFD, 0xFFFD}, {0x1F100, 0x1F10A},
|
||||
{0x1F110, 0x1F12D}, {0x1F130, 0x1F169}, {0x1F170, 0x1F18D},
|
||||
{0x1F18F, 0x1F190}, {0x1F19B, 0x1F1AC}, {0xF0000, 0xFFFFD},
|
||||
{0x100000, 0x10FFFD},
|
||||
}
|
||||
|
||||
var ambiguousLatin = table{
|
||||
{0x00AA, 0x00AA}, {0x00BA, 0x00BA}, {0x00C6, 0x00C6},
|
||||
{0x00D0, 0x00D0}, {0x00D8, 0x00D8}, {0x00DE, 0x00E1},
|
||||
{0x00E6, 0x00E6}, {0x00E8, 0x00EA}, {0x00EC, 0x00ED},
|
||||
{0x00F0, 0x00F0}, {0x00F2, 0x00F3}, {0x00F7, 0x00FA},
|
||||
{0x00F0, 0x00F0}, {0x00F2, 0x00F3}, {0x00F8, 0x00FA},
|
||||
{0x00FC, 0x00FC}, {0x00FE, 0x00FE}, {0x0101, 0x0101},
|
||||
{0x0111, 0x0111}, {0x0113, 0x0113}, {0x011B, 0x011B},
|
||||
{0x0126, 0x0127}, {0x012B, 0x012B}, {0x0131, 0x0133},
|
||||
{0x0138, 0x0138}, {0x013F, 0x0142}, {0x0144, 0x0144},
|
||||
{0x0148, 0x014B}, {0x014D, 0x014D}, {0x0152, 0x0153},
|
||||
{0x0166, 0x0167}, {0x016B, 0x016B}, {0x01CE, 0x01CE},
|
||||
{0x01D0, 0x01D0}, {0x01D2, 0x01D2}, {0x01D4, 0x01D4},
|
||||
{0x01D6, 0x01D6}, {0x01D8, 0x01D8}, {0x01DA, 0x01DA},
|
||||
{0x01DC, 0x01DC}, {0x0251, 0x0251}, {0x0261, 0x0261},
|
||||
{0x02C4, 0x02C4}, {0x02C7, 0x02C7}, {0x02C9, 0x02CB},
|
||||
{0x02CD, 0x02CD}, {0x02D0, 0x02D0}, {0x02D8, 0x02DB},
|
||||
{0x02DD, 0x02DD}, {0x02DF, 0x02DF}, {0x0300, 0x036F},
|
||||
{0x0391, 0x03A1}, {0x03A3, 0x03A9}, {0x03B1, 0x03C1},
|
||||
{0x03C3, 0x03C9}, {0x0401, 0x0401}, {0x0410, 0x044F},
|
||||
{0x0451, 0x0451}, {0x2010, 0x2010}, {0x2013, 0x2016},
|
||||
{0x2018, 0x2019}, {0x201C, 0x201D}, {0x2020, 0x2022},
|
||||
{0x2024, 0x2027}, {0x2030, 0x2030}, {0x2032, 0x2033},
|
||||
{0x2035, 0x2035}, {0x203B, 0x203B}, {0x203E, 0x203E},
|
||||
{0x2074, 0x2074}, {0x207F, 0x207F}, {0x2081, 0x2084},
|
||||
{0x20AC, 0x20AC}, {0x2103, 0x2103}, {0x2105, 0x2105},
|
||||
{0x2109, 0x2109}, {0x2113, 0x2113}, {0x2116, 0x2116},
|
||||
{0x2121, 0x2122}, {0x2126, 0x2126}, {0x212B, 0x212B},
|
||||
{0x2153, 0x2154}, {0x215B, 0x215E}, {0x2160, 0x216B},
|
||||
{0x2170, 0x2179}, {0x2189, 0x2189}, {0x2190, 0x2199},
|
||||
{0x21B8, 0x21B9}, {0x21D2, 0x21D2}, {0x21D4, 0x21D4},
|
||||
{0x21E7, 0x21E7}, {0x2200, 0x2200}, {0x2202, 0x2203},
|
||||
{0x2207, 0x2208}, {0x220B, 0x220B}, {0x220F, 0x220F},
|
||||
{0x2211, 0x2211}, {0x2215, 0x2215}, {0x221A, 0x221A},
|
||||
{0x221D, 0x2220}, {0x2223, 0x2223}, {0x2225, 0x2225},
|
||||
{0x2227, 0x222C}, {0x222E, 0x222E}, {0x2234, 0x2237},
|
||||
{0x223C, 0x223D}, {0x2248, 0x2248}, {0x224C, 0x224C},
|
||||
{0x2252, 0x2252}, {0x2260, 0x2261}, {0x2264, 0x2267},
|
||||
{0x226A, 0x226B}, {0x226E, 0x226F}, {0x2282, 0x2283},
|
||||
{0x2286, 0x2287}, {0x2295, 0x2295}, {0x2299, 0x2299},
|
||||
{0x22A5, 0x22A5}, {0x22BF, 0x22BF}, {0x2312, 0x2312},
|
||||
{0x2460, 0x24E9}, {0x24EB, 0x254B}, {0x2550, 0x2573},
|
||||
{0x2580, 0x258F}, {0x2592, 0x2595}, {0x25A0, 0x25A1},
|
||||
{0x25A3, 0x25A9}, {0x25B2, 0x25B3}, {0x25B6, 0x25B7},
|
||||
{0x25BC, 0x25BD}, {0x25C0, 0x25C1}, {0x25C6, 0x25C8},
|
||||
{0x25CB, 0x25CB}, {0x25CE, 0x25D1}, {0x25E2, 0x25E5},
|
||||
{0x25EF, 0x25EF}, {0x2605, 0x2606}, {0x2609, 0x2609},
|
||||
{0x260E, 0x260F}, {0x261C, 0x261C}, {0x261E, 0x261E},
|
||||
{0x2640, 0x2640}, {0x2642, 0x2642}, {0x2660, 0x2661},
|
||||
{0x2663, 0x2665}, {0x2667, 0x266A}, {0x266C, 0x266D},
|
||||
{0x266F, 0x266F}, {0x269E, 0x269F}, {0x26BF, 0x26BF},
|
||||
{0x26C6, 0x26CD}, {0x26CF, 0x26D3}, {0x26D5, 0x26E1},
|
||||
{0x26E3, 0x26E3}, {0x26E8, 0x26E9}, {0x26EB, 0x26F1},
|
||||
{0x26F4, 0x26F4}, {0x26F6, 0x26F9}, {0x26FB, 0x26FC},
|
||||
{0x26FE, 0x26FF}, {0x273D, 0x273D}, {0x2776, 0x277F},
|
||||
{0x2B56, 0x2B59}, {0x3248, 0x324F}, {0xE000, 0xF8FF},
|
||||
{0xFFFD, 0xFFFD}, {0x1F100, 0x1F10A}, {0x1F110, 0x1F12D},
|
||||
{0x1F130, 0x1F169}, {0x1F170, 0x1F18D}, {0x1F18F, 0x1F190},
|
||||
{0x1F19B, 0x1F1AC}, {0xF0000, 0xFFFFD}, {0x100000, 0x10FFFD},
|
||||
{0x0166, 0x0167}, {0x016B, 0x016B},
|
||||
}
|
||||
|
||||
var narrow = table{
|
||||
{0x0020, 0x007E}, {0x00A2, 0x00A3}, {0x00A5, 0x00A6},
|
||||
{0x00AC, 0x00AC}, {0x00AF, 0x00AF}, {0x27E6, 0x27ED},
|
||||
|
||||
+15
-1
@@ -42,7 +42,7 @@ func (cli *Client) DialHijack(ctx context.Context, url, proto string, meta map[s
|
||||
return conn, err
|
||||
}
|
||||
|
||||
func (cli *Client) setupHijackConn(req *http.Request, proto string) (_ net.Conn, _ string, retErr error) {
|
||||
func (cli *Client) setupHijackConn(req *http.Request, proto string) (retConn net.Conn, mediaType string, retErr error) {
|
||||
ctx := req.Context()
|
||||
req.Header.Set("Connection", "Upgrade")
|
||||
req.Header.Set("Upgrade", proto)
|
||||
@@ -70,6 +70,20 @@ func (cli *Client) setupHijackConn(req *http.Request, proto string) (_ net.Conn,
|
||||
|
||||
hc := &hijackedConn{conn, bufio.NewReader(conn)}
|
||||
|
||||
// Cancel the handshake without tying the returned connection to ctx.
|
||||
cancelled := make(chan struct{})
|
||||
stop := context.AfterFunc(ctx, func() {
|
||||
_ = hc.Conn.Close()
|
||||
close(cancelled)
|
||||
})
|
||||
defer func() {
|
||||
if !stop() {
|
||||
// Cancellation won the race; don't return a connection being closed.
|
||||
<-cancelled
|
||||
retConn, mediaType, retErr = nil, "", ctx.Err()
|
||||
}
|
||||
}()
|
||||
|
||||
cfg := &cli.clientConfig
|
||||
|
||||
var rt http.RoundTripper = otelhttp.NewTransport(hc, cli.traceOpts...)
|
||||
|
||||
+79
-24
@@ -10,6 +10,9 @@ package http2
|
||||
// flow control window update.
|
||||
const inflowMinRefresh = 4 << 10
|
||||
|
||||
// maxFlowWindow is the maximum size of a flow control window.
|
||||
const maxFlowWindow = (1 << 31) - 1
|
||||
|
||||
// inflow accounts for an inbound flow control window.
|
||||
// It tracks both the latest window sent to the peer (used for enforcement)
|
||||
// and the accumulated unsent window.
|
||||
@@ -74,47 +77,99 @@ func takeInflows(f1, f2 *inflow, n uint32) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// outflow is the outbound flow control window's size.
|
||||
// connOutflow is connection-level outbound flow control.
|
||||
type connOutflow struct {
|
||||
initial int32 // SETTINGS_INITIAL_WINDOW_SIZE, changes with settings updates
|
||||
n int32 // connection-level flow control window
|
||||
flowErr bool // set when a flow control error is encountered
|
||||
}
|
||||
|
||||
func (f *connOutflow) init() {
|
||||
f.initial = initialWindowSize // initial stream window size
|
||||
f.n = initialWindowSize // current connection window size
|
||||
}
|
||||
|
||||
func (f *connOutflow) changeInitialWindowSize(size int64) bool {
|
||||
if size > maxFlowWindow {
|
||||
f.flowErr = true
|
||||
return false
|
||||
}
|
||||
f.initial = int32(size)
|
||||
return true
|
||||
}
|
||||
|
||||
func (f *connOutflow) add(n int32) bool {
|
||||
sum := int64(f.n) + int64(n)
|
||||
if sum > maxFlowWindow {
|
||||
f.flowErr = true
|
||||
return false
|
||||
}
|
||||
f.n += n
|
||||
return true
|
||||
}
|
||||
|
||||
// outflow is the stream-level outbound flow control window's size.
|
||||
type outflow struct {
|
||||
_ incomparable
|
||||
|
||||
// n is the number of DATA bytes we're allowed to send.
|
||||
// An outflow is kept both on a conn and a per-stream.
|
||||
n int32
|
||||
// delta is the difference between the stream's flow control window and
|
||||
// the connection's initial window size (conn.initial).
|
||||
//
|
||||
// Another view is that delta is the number of flow control bytes provided to this
|
||||
// stream in WINDOW_UPDATE frames, less the number of bytes sent on the stream.
|
||||
delta int32
|
||||
|
||||
// conn points to the shared connection-level outflow that is
|
||||
// shared by all streams on that conn. It is nil for the outflow
|
||||
// that's on the conn directly.
|
||||
conn *outflow
|
||||
// shared by all streams on that conn.
|
||||
conn *connOutflow
|
||||
}
|
||||
|
||||
func (f *outflow) setConnFlow(cf *outflow) { f.conn = cf }
|
||||
|
||||
func (f *outflow) available() int32 {
|
||||
n := f.n
|
||||
if f.conn != nil && f.conn.n < n {
|
||||
n = f.conn.n
|
||||
func (f *outflow) available() (int32, bool) {
|
||||
if f.conn == nil {
|
||||
return maxFlowWindow, true // only happens in tests
|
||||
}
|
||||
return n
|
||||
if f.conn.flowErr {
|
||||
// Block all sending once any stream observes a flow control error.
|
||||
return 0, false
|
||||
}
|
||||
n := int64(f.conn.initial) + int64(f.delta)
|
||||
if n > maxFlowWindow {
|
||||
f.conn.flowErr = true
|
||||
return 0, false
|
||||
}
|
||||
return min(int32(n), f.conn.n), true
|
||||
}
|
||||
|
||||
func (f *outflow) take(n int32) {
|
||||
if n > f.available() {
|
||||
if f.conn == nil {
|
||||
return // only happens in tests
|
||||
}
|
||||
avail, _ := f.available()
|
||||
if n > avail {
|
||||
panic("internal error: took too much")
|
||||
}
|
||||
f.n -= n
|
||||
if f.conn != nil {
|
||||
f.conn.n -= n
|
||||
}
|
||||
f.delta -= n
|
||||
f.conn.n -= n
|
||||
}
|
||||
|
||||
// add adds n bytes (positive or negative) to the flow control window.
|
||||
// It returns false if the sum would exceed 2^31-1.
|
||||
func (f *outflow) add(n int32) bool {
|
||||
sum := f.n + n
|
||||
if (sum > n) == (f.n > 0) {
|
||||
f.n = sum
|
||||
return true
|
||||
if f.conn == nil {
|
||||
return true // only happens in tests
|
||||
}
|
||||
return false
|
||||
avail := int64(f.conn.initial) + int64(f.delta)
|
||||
if avail > maxFlowWindow {
|
||||
// An earlier change to the initial window pushed this stream over the limit.
|
||||
// This is a connection-level flow control error.
|
||||
f.conn.flowErr = true
|
||||
return false
|
||||
}
|
||||
if avail+int64(n) > maxFlowWindow {
|
||||
// This update would push the stream over the limit.
|
||||
// This is a stream-level flow control error.
|
||||
return false
|
||||
}
|
||||
f.delta += n
|
||||
return true
|
||||
}
|
||||
|
||||
+25
-9
@@ -1711,7 +1711,8 @@ func (fr *Framer) readMetaFrame(hf *HeadersFrame) (Frame, error) {
|
||||
mh := &MetaHeadersFrame{
|
||||
HeadersFrame: hf,
|
||||
}
|
||||
var remainSize = fr.maxHeaderListSize()
|
||||
var headersRemainSize = fr.maxHeaderListSize()
|
||||
var trailersRemainSize = fr.maxHeaderListSize()
|
||||
var sawRegular bool
|
||||
|
||||
var invalid error // pseudo header field errors
|
||||
@@ -1743,14 +1744,29 @@ func (fr *Framer) readMetaFrame(hf *HeadersFrame) (Frame, error) {
|
||||
return
|
||||
}
|
||||
|
||||
size := hf.Size()
|
||||
if size > remainSize {
|
||||
var remainSize *uint32
|
||||
var size uint32
|
||||
if hf.Name == "trailer" {
|
||||
remainSize = &trailersRemainSize
|
||||
fieldCount := strings.Count(hf.Value, ",") + 1
|
||||
// Rather than actually constructing hpack.HeaderField for each
|
||||
// trailer field and cumulatively adding its Size, just do the math
|
||||
// manually to avoid unnecessary work. This does make it so
|
||||
// whitespaces after comma are counted against the budget, but that
|
||||
// should be innocuous.
|
||||
size = uint32(len(hf.Value)-fieldCount+1) + uint32(fieldCount)*hpack.HeaderField{}.Size()
|
||||
} else {
|
||||
remainSize = &headersRemainSize
|
||||
size = hf.Size()
|
||||
}
|
||||
if size > *remainSize {
|
||||
hdec.SetEmitEnabled(false)
|
||||
mh.Truncated = true
|
||||
remainSize = 0
|
||||
headersRemainSize = 0
|
||||
trailersRemainSize = 0
|
||||
return
|
||||
}
|
||||
remainSize -= size
|
||||
*remainSize -= size
|
||||
|
||||
mh.Fields = append(mh.Fields, hf)
|
||||
})
|
||||
@@ -1766,10 +1782,10 @@ func (fr *Framer) readMetaFrame(hf *HeadersFrame) (Frame, error) {
|
||||
// skip parsing the fragment and close the connection.
|
||||
//
|
||||
// "Too much" is either any CONTINUATION frame after we've already
|
||||
// exceeded the max header list size (in which case remainSize is 0),
|
||||
// or a frame whose encoded size is more than twice the remaining
|
||||
// header list bytes we're willing to accept.
|
||||
if int64(len(frag)) > int64(2*remainSize) {
|
||||
// exceeded the max header list size (if so, both budgets are 0), or a
|
||||
// frame whose encoded size is more than twice the remaining header
|
||||
// list bytes we're willing to accept.
|
||||
if int64(len(frag)) > 2*int64(headersRemainSize+trailersRemainSize) {
|
||||
if VerboseLogs {
|
||||
log.Printf("http2: header list too large")
|
||||
}
|
||||
|
||||
+66
-44
@@ -280,7 +280,6 @@ func (s *Server) serveConn(c net.Conn, opts *ServeConnOpts, newf func(*serverCon
|
||||
doneServing: make(chan struct{}),
|
||||
clientMaxStreams: math.MaxUint32, // Section 6.5.2: "Initially, there is no limit to this value"
|
||||
advMaxStreams: conf.MaxConcurrentStreams,
|
||||
initialStreamSendWindowSize: initialWindowSize,
|
||||
initialStreamRecvWindowSize: conf.MaxUploadBufferPerStream,
|
||||
maxFrameSize: initialMaxFrameSize,
|
||||
pingTimeout: conf.PingTimeout,
|
||||
@@ -320,7 +319,7 @@ func (s *Server) serveConn(c net.Conn, opts *ServeConnOpts, newf func(*serverCon
|
||||
// These start at the RFC-specified defaults. If there is a higher
|
||||
// configured value for inflow, that will be updated when we send a
|
||||
// WINDOW_UPDATE shortly after sending SETTINGS.
|
||||
sc.flow.add(initialWindowSize)
|
||||
sc.flow.init()
|
||||
sc.inflow.init(initialWindowSize)
|
||||
sc.hpackEncoder = hpack.NewEncoder(&sc.headerWriteBuf)
|
||||
sc.hpackEncoder.SetMaxDynamicTableSizeLimit(conf.MaxEncoderHeaderTableSize)
|
||||
@@ -427,7 +426,7 @@ type serverConn struct {
|
||||
wroteFrameCh chan frameWriteResult // from writeFrameAsync -> serve, tickles more frame writes
|
||||
bodyReadCh chan bodyReadMsg // from handlers -> serve
|
||||
serveMsgCh chan interface{} // misc messages & code to send to / run on the serve loop
|
||||
flow outflow // conn-wide (not stream-specific) outbound flow control
|
||||
flow connOutflow // conn-wide (not stream-specific) outbound flow control
|
||||
inflow inflow // conn-wide inbound flow control
|
||||
tlsState *tls.ConnectionState // shared by all handlers, like net/http
|
||||
remoteAddrStr string
|
||||
@@ -441,6 +440,9 @@ type serverConn struct {
|
||||
sawFirstSettings bool // got the initial SETTINGS frame after the preface
|
||||
needToSendSettingsAck bool
|
||||
unackedSettings int // how many SETTINGS have we sent without ACKs?
|
||||
pendingEncoderTableSize bool // peer changed SETTINGS_HEADER_TABLE_SIZE; apply to hpackEncoder before the next frame write
|
||||
encoderTableSizeMin uint32 // smallest SETTINGS_HEADER_TABLE_SIZE since the last apply
|
||||
encoderTableSize uint32 // latest SETTINGS_HEADER_TABLE_SIZE
|
||||
queuedControlFrames int // control frames in the writeSched queue
|
||||
clientMaxStreams uint32 // SETTINGS_MAX_CONCURRENT_STREAMS from client (our PUSH_PROMISE limit)
|
||||
advMaxStreams uint32 // our SETTINGS_MAX_CONCURRENT_STREAMS advertised the client
|
||||
@@ -451,7 +453,6 @@ type serverConn struct {
|
||||
maxPushPromiseID uint32 // ID of the last push promise (even), or 0 if there have been no pushes
|
||||
streams map[uint32]*stream
|
||||
unstartedHandlers []unstartedHandler
|
||||
initialStreamSendWindowSize int32
|
||||
initialStreamRecvWindowSize int32
|
||||
maxFrameSize int32
|
||||
peerMaxHeaderListSize uint32 // zero means unknown (default)
|
||||
@@ -521,7 +522,8 @@ type stream struct {
|
||||
// immutable:
|
||||
sc *serverConn
|
||||
id uint32
|
||||
body *pipe // non-nil if expecting DATA frames
|
||||
body *pipe // non-nil if expecting DATA frames
|
||||
reqBody *requestBody
|
||||
cw closeWaiter // closed wait stream transitions to closed state
|
||||
ctx context.Context
|
||||
cancelCtx func()
|
||||
@@ -1172,6 +1174,16 @@ func (sc *serverConn) startFrameWrite(wr FrameWriteRequest) {
|
||||
|
||||
sc.writingFrame = true
|
||||
sc.needsFrameFlush = true
|
||||
if sc.pendingEncoderTableSize {
|
||||
// hpackEncoder may be in use by writeFrameAsync, so SETTINGS
|
||||
// changes to it are deferred until no frame is being written.
|
||||
// Replaying the smallest size before the latest one keeps the
|
||||
// encoder's view identical to having applied every change
|
||||
// (RFC 7541, Section 4.2).
|
||||
sc.pendingEncoderTableSize = false
|
||||
sc.hpackEncoder.SetMaxDynamicTableSize(sc.encoderTableSizeMin)
|
||||
sc.hpackEncoder.SetMaxDynamicTableSize(sc.encoderTableSize)
|
||||
}
|
||||
if wr.write.staysWithinBuffer(sc.bw.Available()) {
|
||||
sc.writingFrameAsync = false
|
||||
err := wr.write.writeFrame(sc)
|
||||
@@ -1271,6 +1283,11 @@ func (sc *serverConn) scheduleFrameWrite() {
|
||||
}
|
||||
sc.inFrameScheduleLoop = true
|
||||
for !sc.writingFrameAsync {
|
||||
if sc.flow.flowErr && (!sc.inGoAway || sc.goAwayCode == ErrCodeNo) {
|
||||
sc.inGoAway = true
|
||||
sc.needToSendGoAway = true
|
||||
sc.goAwayCode = ErrCodeFlowControl
|
||||
}
|
||||
if sc.needToSendGoAway {
|
||||
sc.needToSendGoAway = false
|
||||
sc.startFrameWrite(FrameWriteRequest{
|
||||
@@ -1294,6 +1311,9 @@ func (sc *serverConn) scheduleFrameWrite() {
|
||||
sc.startFrameWrite(wr)
|
||||
continue
|
||||
}
|
||||
if sc.flow.flowErr {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if sc.needsFrameFlush {
|
||||
sc.startFrameWrite(FrameWriteRequest{write: flushFrameWriter{}})
|
||||
@@ -1526,6 +1546,10 @@ func (sc *serverConn) processWindowUpdate(f *WindowUpdateFrame) error {
|
||||
return nil
|
||||
}
|
||||
if !st.flow.add(int32(f.Increment)) {
|
||||
if st.flow.conn.flowErr {
|
||||
// This is a lazily-detected connection-level flow control error.
|
||||
return sc.countError("bad_flow", ConnectionError(ErrCodeFlowControl))
|
||||
}
|
||||
return sc.countError("bad_flow", streamError(f.StreamID, ErrCodeFlowControl))
|
||||
}
|
||||
default: // connection-level flow control
|
||||
@@ -1584,10 +1608,6 @@ func (sc *serverConn) closeStream(st *stream, err error) {
|
||||
}
|
||||
}
|
||||
if p := st.body; p != nil {
|
||||
// Return any buffered unread bytes worth of conn-level flow control.
|
||||
// See golang.org/issue/16481
|
||||
sc.sendWindowUpdate(nil, p.Len())
|
||||
|
||||
p.CloseWithError(err)
|
||||
}
|
||||
if e, ok := err.(StreamError); ok {
|
||||
@@ -1641,7 +1661,12 @@ func (sc *serverConn) processSetting(s Setting) error {
|
||||
}
|
||||
switch s.ID {
|
||||
case SettingHeaderTableSize:
|
||||
sc.hpackEncoder.SetMaxDynamicTableSize(s.Val)
|
||||
// Applied by startFrameWrite; see comment there.
|
||||
if !sc.pendingEncoderTableSize || s.Val < sc.encoderTableSizeMin {
|
||||
sc.encoderTableSizeMin = s.Val
|
||||
}
|
||||
sc.encoderTableSize = s.Val
|
||||
sc.pendingEncoderTableSize = true
|
||||
case SettingEnablePush:
|
||||
sc.pushEnabled = s.Val != 0
|
||||
case SettingMaxConcurrentStreams:
|
||||
@@ -1672,28 +1697,14 @@ func (sc *serverConn) processSetting(s Setting) error {
|
||||
|
||||
func (sc *serverConn) processSettingInitialWindowSize(val uint32) error {
|
||||
sc.serveG.check()
|
||||
// Note: val already validated to be within range by
|
||||
// processSetting's Valid call.
|
||||
|
||||
// "A SETTINGS frame can alter the initial flow control window
|
||||
// size for all current streams. When the value of
|
||||
// SETTINGS_INITIAL_WINDOW_SIZE changes, a receiver MUST
|
||||
// adjust the size of all stream flow control windows that it
|
||||
// maintains by the difference between the new value and the
|
||||
// old value."
|
||||
old := sc.initialStreamSendWindowSize
|
||||
sc.initialStreamSendWindowSize = int32(val)
|
||||
growth := int32(val) - old // may be negative
|
||||
for _, st := range sc.streams {
|
||||
if !st.flow.add(growth) {
|
||||
// 6.9.2 Initial Flow Control Window Size
|
||||
// "An endpoint MUST treat a change to
|
||||
// SETTINGS_INITIAL_WINDOW_SIZE that causes any flow
|
||||
// control window to exceed the maximum size as a
|
||||
// connection error (Section 5.4.1) of type
|
||||
// FLOW_CONTROL_ERROR."
|
||||
return sc.countError("setting_win_size", ConnectionError(ErrCodeFlowControl))
|
||||
}
|
||||
if !sc.flow.changeInitialWindowSize(int64(val)) {
|
||||
// 6.9.2 Initial Flow Control Window Size
|
||||
// "An endpoint MUST treat a change to
|
||||
// SETTINGS_INITIAL_WINDOW_SIZE that causes any flow
|
||||
// control window to exceed the maximum size as a
|
||||
// connection error (Section 5.4.1) of type
|
||||
// FLOW_CONTROL_ERROR."
|
||||
return sc.countError("setting_win_size", ConnectionError(ErrCodeFlowControl))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1966,7 +1977,7 @@ func (sc *serverConn) processHeaders(f *MetaHeadersFrame) error {
|
||||
if st.reqTrailer != nil {
|
||||
st.trailer = make(http.Header)
|
||||
}
|
||||
st.body = req.Body.(*requestBody).pipe // may be nil
|
||||
st.body = st.reqBody.pipe // may be nil
|
||||
st.declBodyBytes = req.ContentLength
|
||||
|
||||
handler := sc.handler.ServeHTTP
|
||||
@@ -1989,7 +2000,7 @@ func (sc *serverConn) processHeaders(f *MetaHeadersFrame) error {
|
||||
st.readDeadline = time.AfterFunc(sc.hs.ReadTimeout, st.onReadTimeout)
|
||||
}
|
||||
|
||||
return sc.scheduleHandler(id, rw, req, handler)
|
||||
return sc.scheduleHandler(st, rw, req, handler)
|
||||
}
|
||||
|
||||
func (sc *serverConn) upgradeRequest(req *http.Request) {
|
||||
@@ -2102,7 +2113,6 @@ func (sc *serverConn) newStream(id, pusherID uint32, state streamState, priority
|
||||
}
|
||||
st.cw.Init()
|
||||
st.flow.conn = &sc.flow // link to conn-level counter
|
||||
st.flow.add(sc.initialStreamSendWindowSize)
|
||||
st.inflow.init(sc.initialStreamRecvWindowSize)
|
||||
if sc.hs.WriteTimeout > 0 {
|
||||
st.writeDeadline = time.AfterFunc(sc.hs.WriteTimeout, st.onWriteTimeout)
|
||||
@@ -2184,7 +2194,7 @@ func (sc *serverConn) newWriterAndRequest(st *stream, f *MetaHeadersFrame) (*res
|
||||
} else {
|
||||
req.ContentLength = -1
|
||||
}
|
||||
req.Body.(*requestBody).pipe = &pipe{
|
||||
st.reqBody.pipe = &pipe{
|
||||
b: &dataBuffer{expected: req.ContentLength},
|
||||
}
|
||||
}
|
||||
@@ -2204,7 +2214,7 @@ func (sc *serverConn) newWriterAndRequestNoBody(st *stream, rp httpcommon.Server
|
||||
return nil, nil, sc.countError(res.InvalidReason, streamError(st.id, ErrCodeProtocol))
|
||||
}
|
||||
|
||||
body := &requestBody{
|
||||
st.reqBody = &requestBody{
|
||||
conn: sc,
|
||||
stream: st,
|
||||
needsContinue: res.NeedsContinue,
|
||||
@@ -2220,7 +2230,7 @@ func (sc *serverConn) newWriterAndRequestNoBody(st *stream, rp httpcommon.Server
|
||||
ProtoMinor: 0,
|
||||
TLS: tlsState,
|
||||
Host: rp.Authority,
|
||||
Body: body,
|
||||
Body: st.reqBody,
|
||||
Trailer: res.Trailer,
|
||||
}).WithContext(st.ctx)
|
||||
rw := sc.newResponseWriter(st, req)
|
||||
@@ -2244,11 +2254,12 @@ type unstartedHandler struct {
|
||||
rw *responseWriter
|
||||
req *http.Request
|
||||
handler func(http.ResponseWriter, *http.Request)
|
||||
body *pipe
|
||||
}
|
||||
|
||||
// scheduleHandler starts a handler goroutine,
|
||||
// or schedules one to start as soon as an existing handler finishes.
|
||||
func (sc *serverConn) scheduleHandler(streamID uint32, rw *responseWriter, req *http.Request, handler func(http.ResponseWriter, *http.Request)) error {
|
||||
func (sc *serverConn) scheduleHandler(st *stream, rw *responseWriter, req *http.Request, handler func(http.ResponseWriter, *http.Request)) error {
|
||||
sc.serveG.check()
|
||||
maxHandlers := sc.advMaxStreams
|
||||
if sc.curHandlers < maxHandlers {
|
||||
@@ -2260,10 +2271,11 @@ func (sc *serverConn) scheduleHandler(streamID uint32, rw *responseWriter, req *
|
||||
return sc.countError("too_many_early_resets", ConnectionError(ErrCodeEnhanceYourCalm))
|
||||
}
|
||||
sc.unstartedHandlers = append(sc.unstartedHandlers, unstartedHandler{
|
||||
streamID: streamID,
|
||||
streamID: st.id,
|
||||
rw: rw,
|
||||
req: req,
|
||||
handler: handler,
|
||||
body: st.body,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
@@ -2277,6 +2289,10 @@ func (sc *serverConn) handlerDone() {
|
||||
u := sc.unstartedHandlers[i]
|
||||
if sc.streams[u.streamID] == nil {
|
||||
// This stream was reset before its goroutine had a chance to start.
|
||||
if u.body != nil {
|
||||
u.body.BreakWithError(errClosedBody)
|
||||
sc.sendWindowUpdate(nil, u.body.Len())
|
||||
}
|
||||
continue
|
||||
}
|
||||
if sc.curHandlers >= maxHandlers {
|
||||
@@ -2298,6 +2314,12 @@ func (sc *serverConn) runHandler(rw *responseWriter, req *http.Request, handler
|
||||
didPanic := true
|
||||
defer func() {
|
||||
rw.rws.stream.cancelCtx()
|
||||
if b := rw.rws.stream.reqBody; b != nil {
|
||||
// Closing the body refunds flow control credit for any unconsumed data.
|
||||
// (reqBody is nil for Upgrade: h2c requests, but those do not use flow
|
||||
// control for the request body.)
|
||||
b.Close()
|
||||
}
|
||||
if req.MultipartForm != nil {
|
||||
req.MultipartForm.RemoveAll()
|
||||
}
|
||||
@@ -2396,7 +2418,7 @@ func (sc *serverConn) noteBodyReadFromHandler(st *stream, n int, err error) {
|
||||
func (sc *serverConn) noteBodyRead(st *stream, n int) {
|
||||
sc.serveG.check()
|
||||
sc.sendWindowUpdate(nil, n) // conn-level
|
||||
if st.state != stateHalfClosedRemote && st.state != stateClosed {
|
||||
if st != nil && st.state != stateHalfClosedRemote && st.state != stateClosed {
|
||||
// Don't send this WINDOW_UPDATE if the stream is closed
|
||||
// remotely.
|
||||
sc.sendWindowUpdate(st, n)
|
||||
@@ -2444,6 +2466,9 @@ func (b *requestBody) Close() error {
|
||||
b.closeOnce.Do(func() {
|
||||
if b.pipe != nil {
|
||||
b.pipe.BreakWithError(errClosedBody)
|
||||
if unread := b.pipe.Len(); unread > 0 {
|
||||
b.conn.noteBodyReadFromHandler(nil, unread, errClosedBody)
|
||||
}
|
||||
}
|
||||
})
|
||||
return nil
|
||||
@@ -2461,9 +2486,6 @@ func (b *requestBody) Read(p []byte) (n int, err error) {
|
||||
if err == io.EOF {
|
||||
b.sawEOF = true
|
||||
}
|
||||
if b.conn == nil {
|
||||
return
|
||||
}
|
||||
b.conn.noteBodyReadFromHandler(b.stream, n, err)
|
||||
return
|
||||
}
|
||||
|
||||
+94
-48
@@ -27,6 +27,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"net/textproto"
|
||||
"slices"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -203,7 +204,7 @@ type ClientConn struct {
|
||||
|
||||
mu sync.Mutex // guards following
|
||||
cond *sync.Cond // hold mu; broadcast on flow/closed changes
|
||||
flow outflow // our conn-level flow control quota (cs.outflow is per stream)
|
||||
flow connOutflow // our conn-level flow control quota (cs.outflow is per stream)
|
||||
inflow inflow // peer's conn-level flow control
|
||||
doNotReuse bool // whether conn is marked to not be reused for any future requests
|
||||
closing bool
|
||||
@@ -227,7 +228,6 @@ type ClientConn struct {
|
||||
maxConcurrentStreams uint32
|
||||
peerMaxHeaderListSize uint64
|
||||
peerMaxHeaderTableSize uint32
|
||||
initialWindowSize uint32
|
||||
initialStreamRecvWindowSize int32
|
||||
readIdleTimeout time.Duration
|
||||
pingTimeout time.Duration
|
||||
@@ -472,7 +472,6 @@ func (t *Transport) newClientConn(c net.Conn, singleUse bool, internalStateHook
|
||||
readerDone: make(chan struct{}),
|
||||
nextStreamID: 1,
|
||||
maxFrameSize: 16 << 10, // spec default
|
||||
initialWindowSize: 65535, // spec default
|
||||
initialStreamRecvWindowSize: conf.MaxUploadBufferPerStream,
|
||||
maxConcurrentStreams: initialMaxConcurrentStreams, // "infinite", per spec. Use a smaller value until we have received server settings.
|
||||
strictMaxConcurrentStreams: conf.StrictMaxConcurrentRequests,
|
||||
@@ -497,7 +496,7 @@ func (t *Transport) newClientConn(c net.Conn, singleUse bool, internalStateHook
|
||||
}
|
||||
|
||||
cc.cond = sync.NewCond(&cc.mu)
|
||||
cc.flow.add(int32(initialWindowSize))
|
||||
cc.flow.init()
|
||||
|
||||
// TODO: adjust this writer size to account for frame size +
|
||||
// MTU + crypto/tls record padding.
|
||||
@@ -876,6 +875,33 @@ func (cc *ClientConn) sendGoAway() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cc *ClientConn) goAwayAndClose(code ErrCode) {
|
||||
cc.mu.Lock()
|
||||
closed := cc.closed
|
||||
cc.closing = true
|
||||
cc.closed = true
|
||||
cc.mu.Unlock()
|
||||
if closed {
|
||||
return
|
||||
}
|
||||
if f := cc.fr.countError; f != nil {
|
||||
f(fmt.Sprintf("conn_close_error_%s", code.stringToken()))
|
||||
}
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
cc.wmu.Lock()
|
||||
cc.fr.WriteGoAway(0, code, nil)
|
||||
cc.bw.Flush()
|
||||
cc.wmu.Unlock()
|
||||
}()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(250 * time.Millisecond):
|
||||
}
|
||||
cc.closeForError(fmt.Errorf("http2: closing connection with %v", code))
|
||||
}
|
||||
|
||||
// closes the client connection immediately. In-flight requests are interrupted.
|
||||
// err is sent to streams.
|
||||
func (cc *ClientConn) closeForError(err error) {
|
||||
@@ -1319,7 +1345,9 @@ func (cs *clientStream) cleanupWriteRequest(err error) {
|
||||
}
|
||||
if err != nil {
|
||||
cs.abortStream(err) // possibly redundant, but harmless
|
||||
if cs.sentHeaders {
|
||||
if ce, ok := err.(ConnectionError); ok {
|
||||
cc.goAwayAndClose(ErrCode(ce))
|
||||
} else if cs.sentHeaders {
|
||||
if se, ok := err.(StreamError); ok {
|
||||
if se.Cause != errFromPeer {
|
||||
cc.writeStreamReset(cs.ID, se.Code, false, err)
|
||||
@@ -1648,8 +1676,12 @@ func (cs *clientStream) awaitFlowControl(maxBytes int) (taken int32, err error)
|
||||
return 0, errRequestCanceled
|
||||
default:
|
||||
}
|
||||
if a := cs.flow.available(); a > 0 {
|
||||
take := a
|
||||
avail, ok := cs.flow.available()
|
||||
if !ok {
|
||||
return 0, ConnectionError(ErrCodeFlowControl)
|
||||
}
|
||||
if avail > 0 {
|
||||
take := avail
|
||||
if int(take) > maxBytes {
|
||||
|
||||
take = int32(maxBytes) // can't truncate int; take is int32
|
||||
@@ -1710,8 +1742,7 @@ type resAndError struct {
|
||||
|
||||
// requires cc.mu be held.
|
||||
func (cc *ClientConn) addStreamLocked(cs *clientStream) {
|
||||
cs.flow.add(int32(cc.initialWindowSize))
|
||||
cs.flow.setConnFlow(&cc.flow)
|
||||
cs.flow.conn = &cc.flow
|
||||
cs.inflow.init(cc.initialStreamRecvWindowSize)
|
||||
cs.ID = cc.nextStreamID
|
||||
cc.nextStreamID += 2
|
||||
@@ -2095,18 +2126,42 @@ func (rl *clientConnReadLoop) handleResponse(cs *clientStream, f *MetaHeadersFra
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Delete various headers that might mess up framing for HTTP/1. This is
|
||||
// not a problem for HTTP/2, but someone might use HTTP/2 transport as a
|
||||
// reverse proxy which forwards the response to an HTTP/1 client. Our
|
||||
// HTTP/1 client transport will properly reject improper headers such as
|
||||
// multiple conflicting Content-Length headers, but other implementations
|
||||
// might not.
|
||||
// TODO: just reject such responses? We deleted them for compatibility
|
||||
// since this was done in a security fix (go.dev/issue/81115). However,
|
||||
// rejecting them seems entirely reasonable and relatively safe.
|
||||
|
||||
// Connection-specific header fields must not appear in an HTTP/2 message,
|
||||
// and any message containing them is malformed. RFC 9113, Section 8.2.2.
|
||||
for _, k := range connHeaders {
|
||||
delete(res.Header, k)
|
||||
}
|
||||
res.ContentLength = -1
|
||||
if clens := res.Header["Content-Length"]; len(clens) == 1 {
|
||||
if cl, err := strconv.ParseUint(clens[0], 10, 63); err == nil {
|
||||
res.ContentLength = int64(cl)
|
||||
if clens, ok := res.Header["Content-Length"]; ok {
|
||||
// Repeated Content-Length values may be collapsed into one only if
|
||||
// they are identical per RFC 9110 Section 8.6.
|
||||
// No need to trim whitespace, HTTP/2 header values must not have
|
||||
// extraneous whitespace per RFC 9113 Section 8.2.1.
|
||||
// Non-canonical headers are already rejected by our framer at this
|
||||
// point.
|
||||
conflicting := slices.ContainsFunc(clens[1:], func(clen string) bool {
|
||||
return clen != clens[0]
|
||||
})
|
||||
cl, err := strconv.ParseUint(clens[0], 10, 63)
|
||||
if conflicting || err != nil {
|
||||
delete(res.Header, "Content-Length")
|
||||
} else {
|
||||
// TODO: care? unlike http/1, it won't mess up our framing, so it's
|
||||
// more safe smuggling-wise to ignore.
|
||||
res.Header["Content-Length"] = clens[:1]
|
||||
res.ContentLength = int64(cl)
|
||||
}
|
||||
} else if len(clens) > 1 {
|
||||
// TODO: care? unlike http/1, it won't mess up our framing, so it's
|
||||
// more safe smuggling-wise to ignore.
|
||||
} else if f.StreamEnded() && !cs.isHead {
|
||||
}
|
||||
|
||||
if res.ContentLength < 0 && f.StreamEnded() && !cs.isHead {
|
||||
res.ContentLength = 0
|
||||
}
|
||||
|
||||
@@ -2417,6 +2472,10 @@ const (
|
||||
func (rl *clientConnReadLoop) streamByID(id uint32, headerOrData bool) *clientStream {
|
||||
rl.cc.mu.Lock()
|
||||
defer rl.cc.mu.Unlock()
|
||||
return rl.streamByIDLocked(id, headerOrData)
|
||||
}
|
||||
|
||||
func (rl *clientConnReadLoop) streamByIDLocked(id uint32, headerOrData bool) *clientStream {
|
||||
if headerOrData {
|
||||
// Work around an unfortunate gRPC behavior.
|
||||
// See comment on ClientConn.rstStreamPingsBlocked for details.
|
||||
@@ -2499,24 +2558,10 @@ func (rl *clientConnReadLoop) processSettingsNoWrite(f *SettingsFrame) error {
|
||||
case SettingMaxHeaderListSize:
|
||||
cc.peerMaxHeaderListSize = uint64(s.Val)
|
||||
case SettingInitialWindowSize:
|
||||
// Values above the maximum flow-control
|
||||
// window size of 2^31-1 MUST be treated as a
|
||||
// connection error (Section 5.4.1) of type
|
||||
// FLOW_CONTROL_ERROR.
|
||||
if s.Val > math.MaxInt32 {
|
||||
if !cc.flow.changeInitialWindowSize(int64(s.Val)) {
|
||||
return ConnectionError(ErrCodeFlowControl)
|
||||
}
|
||||
|
||||
// Adjust flow control of currently-open
|
||||
// frames by the difference of the old initial
|
||||
// window size and this one.
|
||||
delta := int32(s.Val) - int32(cc.initialWindowSize)
|
||||
for _, cs := range cc.streams {
|
||||
cs.flow.add(delta)
|
||||
}
|
||||
cc.cond.Broadcast()
|
||||
|
||||
cc.initialWindowSize = s.Val
|
||||
case SettingHeaderTableSize:
|
||||
cc.henc.SetMaxDynamicTableSize(s.Val)
|
||||
cc.peerMaxHeaderTableSize = s.Val
|
||||
@@ -2558,29 +2603,30 @@ func (rl *clientConnReadLoop) processSettingsNoWrite(f *SettingsFrame) error {
|
||||
|
||||
func (rl *clientConnReadLoop) processWindowUpdate(f *WindowUpdateFrame) error {
|
||||
cc := rl.cc
|
||||
cs := rl.streamByID(f.StreamID, notHeaderOrDataFrame)
|
||||
if f.StreamID != 0 && cs == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
cc.mu.Lock()
|
||||
defer cc.mu.Unlock()
|
||||
|
||||
fl := &cc.flow
|
||||
if cs != nil {
|
||||
fl = &cs.flow
|
||||
}
|
||||
if !fl.add(int32(f.Increment)) {
|
||||
// For stream, the sender sends RST_STREAM with an error code of FLOW_CONTROL_ERROR
|
||||
if cs != nil {
|
||||
if f.StreamID == 0 {
|
||||
if !cc.flow.add(int32(f.Increment)) {
|
||||
return ConnectionError(ErrCodeFlowControl)
|
||||
}
|
||||
} else {
|
||||
cs := rl.streamByIDLocked(f.StreamID, notHeaderOrDataFrame)
|
||||
if cs == nil {
|
||||
return nil
|
||||
}
|
||||
if !cs.flow.add(int32(f.Increment)) {
|
||||
if cs.flow.conn.flowErr {
|
||||
// This is a lazily-detected connection-level flow control error.
|
||||
return ConnectionError(ErrCodeFlowControl)
|
||||
}
|
||||
// For stream, the sender sends RST_STREAM with
|
||||
// an error code of FLOW_CONTROL_ERROR.
|
||||
rl.endStreamErrorLocked(cs, StreamError{
|
||||
StreamID: f.StreamID,
|
||||
Code: ErrCodeFlowControl,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
return ConnectionError(ErrCodeFlowControl)
|
||||
}
|
||||
cc.cond.Broadcast()
|
||||
return nil
|
||||
|
||||
+194
-20
@@ -119,33 +119,27 @@ type http2TransportContextKey struct{}
|
||||
|
||||
// DialFromContext dials a new connection using the http2.Transport's DialTLS/DialTLSContext.
|
||||
func (t transportConfig) DialFromContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
if ctx.Value(http2TransportContextKey{}) == nil {
|
||||
dial, _ := ctx.Value(http2TransportContextKey{}).(*transportRoundTripState)
|
||||
if dial == nil {
|
||||
// We're being called from a RoundTrip that did not start with an http2.Transport.
|
||||
// Use the http.Transport's dialer.
|
||||
return nil, errors.ErrUnsupported
|
||||
}
|
||||
|
||||
tlsConf := t.t.TLSClientConfig
|
||||
if tlsConf == nil {
|
||||
tlsConf = &tls.Config{}
|
||||
} else {
|
||||
tlsConf = tlsConf.Clone()
|
||||
}
|
||||
if !slices.Contains(tlsConf.NextProtos, "h2") {
|
||||
tlsConf.NextProtos = append([]string{"h2"}, tlsConf.NextProtos...)
|
||||
}
|
||||
if tlsConf.ServerName == "" {
|
||||
host, _, err := net.SplitHostPort(address)
|
||||
if err == nil {
|
||||
tlsConf.ServerName = host
|
||||
}
|
||||
}
|
||||
return t.t.dialTLS(ctx, network, address, tlsConf)
|
||||
return t.t.dialForRoundTrip(ctx, dial, network, address)
|
||||
}
|
||||
|
||||
type transportInternal struct {
|
||||
initOnce sync.Once
|
||||
lazyt1 *http.Transport
|
||||
|
||||
dialMu sync.Mutex
|
||||
dials map[string]*transportDialState
|
||||
}
|
||||
|
||||
type transportDialState struct {
|
||||
dialc chan struct{} // close when dial returns
|
||||
dialErr error // dial result, set before dialc is closed
|
||||
rtdonec chan struct{} // closed when RoundTrip initiating the dial returns
|
||||
}
|
||||
|
||||
func (t *Transport) init() *http.Transport {
|
||||
@@ -167,6 +161,52 @@ func (t *Transport) configure(t1 *http.Transport) {
|
||||
}
|
||||
}
|
||||
|
||||
// transportRoundTripState is the state of the dial for an http2.Transport.RoundTrip.
|
||||
type transportRoundTripState struct {
|
||||
mu sync.Mutex
|
||||
rtdone bool // set when RoundTrip returns
|
||||
rtdonec chan struct{} // closed when RoundTrip returns
|
||||
gotconnc chan struct{} // closed when GotConn hook is called
|
||||
}
|
||||
|
||||
// startDial is called when a dial starts.
|
||||
//
|
||||
// It returns done=true if the RoundTrip has already returned,
|
||||
// in which case we should skip dialing.
|
||||
func (dial *transportRoundTripState) startDial() (gotconnc chan struct{}, done bool) {
|
||||
dial.mu.Lock()
|
||||
defer dial.mu.Unlock()
|
||||
if dial.rtdone {
|
||||
return nil, true
|
||||
}
|
||||
if dial.rtdonec == nil {
|
||||
dial.rtdonec = make(chan struct{})
|
||||
}
|
||||
dial.gotconnc = make(chan struct{})
|
||||
return dial.gotconnc, false
|
||||
}
|
||||
|
||||
// gotConn is called when RoundTrip gets a connection.
|
||||
// This may happen multiple times per RoundTrip, if a request is retried.
|
||||
func (dial *transportRoundTripState) gotConn() {
|
||||
dial.mu.Lock()
|
||||
defer dial.mu.Unlock()
|
||||
if dial.gotconnc != nil {
|
||||
close(dial.gotconnc)
|
||||
dial.gotconnc = nil
|
||||
}
|
||||
}
|
||||
|
||||
// roundTripDone is called when RoundTrip returns.
|
||||
func (dial *transportRoundTripState) roundTripDone() {
|
||||
dial.mu.Lock()
|
||||
defer dial.mu.Unlock()
|
||||
dial.rtdone = true
|
||||
if dial.rtdonec != nil {
|
||||
close(dial.rtdonec)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Transport) roundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Response, error) {
|
||||
t1 := t.init()
|
||||
|
||||
@@ -186,12 +226,146 @@ func (t *Transport) roundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Res
|
||||
// Both http.Transport and http2.Transport allow the user to provide a custom
|
||||
// dial function, and historically you only get the dial function from the
|
||||
// Transport you're calling RoundTrip on.
|
||||
ctx := context.WithValue(req.Context(), http2TransportContextKey{}, t)
|
||||
//
|
||||
// In addition, http2.Transport coalesces dials, which http.Transport historically
|
||||
// has not.
|
||||
dial := &transportRoundTripState{}
|
||||
defer dial.roundTripDone()
|
||||
ctx := context.WithValue(req.Context(), http2TransportContextKey{}, dial)
|
||||
ctx = httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{
|
||||
GotConn: func(httptrace.GotConnInfo) {
|
||||
// Tell dialForRoundTrip that we have received a connection.
|
||||
dial.gotConn()
|
||||
},
|
||||
})
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
return t1.RoundTrip(req)
|
||||
}
|
||||
|
||||
var errCoalescedDialAbandoned = errors.New("http2: abandoned coalesced dial")
|
||||
|
||||
const coalescedDialRetryTimeout = 50 * time.Millisecond
|
||||
|
||||
// dialForRoundTrip is called (indirectly) by net/http when dialing a new connection
|
||||
// for a request call which originated as an http2.Transport.RoundTrip call.
|
||||
//
|
||||
// It uses the http2.Transport's Dial hooks and coalesces dials.
|
||||
func (t *Transport) dialForRoundTrip(ctx context.Context, dial *transportRoundTripState, network, address string) (net.Conn, error) {
|
||||
gotconnc, rtdone := dial.startDial()
|
||||
if rtdone {
|
||||
// RoundTrip returned, no need for this dial to proceed.
|
||||
return nil, errCoalescedDialAbandoned
|
||||
}
|
||||
|
||||
var state *transportDialState
|
||||
for {
|
||||
// The first dial to an address registers itself with t.dials.
|
||||
// When the dial finishes, it records the outcome and removes itself from t.dials
|
||||
// so future dials will start a new round of coalescing.
|
||||
//
|
||||
// Subsequent dials observe that a dial is in progress, and skip dialing.
|
||||
t.dialMu.Lock()
|
||||
if t.dials == nil {
|
||||
t.dials = make(map[string]*transportDialState)
|
||||
}
|
||||
leader := t.dials[address]
|
||||
if leader == nil {
|
||||
// No entry in t.dials to coalesce with. Add ourselves as leader.
|
||||
state = &transportDialState{
|
||||
dialc: make(chan struct{}),
|
||||
rtdonec: dial.rtdonec,
|
||||
}
|
||||
t.dials[address] = state
|
||||
}
|
||||
t.dialMu.Unlock()
|
||||
if leader == nil {
|
||||
// We are the leader, so we should dial for real.
|
||||
break
|
||||
}
|
||||
|
||||
// Coalesce with a previous dial.
|
||||
var (
|
||||
done = false
|
||||
leaderdialc = leader.dialc
|
||||
leaderrtdonec = leader.rtdonec
|
||||
timerc <-chan time.Time
|
||||
)
|
||||
for !done {
|
||||
if leaderdialc == nil && leaderrtdonec == nil && timerc == nil {
|
||||
// The leader finished dialing, and its RoundTrip finished.
|
||||
//
|
||||
// There are several possibilities, which reduce to:
|
||||
// - The RoundTrip we are dialing for is about to receive
|
||||
// a connection (possibly the one the leader just dialed),
|
||||
// but we haven't observed it yet.
|
||||
// - There's something wrong with the leader's connection,
|
||||
// such as a TLS handshake failure.
|
||||
//
|
||||
// We have no good way to distinguish between these cases.
|
||||
//
|
||||
// Wait a short time for the leader's connection (or some
|
||||
// other usable connection) to be delivered to our RoundTrip.
|
||||
// If it is not, start a dial of our own.
|
||||
timerc = time.After(coalescedDialRetryTimeout)
|
||||
}
|
||||
select {
|
||||
case <-leaderdialc:
|
||||
// The leader's dial completed.
|
||||
if leader.dialErr != nil {
|
||||
return nil, leader.dialErr
|
||||
}
|
||||
leaderdialc = nil
|
||||
case <-leaderrtdonec:
|
||||
// The leader's RoundTrip completed.
|
||||
leaderrtdonec = nil
|
||||
case <-timerc:
|
||||
// The leader's dial and RoundTrip completed, and some time
|
||||
// has passed. Assume we're not getting a connection and
|
||||
// restart the dial process.
|
||||
done = true
|
||||
case <-dial.rtdonec:
|
||||
// Our RoundTrip returned.
|
||||
return nil, errCoalescedDialAbandoned
|
||||
case <-gotconnc:
|
||||
// Our RoundTrip got a connection.
|
||||
return nil, errCoalescedDialAbandoned
|
||||
case <-ctx.Done():
|
||||
// net/http abandoned the dial.
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dial for real.
|
||||
tlsConf := t.TLSClientConfig
|
||||
if tlsConf == nil {
|
||||
tlsConf = &tls.Config{}
|
||||
} else {
|
||||
tlsConf = tlsConf.Clone()
|
||||
}
|
||||
if !slices.Contains(tlsConf.NextProtos, "h2") {
|
||||
tlsConf.NextProtos = append([]string{"h2"}, tlsConf.NextProtos...)
|
||||
}
|
||||
if tlsConf.ServerName == "" {
|
||||
host, _, err := net.SplitHostPort(address)
|
||||
if err == nil {
|
||||
tlsConf.ServerName = host
|
||||
}
|
||||
}
|
||||
nc, err := t.dialTLS(ctx, network, address, tlsConf)
|
||||
|
||||
// Remove this dial from t.dials, so future dial attempts will not coalesce with it.
|
||||
t.dialMu.Lock()
|
||||
delete(t.dials, address)
|
||||
t.dialMu.Unlock()
|
||||
|
||||
// Notify any dial coalesced with this one that we are done.
|
||||
state.dialErr = err
|
||||
close(state.dialc)
|
||||
|
||||
return nc, err
|
||||
}
|
||||
|
||||
func (t *Transport) closeIdleConnections() {
|
||||
t1 := t.init()
|
||||
t1.CloseIdleConnections()
|
||||
|
||||
+4
-3
@@ -79,10 +79,11 @@ func (wr FrameWriteRequest) Consume(n int32) (FrameWriteRequest, FrameWriteReque
|
||||
}
|
||||
|
||||
// Might need to split after applying limits.
|
||||
allowed := wr.stream.flow.available()
|
||||
if n < allowed {
|
||||
allowed = n
|
||||
avail, ok := wr.stream.flow.available()
|
||||
if !ok {
|
||||
return empty, empty, 0
|
||||
}
|
||||
allowed := min(n, avail)
|
||||
if wr.stream.sc.maxFrameSize < allowed {
|
||||
allowed = wr.stream.sc.maxFrameSize
|
||||
}
|
||||
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
// Copyright 2026 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package httpcommon
|
||||
|
||||
import (
|
||||
"compress/flate"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var errConcurrentRead = errors.New("http: concurrent read on response body")
|
||||
|
||||
// incomparable is a zero-width, non-comparable type. Adding it to a struct
|
||||
// makes that struct also non-comparable, and generally doesn't add
|
||||
// any size (as long as it's first).
|
||||
type incomparable [0]func()
|
||||
|
||||
// GzipReader wraps a response body so it can lazily
|
||||
// get gzip.Reader from the pool on the first call to Read.
|
||||
// After Close is called it puts gzip.Reader to the pool immediately
|
||||
// if there is no Read in progress or later when Read completes.
|
||||
type GzipReader struct {
|
||||
_ incomparable
|
||||
Body io.ReadCloser // underlying Response.Body
|
||||
mu sync.Mutex // guards zr and zerr
|
||||
zr *gzip.Reader // stores gzip reader from the pool between reads
|
||||
zerr error // sticky gzip reader init error or sentinel value to detect concurrent read and read after close
|
||||
}
|
||||
|
||||
type eofReader struct{}
|
||||
|
||||
func (eofReader) Read([]byte) (int, error) { return 0, io.EOF }
|
||||
func (eofReader) ReadByte() (byte, error) { return 0, io.EOF }
|
||||
|
||||
var gzipPool = sync.Pool{New: func() any { return new(gzip.Reader) }}
|
||||
|
||||
// gzipPoolGet gets a gzip.Reader from the pool and resets it to read from r.
|
||||
func gzipPoolGet(r io.Reader) (*gzip.Reader, error) {
|
||||
zr := gzipPool.Get().(*gzip.Reader)
|
||||
if err := zr.Reset(r); err != nil {
|
||||
gzipPoolPut(zr)
|
||||
return nil, err
|
||||
}
|
||||
return zr, nil
|
||||
}
|
||||
|
||||
// gzipPoolPut puts a gzip.Reader back into the pool.
|
||||
func gzipPoolPut(zr *gzip.Reader) {
|
||||
// Reset will allocate bufio.Reader if we pass it anything
|
||||
// other than a flate.Reader, so ensure that it's getting one.
|
||||
var r flate.Reader = eofReader{}
|
||||
zr.Reset(r)
|
||||
gzipPool.Put(zr)
|
||||
}
|
||||
|
||||
// acquire returns a gzip.Reader for reading response body.
|
||||
// The reader must be released after use.
|
||||
func (gz *GzipReader) acquire() (*gzip.Reader, error) {
|
||||
gz.mu.Lock()
|
||||
defer gz.mu.Unlock()
|
||||
if gz.zerr != nil {
|
||||
return nil, gz.zerr
|
||||
}
|
||||
if gz.zr == nil {
|
||||
// gzipPoolGet might block indefinitely since it reads the gzip header.
|
||||
// Therefore, drop mu temporarily when using gzipPoolGet.
|
||||
// We set zerr to errConcurrentRead to prevent concurrent read
|
||||
// even when mu is temporarily dropped.
|
||||
gz.zerr = errConcurrentRead
|
||||
gz.mu.Unlock()
|
||||
zr, err := gzipPoolGet(gz.Body)
|
||||
gz.mu.Lock()
|
||||
// Guard against Close being called while gzipPoolGet is running.
|
||||
if gz.zerr != errConcurrentRead {
|
||||
if zr != nil {
|
||||
gzipPoolPut(zr)
|
||||
}
|
||||
return nil, gz.zerr
|
||||
}
|
||||
gz.zr, gz.zerr = zr, err
|
||||
if gz.zerr != nil {
|
||||
return nil, gz.zerr
|
||||
}
|
||||
}
|
||||
ret := gz.zr
|
||||
gz.zr, gz.zerr = nil, errConcurrentRead
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// release returns the gzip.Reader to the pool if Close was called during Read.
|
||||
func (gz *GzipReader) release(zr *gzip.Reader) {
|
||||
gz.mu.Lock()
|
||||
defer gz.mu.Unlock()
|
||||
if gz.zerr == errConcurrentRead {
|
||||
gz.zr, gz.zerr = zr, nil
|
||||
} else { // fs.ErrClosed
|
||||
gzipPoolPut(zr)
|
||||
}
|
||||
}
|
||||
|
||||
// close returns the gzip.Reader to the pool immediately or
|
||||
// signals release to do so after Read completes.
|
||||
func (gz *GzipReader) close() {
|
||||
gz.mu.Lock()
|
||||
defer gz.mu.Unlock()
|
||||
if gz.zerr == nil && gz.zr != nil {
|
||||
gzipPoolPut(gz.zr)
|
||||
gz.zr = nil
|
||||
}
|
||||
gz.zerr = fs.ErrClosed
|
||||
}
|
||||
|
||||
func (gz *GzipReader) Read(p []byte) (n int, err error) {
|
||||
zr, err := gz.acquire()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer gz.release(zr)
|
||||
|
||||
return zr.Read(p)
|
||||
}
|
||||
|
||||
func (gz *GzipReader) Close() error {
|
||||
gz.close()
|
||||
|
||||
return gz.Body.Close()
|
||||
}
|
||||
Vendored
+7
-7
@@ -1,11 +1,11 @@
|
||||
# dario.cat/mergo v1.0.2
|
||||
## explicit; go 1.13
|
||||
dario.cat/mergo
|
||||
# github.com/Azure/go-ansiterm v0.0.0-20260917205352-e937bb47801a
|
||||
## explicit; go 1.16
|
||||
# github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4
|
||||
## explicit; go 1.17
|
||||
github.com/Azure/go-ansiterm
|
||||
github.com/Azure/go-ansiterm/winterm
|
||||
# github.com/Microsoft/go-winio v0.6.3-0.20260930231756-f19d9717deb0
|
||||
# github.com/Microsoft/go-winio v0.6.3
|
||||
## explicit; go 1.26.0
|
||||
github.com/Microsoft/go-winio
|
||||
github.com/Microsoft/go-winio/internal/fs
|
||||
@@ -71,7 +71,7 @@ github.com/docker/distribution/registry/storage/cache/memory
|
||||
## explicit; go 1.21
|
||||
github.com/docker/docker-credential-helpers/client
|
||||
github.com/docker/docker-credential-helpers/credentials
|
||||
# github.com/docker/go-connections v0.8.1
|
||||
# github.com/docker/go-connections v0.8.2
|
||||
## explicit; go 1.23
|
||||
github.com/docker/go-connections/nat
|
||||
github.com/docker/go-connections/sockets
|
||||
@@ -155,7 +155,7 @@ github.com/klauspost/compress/internal/le
|
||||
github.com/klauspost/compress/internal/snapref
|
||||
github.com/klauspost/compress/zstd
|
||||
github.com/klauspost/compress/zstd/internal/xxhash
|
||||
# github.com/mattn/go-runewidth v0.0.30
|
||||
# github.com/mattn/go-runewidth v0.0.31
|
||||
## explicit; go 1.23
|
||||
github.com/mattn/go-runewidth
|
||||
# github.com/moby/docker-image-spec v1.3.1
|
||||
@@ -189,7 +189,7 @@ github.com/moby/moby/api/types/storage
|
||||
github.com/moby/moby/api/types/swarm
|
||||
github.com/moby/moby/api/types/system
|
||||
github.com/moby/moby/api/types/volume
|
||||
# github.com/moby/moby/client v0.6.1
|
||||
# github.com/moby/moby/client v0.6.2
|
||||
## explicit; go 1.24
|
||||
github.com/moby/moby/client
|
||||
github.com/moby/moby/client/internal
|
||||
@@ -397,7 +397,7 @@ golang.org/x/mod/internal/lazyregexp
|
||||
golang.org/x/mod/modfile
|
||||
golang.org/x/mod/module
|
||||
golang.org/x/mod/semver
|
||||
# golang.org/x/net v0.59.0
|
||||
# golang.org/x/net v0.60.0
|
||||
## explicit; go 1.26.0
|
||||
golang.org/x/net/http/httpguts
|
||||
golang.org/x/net/http2
|
||||
|
||||
Reference in New Issue
Block a user