1+ //go:build windows
2+
13/*
24 Copyright The containerd Authors.
35
@@ -18,41 +20,234 @@ package shim
1820
1921import (
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.
3475func 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.
3881func 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.
49121func 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.
53138func 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