Files
windows_exporter/internal/headers/cri/pipe_internal_test.go
T
2026-10-08 23:11:10 +02:00

464 lines
9.3 KiB
Go

// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package cri
import (
"errors"
"fmt"
"io"
"net"
"os"
"sync"
"sync/atomic"
"testing"
"testing/synctest"
"time"
"github.com/stretchr/testify/require"
"golang.org/x/sys/windows"
)
// pipeCounter makes pipe names unique, the wall clock is too coarse for parallel tests.
var pipeCounter atomic.Int64 //nolint:gochecknoglobals
// pipeListener accepts connections on a named pipe without buffer, like the one containerd creates.
// Like go-winio, it always keeps one pipe instance waiting, so clients never see a missing pipe.
type pipeListener struct {
path string
mu sync.Mutex
next windows.Handle
closed chan struct{}
}
func listenPipe(t *testing.T) *pipeListener {
t.Helper()
l := &pipeListener{
path: fmt.Sprintf(`\\.\pipe\windows_exporter-cri-test-%d-%d`, os.Getpid(), pipeCounter.Add(1)),
closed: make(chan struct{}),
}
var err error
l.next, err = l.createInstance()
require.NoError(t, err)
t.Cleanup(func() { _ = l.Close() })
return l
}
func (l *pipeListener) createInstance() (windows.Handle, error) {
name, err := windows.UTF16PtrFromString(l.path)
if err != nil {
return 0, err
}
return windows.CreateNamedPipe(
name,
windows.PIPE_ACCESS_DUPLEX|windows.FILE_FLAG_OVERLAPPED,
windows.PIPE_TYPE_BYTE|windows.PIPE_READMODE_BYTE|windows.PIPE_WAIT,
windows.PIPE_UNLIMITED_INSTANCES,
0, // no output buffer
0, // no input buffer
0,
nil,
)
}
func (l *pipeListener) Accept() (net.Conn, error) {
l.mu.Lock()
handle := l.next
l.next = 0
l.mu.Unlock()
if handle == 0 {
return nil, net.ErrClosed
}
if err := l.connect(handle); err != nil {
_ = windows.CloseHandle(handle)
return nil, err
}
next, err := l.createInstance()
if err != nil {
_ = windows.CloseHandle(handle)
return nil, err
}
l.mu.Lock()
select {
case <-l.closed:
_ = windows.CloseHandle(next)
default:
l.next = next
}
l.mu.Unlock()
return &pipeConn{File: os.NewFile(uintptr(handle), l.path)}, nil
}
// connect waits for a client to connect, or for the listener to be closed.
func (l *pipeListener) connect(handle windows.Handle) error {
event, err := windows.CreateEvent(nil, 1, 0, nil)
if err != nil {
return err
}
defer func() {
_ = windows.CloseHandle(event)
}()
overlapped := windows.Overlapped{HEvent: event}
err = windows.ConnectNamedPipe(handle, &overlapped)
if errors.Is(err, windows.ERROR_PIPE_CONNECTED) {
return nil
} else if !errors.Is(err, windows.ERROR_IO_PENDING) {
return err
}
for {
select {
case <-l.closed:
_ = windows.CancelIoEx(handle, &overlapped)
_, _ = windows.WaitForSingleObject(event, windows.INFINITE)
return net.ErrClosed
default:
}
result, err := windows.WaitForSingleObject(event, 50)
if err != nil {
return err
}
if result == windows.WAIT_OBJECT_0 {
return nil
}
}
}
func (l *pipeListener) Close() error {
l.mu.Lock()
defer l.mu.Unlock()
select {
case <-l.closed:
return nil
default:
}
close(l.closed)
if l.next != 0 {
_ = windows.CloseHandle(l.next)
l.next = 0
}
return nil
}
func (l *pipeListener) Addr() net.Addr { return pipeAddr(l.path) }
func TestPipePath(t *testing.T) {
t.Parallel()
for endpoint, want := range map[string]string{
"npipe:////./pipe/containerd-containerd": `\\.\pipe\containerd-containerd`,
`npipe://\\.\pipe\containerd-containerd`: `\\.\pipe\containerd-containerd`,
"npipe:////./pipe/": "",
"npipe://./pipe/containerd-containerd": "",
"unix:///run/containerd/containerd.sock": "",
`\\.\pipe\containerd-containerd`: "",
} {
got, err := pipePath(endpoint)
if want == "" {
require.Error(t, err, endpoint)
continue
}
require.NoError(t, err, endpoint)
require.Equal(t, want, got, endpoint)
}
}
// Closing a pipe must cancel an event-based read while Control holds the handle.
func TestPipeConnClose(t *testing.T) {
t.Parallel()
l := listenPipe(t)
accepted := make(chan net.Conn, 1)
acceptErr := make(chan error, 1)
go func() {
conn, err := l.Accept()
if err != nil {
acceptErr <- err
return
}
accepted <- conn
}()
client, err := dialPipe(t.Context(), l.path)
require.NoError(t, err)
t.Cleanup(func() { _ = client.Close() })
var server net.Conn
select {
case server = <-accepted:
case err := <-acceptErr:
t.Fatal(err)
case <-time.After(5 * time.Second):
t.Fatal("pipe was not accepted")
}
t.Cleanup(func() { _ = server.Close() })
readErr := make(chan error, 1)
go func() {
_, err := server.Read(make([]byte, 1))
readErr <- err
}()
// Sending a byte proves the reader can complete an overlapped operation.
require.NoError(t, client.SetWriteDeadline(time.Now().Add(5*time.Second)))
_, err = client.Write([]byte{1})
require.NoError(t, err)
select {
case err := <-readErr:
require.NoError(t, err)
case <-time.After(5 * time.Second):
t.Fatal("pipe read did not complete")
}
go func() {
_, err := server.Read(make([]byte, 1))
readErr <- err
}()
closed := make(chan error, 1)
go func() { closed <- server.Close() }()
select {
case err := <-readErr:
require.ErrorIs(t, err, os.ErrClosed)
case <-time.After(5 * time.Second):
t.Fatal("pending pipe read was not canceled by Close")
}
select {
case err := <-closed:
require.NoError(t, err)
case <-time.After(5 * time.Second):
t.Fatal("Close did not release the pipe handle")
}
// A read started after Close must return the same error as a canceled read.
n, err := server.Read(make([]byte, 1))
require.Zero(t, n)
require.ErrorIs(t, err, os.ErrClosed)
}
// Both peers write first. On a pipe without buffer, this blocks both writes
// until the peer reads, unless the client reads ahead.
func TestReadAheadConnPreventsDeadlock(t *testing.T) {
t.Parallel()
l := listenPipe(t)
type result struct {
conn net.Conn
err error
}
accepted := make(chan result, 1)
go func() {
conn, err := l.Accept()
accepted <- result{conn, err}
}()
var (
client net.Conn
err error
)
require.Eventually(t, func() bool {
client, err = dialPipe(t.Context(), l.path)
return err == nil
}, 5*time.Second, 10*time.Millisecond)
defer client.Close()
server := <-accepted
require.NoError(t, server.err)
defer server.conn.Close()
done := make(chan error, 2)
go func() {
// Server: write the preface, then read the peer's.
if _, err := server.conn.Write([]byte("server-preface")); err != nil {
done <- err
return
}
buf := make([]byte, len("client-preface"))
_, err := io.ReadFull(server.conn, buf)
done <- err
}()
go func() {
// Client: write the preface, then read the peer's.
if _, err := client.Write([]byte("client-preface")); err != nil {
done <- err
return
}
buf := make([]byte, len("server-preface"))
if _, err := io.ReadFull(client, buf); err != nil {
done <- err
return
}
if string(buf) != "server-preface" {
done <- fmt.Errorf("unexpected preface %q", buf)
return
}
done <- nil
}()
for range 2 {
select {
case err := <-done:
require.NoError(t, err)
case <-time.After(5 * time.Second):
t.Fatal("deadlock: both peers are blocked writing")
}
}
}
// Closing the connection must stop the background reader and fail pending reads.
func TestReadAheadConnClose(t *testing.T) {
t.Parallel()
client, server := net.Pipe()
defer server.Close()
conn := newReadAheadConn(client)
readErr := make(chan error, 1)
go func() {
_, err := conn.Read(make([]byte, 1))
readErr <- err
}()
require.NoError(t, conn.Close())
select {
case err := <-readErr:
require.ErrorIs(t, err, io.ErrClosedPipe)
case <-time.After(5 * time.Second):
t.Fatal("pending read was not unblocked by Close")
}
}
// delayedReadConn keeps a canceled read active until the test releases it.
type delayedReadConn struct {
net.Conn
started chan struct{}
canceled chan struct{}
finish <-chan struct{}
}
func (c *delayedReadConn) Read(p []byte) (int, error) {
close(c.started)
n, err := c.Conn.Read(p)
close(c.canceled)
<-c.finish
return n, err
}
func TestReadAheadConnCloseWaitsForReader(t *testing.T) {
t.Parallel()
synctest.Test(t, func(t *testing.T) {
client, server := net.Pipe()
defer server.Close()
finish := make(chan struct{})
release := sync.OnceFunc(func() { close(finish) })
defer release()
reader := &delayedReadConn{
Conn: client,
started: make(chan struct{}),
canceled: make(chan struct{}),
finish: finish,
}
conn := newReadAheadConn(reader)
<-reader.started
closed := make(chan error, 1)
go func() { closed <- conn.Close() }()
<-reader.canceled
synctest.Wait()
select {
case <-closed:
t.Fatal("Close returned before the background read finished")
default:
}
release()
require.NoError(t, <-closed)
_, err := conn.Read(make([]byte, 1))
require.ErrorIs(t, err, io.ErrClosedPipe)
})
}