mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-17 18:19:04 +02:00
464 lines
9.3 KiB
Go
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)
|
|
})
|
|
}
|