github.com/containerd/nerdctl/v2@v2.0.0-beta.5.0.20240520001846-b5758f54fa28/pkg/cmd/container/attach.go (about) 1 /* 2 Copyright The containerd Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package container 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 24 "github.com/containerd/console" 25 "github.com/containerd/containerd" 26 "github.com/containerd/containerd/cio" 27 "github.com/containerd/log" 28 "github.com/containerd/nerdctl/v2/pkg/api/types" 29 "github.com/containerd/nerdctl/v2/pkg/consoleutil" 30 "github.com/containerd/nerdctl/v2/pkg/errutil" 31 "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker" 32 "github.com/containerd/nerdctl/v2/pkg/signalutil" 33 ) 34 35 // Attach attaches stdin, stdout, and stderr to a running container. 36 func Attach(ctx context.Context, client *containerd.Client, req string, options types.ContainerAttachOptions) error { 37 // Find the container. 38 var container containerd.Container 39 walker := &containerwalker.ContainerWalker{ 40 Client: client, 41 OnFound: func(ctx context.Context, found containerwalker.Found) error { 42 container = found.Container 43 return nil 44 }, 45 } 46 n, err := walker.Walk(ctx, req) 47 if err != nil { 48 return fmt.Errorf("error when trying to find the container: %w", err) 49 } 50 if n == 0 { 51 return fmt.Errorf("no container is found given the string: %s", req) 52 } else if n > 1 { 53 return fmt.Errorf("more than one containers are found given the string: %s", req) 54 } 55 56 // Attach to the container. 57 var task containerd.Task 58 detachC := make(chan struct{}) 59 spec, err := container.Spec(ctx) 60 if err != nil { 61 return fmt.Errorf("failed to get the OCI runtime spec for the container: %w", err) 62 } 63 var ( 64 opt cio.Opt 65 con console.Console 66 ) 67 if spec.Process.Terminal { 68 con = console.Current() 69 defer con.Reset() 70 if err := con.SetRaw(); err != nil { 71 return fmt.Errorf("failed to set the console to raw mode: %w", err) 72 } 73 closer := func() { 74 detachC <- struct{}{} 75 // task will be set by container.Task later. 76 // 77 // We cannot use container.Task(ctx, cio.Load) to get the IO here 78 // because the `cancel` field of the returned `*cio` is nil. [1] 79 // 80 // [1] https://github.com/containerd/containerd/blob/8f756bc8c26465bd93e78d9cd42082b66f276e10/cio/io.go#L358-L359 81 io := task.IO() 82 if io == nil { 83 log.G(ctx).Errorf("got a nil io") 84 return 85 } 86 io.Cancel() 87 } 88 in, err := consoleutil.NewDetachableStdin(con, options.DetachKeys, closer) 89 if err != nil { 90 return err 91 } 92 opt = cio.WithStreams(in, con, nil) 93 } else { 94 opt = cio.WithStreams(options.Stdin, options.Stdout, options.Stderr) 95 } 96 task, err = container.Task(ctx, cio.NewAttach(opt)) 97 if err != nil { 98 return fmt.Errorf("failed to attach to the container: %w", err) 99 } 100 if spec.Process.Terminal { 101 if err := consoleutil.HandleConsoleResize(ctx, task, con); err != nil { 102 log.G(ctx).WithError(err).Error("console resize") 103 } 104 } 105 sigC := signalutil.ForwardAllSignals(ctx, task) 106 defer signalutil.StopCatch(sigC) 107 108 // Wait for the container to exit. 109 statusC, err := task.Wait(ctx) 110 if err != nil { 111 return fmt.Errorf("failed to init an async wait for the container to exit: %w", err) 112 } 113 select { 114 // io.Wait() would return when either 1) the user detaches from the container OR 2) the container is about to exit. 115 // 116 // If we replace the `select` block with io.Wait() and 117 // directly use task.Status() to check the status of the container after io.Wait() returns, 118 // it can still be running even though the container is about to exit (somehow especially for Windows). 119 // 120 // As a result, we need a separate detachC to distinguish from the 2 cases mentioned above. 121 case <-detachC: 122 io := task.IO() 123 if io == nil { 124 return errors.New("got a nil IO from the task") 125 } 126 io.Wait() 127 case status := <-statusC: 128 code, _, err := status.Result() 129 if err != nil { 130 return err 131 } 132 if code != 0 { 133 return errutil.NewExitCoderErr(int(code)) 134 } 135 } 136 return nil 137 }