Skip to content

Commit fa57661

Browse files
committed
patch the internal/shim package to work with Windows shims
This commit adds the changes to ensure that upstream pkg/shim works with Windows shims. Signed-off-by: Harsh Rawat <harshrawat@microsoft.com>
1 parent c6db54f commit fa57661

8 files changed

Lines changed: 729 additions & 27 deletions

File tree

internal/shim/publisher.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//go:build windows
2+
13
/*
24
Copyright The containerd Authors.
35
@@ -18,6 +20,7 @@ package shim
1820

1921
import (
2022
"context"
23+
"errors"
2124
"sync"
2225
"time"
2326

@@ -157,7 +160,7 @@ func (l *RemoteEventsPublisher) forwardRequest(ctx context.Context, req *v1.Forw
157160
}
158161
}
159162

160-
if err != ttrpc.ErrClosed {
163+
if !errors.Is(err, ttrpc.ErrClosed) {
161164
return err
162165
}
163166

internal/shim/shim.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//go:build windows
2+
13
/*
24
Copyright The containerd Authors.
35
@@ -263,7 +265,9 @@ func run(ctx context.Context, manager Manager, config Config) error {
263265
if debugFlag {
264266
logger.Logger.SetLevel(log.DebugLevel)
265267
}
266-
go reap(ctx, logger, signals)
268+
go func() {
269+
_ = reap(ctx, logger, signals)
270+
}()
267271
ss, err := manager.Stop(ctx, id)
268272
if err != nil {
269273
return err
@@ -453,13 +457,25 @@ func serve(ctx context.Context, server *ttrpc.Server, signals chan os.Signal, sh
453457
if err != nil {
454458
return err
455459
}
460+
461+
serrs := make(chan error, 1)
462+
defer close(serrs)
456463
go func() {
457464
defer l.Close()
458465
if err := server.Serve(ctx, l); err != nil && !errors.Is(err, net.ErrClosed) {
459466
log.G(ctx).WithError(err).Fatal("containerd-shim: ttrpc server failure")
467+
serrs <- err
468+
return
460469
}
470+
serrs <- nil
461471
}()
462472

473+
// Notify the parent process that the shim is ready.
474+
// On Windows this signals a named event; on Unix this is a no-op.
475+
if err = notifyReady(ctx, serrs); err != nil {
476+
return err
477+
}
478+
463479
if debugFlag && pprof != nil {
464480
if err := setupPprof(ctx, pprof); err != nil {
465481
log.G(ctx).WithError(err).Warn("Could not setup pprof")

internal/shim/shim_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//go:build windows
2+
13
/*
24
Copyright The containerd Authors.
35

internal/shim/shim_windows.go

Lines changed: 206 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//go:build windows
2+
13
/*
24
Copyright The containerd Authors.
35
@@ -18,41 +20,234 @@ package shim
1820

1921
import (
2022
"context"
23+
"fmt"
2124
"io"
2225
"net"
2326
"os"
27+
"os/signal"
28+
"strings"
29+
"sync"
30+
"syscall"
31+
"time"
2432

25-
"github.com/containerd/errdefs"
33+
winio "github.com/Microsoft/go-winio"
34+
"github.com/containerd/containerd/v2/pkg/namespaces"
2635
"github.com/containerd/log"
2736
"github.com/containerd/ttrpc"
37+
"golang.org/x/sys/windows"
2838
)
2939

30-
func setupSignals(config Config) (chan os.Signal, error) {
31-
return nil, errdefs.ErrNotImplemented
40+
// notifyReady signals the parent process that the shim server is ready.
41+
// On Windows, this closes stdout and sets a named event that the parent
42+
// process is waiting on to know the shim has started successfully.
43+
func notifyReady(_ context.Context, serrs chan error) error {
44+
select {
45+
case err := <-serrs:
46+
return err
47+
case <-time.After(2 * time.Millisecond):
48+
// This is our best indication that we have not errored on creation
49+
// and are successfully serving the API.
50+
os.Stdout.Close()
51+
eventName, _ := windows.UTF16PtrFromString(fmt.Sprintf("%s-%s", namespaceFlag, id))
52+
// Open the existing event and set it to wake up the parent process which is waiting for the shim to be ready.
53+
handle, err := windows.OpenEvent(windows.EVENT_MODIFY_STATE, false, eventName)
54+
if err == nil {
55+
_ = windows.SetEvent(handle) // Wake up the parent
56+
_ = windows.CloseHandle(handle) // Clean up
57+
}
58+
}
59+
return nil
60+
}
61+
62+
// setupSignals creates a signal channel for Windows.
63+
// On Windows, we don't register any signals here because:
64+
// 1. Child process reaping (SIGCHLD) is not needed - the OS handles it.
65+
// 2. Exit signals (SIGINT/SIGTERM) are handled by handleExitSignals separately.
66+
// We return an empty channel that reap() can use, but it won't receive signals.
67+
func setupSignals(_ Config) (chan os.Signal, error) {
68+
signals := make(chan os.Signal, 32)
69+
return signals, nil
3270
}
3371

72+
// newServer creates a new ttrpc server for Windows.
73+
// Unlike Unix, Windows doesn't have user-based socket authentication,
74+
// so we create a basic ttrpc server without the handshaker.
3475
func newServer(opts ...ttrpc.ServerOpt) (*ttrpc.Server, error) {
35-
return nil, errdefs.ErrNotImplemented
76+
return ttrpc.NewServer(opts...)
3677
}
3778

79+
// subreaper is not applicable on Windows as the OS automatically
80+
// handles orphaned processes differently than Unix systems.
3881
func subreaper() error {
39-
return errdefs.ErrNotImplemented
82+
// This is a no-op on Windows - the OS handles orphaned processes
83+
return nil
4084
}
4185

42-
func setupDumpStacks(dump chan<- os.Signal) {
86+
// setupDumpStacks is currently not implemented for Windows.
87+
// Windows doesn't have SIGUSR1, so stack dumping would need to use
88+
// a different mechanism (e.g., a named event or debug console).
89+
func setupDumpStacks(_ chan<- os.Signal) {
90+
// No-op on Windows - SIGUSR1 doesn't exist
91+
// Future: could implement using Windows events or console signals
4392
}
4493

45-
func serveListener(path string, fd uintptr) (net.Listener, error) {
46-
return nil, errdefs.ErrNotImplemented
94+
// serveListener creates a named pipe listener for Windows.
95+
// If path is provided, it creates a new named pipe at that location.
96+
// If path is empty and fd is provided, it attempts to inherit the listener (not commonly used on Windows).
97+
func serveListener(path string, _ uintptr) (net.Listener, error) {
98+
if path == "" {
99+
// On Windows, inheriting file descriptors is more complex and rarely used
100+
// with named pipes. We'll return an error if no path is provided.
101+
return nil, fmt.Errorf("named pipe path is required on Windows")
102+
}
103+
104+
// Ensure the path is in the correct Windows named pipe format
105+
// Expected format: \\.\pipe\<name>
106+
if !strings.HasPrefix(path, `\\.\pipe`) {
107+
return nil, fmt.Errorf("socket is required to be pipe address")
108+
}
109+
110+
l, err := winio.ListenPipe(path, nil)
111+
if err != nil {
112+
return nil, fmt.Errorf("failed to create named pipe listener at %s: %w", path, err)
113+
}
114+
115+
log.L.WithField("pipe", path).Debug("serving api on named pipe")
116+
return l, nil
47117
}
48118

119+
// reap handles signals on Windows. Unlike Unix, Windows doesn't send SIGCHLD
120+
// when child processes exit, so we only need to handle shutdown signals.
49121
func reap(ctx context.Context, logger *log.Entry, signals chan os.Signal) error {
50-
return errdefs.ErrNotImplemented
122+
logger.Debug("starting signal loop")
123+
124+
for {
125+
select {
126+
case <-ctx.Done():
127+
return ctx.Err()
128+
case s := <-signals:
129+
logger.WithField("signal", s).Debug("received signal in reap loop")
130+
// On Windows, we just log the signal
131+
// Exit signals are handled in handleExitSignals
132+
}
133+
}
51134
}
52135

136+
// handleExitSignals listens for shutdown signals (SIGINT, SIGTERM) and
137+
// triggers the provided cancel function for graceful shutdown.
53138
func handleExitSignals(ctx context.Context, logger *log.Entry, cancel context.CancelFunc) {
139+
ch := make(chan os.Signal, 32)
140+
// On Windows, os.Kill cannot be caught. We handle os.Interrupt (Ctrl+C) and SIGTERM.
141+
signal.Notify(ch, os.Interrupt, syscall.SIGTERM)
142+
143+
for {
144+
select {
145+
case s := <-ch:
146+
logger.WithField("signal", s).Debug("caught exit signal")
147+
cancel()
148+
return
149+
case <-ctx.Done():
150+
return
151+
}
152+
}
54153
}
55154

56-
func openLog(ctx context.Context, _ string) (io.Writer, error) {
57-
return nil, errdefs.ErrNotImplemented
155+
// openLog creates a named pipe for shim logging on Windows.
156+
// The containerd daemon connects to this pipe as a client to read log output.
157+
// The pipe format is: \\.\pipe\containerd-shim-{namespace}-{id}-log
158+
func openLog(ctx context.Context, id string) (io.Writer, error) {
159+
ns, err := namespaces.NamespaceRequired(ctx)
160+
if err != nil {
161+
return nil, err
162+
}
163+
pipePath := fmt.Sprintf("\\\\.\\pipe\\containerd-shim-%s-%s-log", ns, id)
164+
l, err := winio.ListenPipe(pipePath, nil)
165+
if err != nil {
166+
return nil, fmt.Errorf("failed to create shim log pipe: %w", err)
167+
}
168+
169+
rlw := &reconnectingLogWriter{
170+
l: l,
171+
}
172+
173+
// Accept connections from containerd in the background.
174+
// Supports reconnection if containerd restarts.
175+
go rlw.acceptConnections()
176+
177+
return rlw, nil
178+
}
179+
180+
// reconnectingLogWriter is a writer that accepts log connections from containerd.
181+
// It supports reconnection - if containerd restarts, a new connection is accepted
182+
// and the old one is closed. Logs generated during reconnection may be lost.
183+
type reconnectingLogWriter struct {
184+
l net.Listener // The named pipe listener waiting for connections
185+
mu sync.Mutex // Protects the current connection
186+
conn net.Conn // The current active connection (may be nil)
187+
}
188+
189+
// acceptConnections listens for log connections in the background.
190+
func (rlw *reconnectingLogWriter) acceptConnections() {
191+
for {
192+
newConn, err := rlw.l.Accept()
193+
if err != nil {
194+
// Listener was closed, stop accepting
195+
return
196+
}
197+
198+
rlw.mu.Lock()
199+
// Close the old connection if one exists
200+
if rlw.conn != nil {
201+
rlw.conn.Close()
202+
}
203+
rlw.conn = newConn
204+
rlw.mu.Unlock()
205+
}
206+
}
207+
208+
// Write implements io.Writer. It writes to the current connection if one exists.
209+
// If no connection is established yet, writes are silently dropped to avoid
210+
// blocking the shim.
211+
func (rlw *reconnectingLogWriter) Write(p []byte) (n int, err error) {
212+
rlw.mu.Lock()
213+
conn := rlw.conn
214+
rlw.mu.Unlock()
215+
216+
if conn == nil {
217+
// No connection yet, drop the log.
218+
return len(p), nil
219+
}
220+
221+
n, err = conn.Write(p)
222+
if err != nil {
223+
// Connection may have been closed, clear it so next write
224+
// doesn't try to use a broken connection
225+
rlw.mu.Lock()
226+
if rlw.conn == conn {
227+
rlw.conn.Close()
228+
rlw.conn = nil
229+
}
230+
rlw.mu.Unlock()
231+
// Return success anyway to avoid log write errors propagating
232+
return len(p), nil
233+
}
234+
return n, nil
235+
}
236+
237+
// Close implements io.Closer. It closes both the listener and any active connection.
238+
func (rlw *reconnectingLogWriter) Close() error {
239+
rlw.mu.Lock()
240+
defer rlw.mu.Unlock()
241+
242+
var err error
243+
if rlw.l != nil {
244+
err = rlw.l.Close()
245+
}
246+
if rlw.conn != nil {
247+
if cerr := rlw.conn.Close(); cerr != nil && err == nil {
248+
err = cerr
249+
}
250+
rlw.conn = nil
251+
}
252+
return err
58253
}

0 commit comments

Comments
 (0)