github.com/apptainer/singularity@v3.1.1+incompatible/internal/app/singularity/oci_attach_linux.go (about)

     1  // Copyright (c) 2018-2019, Sylabs Inc. All rights reserved.
     2  // This software is licensed under a 3-clause BSD license. Please consult the
     3  // LICENSE.md file distributed with the sources of this project regarding your
     4  // rights to use or distribute this software.
     5  
     6  package singularity
     7  
     8  import (
     9  	"encoding/json"
    10  	"fmt"
    11  	"io"
    12  	"io/ioutil"
    13  	"net"
    14  	"os"
    15  	osignal "os/signal"
    16  	"sync"
    17  	"syscall"
    18  
    19  	"github.com/kr/pty"
    20  
    21  	specs "github.com/opencontainers/runtime-spec/specs-go"
    22  	"github.com/sylabs/singularity/internal/pkg/runtime/engines/oci"
    23  	"github.com/sylabs/singularity/internal/pkg/sylog"
    24  	"github.com/sylabs/singularity/pkg/ociruntime"
    25  	"github.com/sylabs/singularity/pkg/util/unix"
    26  	"golang.org/x/crypto/ssh/terminal"
    27  )
    28  
    29  func resize(controlSocket string, oversized bool) {
    30  	ctrl := &ociruntime.Control{}
    31  	ctrl.ConsoleSize = &specs.Box{}
    32  
    33  	c, err := unix.Dial(controlSocket)
    34  	if err != nil {
    35  		sylog.Errorf("failed to connect to control socket")
    36  		return
    37  	}
    38  	defer c.Close()
    39  
    40  	rows, cols, err := pty.Getsize(os.Stdin)
    41  	if err != nil {
    42  		sylog.Errorf("terminal resize error: %s", err)
    43  		return
    44  	}
    45  
    46  	ctrl.ConsoleSize.Height = uint(rows)
    47  	ctrl.ConsoleSize.Width = uint(cols)
    48  
    49  	if oversized {
    50  		ctrl.ConsoleSize.Height++
    51  		ctrl.ConsoleSize.Width++
    52  	}
    53  
    54  	enc := json.NewEncoder(c)
    55  	if err != nil {
    56  		sylog.Errorf("%s", err)
    57  		return
    58  	}
    59  
    60  	if err := enc.Encode(ctrl); err != nil {
    61  		sylog.Errorf("%s", err)
    62  		return
    63  	}
    64  }
    65  
    66  func attach(engineConfig *oci.EngineConfig, run bool) error {
    67  	var ostate *terminal.State
    68  	var conn net.Conn
    69  	var wg sync.WaitGroup
    70  
    71  	state := &engineConfig.State
    72  
    73  	if state.AttachSocket == "" {
    74  		return fmt.Errorf("attach socket not available, container state: %s", state.Status)
    75  	}
    76  	if state.ControlSocket == "" {
    77  		return fmt.Errorf("control socket not available, container state: %s", state.Status)
    78  	}
    79  
    80  	hasTerminal := engineConfig.OciConfig.Process.Terminal
    81  	if hasTerminal && !terminal.IsTerminal(0) {
    82  		return fmt.Errorf("attach requires a terminal when terminal config is set to true")
    83  	}
    84  
    85  	var err error
    86  	conn, err = unix.Dial(state.AttachSocket)
    87  	if err != nil {
    88  		return err
    89  	}
    90  	defer conn.Close()
    91  
    92  	if hasTerminal {
    93  		ostate, _ = terminal.MakeRaw(0)
    94  		resize(state.ControlSocket, true)
    95  		resize(state.ControlSocket, false)
    96  	}
    97  
    98  	wg.Add(1)
    99  
   100  	go func() {
   101  		// catch SIGWINCH signal for terminal resize
   102  		signals := make(chan os.Signal, 1)
   103  		pid := state.Pid
   104  		osignal.Notify(signals)
   105  
   106  		for {
   107  			s := <-signals
   108  			switch s {
   109  			case syscall.SIGWINCH:
   110  				if hasTerminal {
   111  					resize(state.ControlSocket, false)
   112  				}
   113  			default:
   114  				syscall.Kill(pid, s.(syscall.Signal))
   115  			}
   116  		}
   117  	}()
   118  
   119  	if hasTerminal || !run {
   120  		// Pipe session to bash and visa-versa
   121  		go func() {
   122  			io.Copy(os.Stdout, conn)
   123  			wg.Done()
   124  		}()
   125  		go func() {
   126  			io.Copy(conn, os.Stdin)
   127  		}()
   128  		wg.Wait()
   129  
   130  		if hasTerminal {
   131  			fmt.Printf("\r")
   132  			return terminal.Restore(0, ostate)
   133  		}
   134  		return nil
   135  	}
   136  
   137  	io.Copy(ioutil.Discard, conn)
   138  	return nil
   139  }
   140  
   141  // OciAttach attaches console to a running container
   142  func OciAttach(containerID string) error {
   143  	engineConfig, err := getEngineConfig(containerID)
   144  	if err != nil {
   145  		return err
   146  	}
   147  	if engineConfig.GetState().Status != ociruntime.Running {
   148  		return fmt.Errorf("could not attach to %s: not in running state", containerID)
   149  	}
   150  
   151  	defer exitContainer(containerID, false)
   152  
   153  	return attach(engineConfig, false)
   154  }