github.com/demonoid81/containerd@v1.3.4/cmd/containerd/command/service_windows.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 command
    18  
    19  import (
    20  	"fmt"
    21  	"io/ioutil"
    22  	"log"
    23  	"os"
    24  	"os/exec"
    25  	"path/filepath"
    26  	"time"
    27  	"unsafe"
    28  
    29  	"github.com/containerd/containerd/errdefs"
    30  	"github.com/containerd/containerd/services/server"
    31  	"github.com/pkg/errors"
    32  	"github.com/sirupsen/logrus"
    33  	"github.com/urfave/cli"
    34  	"golang.org/x/sys/windows"
    35  	"golang.org/x/sys/windows/svc"
    36  	"golang.org/x/sys/windows/svc/debug"
    37  	"golang.org/x/sys/windows/svc/mgr"
    38  )
    39  
    40  var (
    41  	serviceNameFlag       string
    42  	registerServiceFlag   bool
    43  	unregisterServiceFlag bool
    44  	runServiceFlag        bool
    45  	logFileFlag           string
    46  
    47  	kernel32     = windows.NewLazySystemDLL("kernel32.dll")
    48  	setStdHandle = kernel32.NewProc("SetStdHandle")
    49  	allocConsole = kernel32.NewProc("AllocConsole")
    50  	oldStderr    windows.Handle
    51  	panicFile    *os.File
    52  
    53  	service *handler
    54  )
    55  
    56  const defaultServiceName = "containerd"
    57  
    58  // serviceFlags returns an array of flags for configuring containerd to run
    59  // as a Windows service under control of SCM.
    60  func serviceFlags() []cli.Flag {
    61  	return []cli.Flag{
    62  		cli.StringFlag{
    63  			Name:  "service-name",
    64  			Usage: "Set the Windows service name",
    65  			Value: defaultServiceName,
    66  		},
    67  		cli.BoolFlag{
    68  			Name:  "register-service",
    69  			Usage: "Register the service and exit",
    70  		},
    71  		cli.BoolFlag{
    72  			Name:  "unregister-service",
    73  			Usage: "Unregister the service and exit",
    74  		},
    75  		cli.BoolFlag{
    76  			Name:   "run-service",
    77  			Usage:  "",
    78  			Hidden: true,
    79  		},
    80  		cli.StringFlag{
    81  			Name:  "log-file",
    82  			Usage: "Path to the containerd log file",
    83  		},
    84  	}
    85  }
    86  
    87  // applyPlatformFlags applies platform-specific flags.
    88  func applyPlatformFlags(context *cli.Context) {
    89  	serviceNameFlag = context.GlobalString("service-name")
    90  	if serviceNameFlag == "" {
    91  		serviceNameFlag = defaultServiceName
    92  	}
    93  	for _, v := range []struct {
    94  		name string
    95  		d    *bool
    96  	}{
    97  		{
    98  			name: "register-service",
    99  			d:    &registerServiceFlag,
   100  		},
   101  		{
   102  			name: "unregister-service",
   103  			d:    &unregisterServiceFlag,
   104  		},
   105  		{
   106  			name: "run-service",
   107  			d:    &runServiceFlag,
   108  		},
   109  	} {
   110  		*v.d = context.GlobalBool(v.name)
   111  	}
   112  	logFileFlag = context.GlobalString("log-file")
   113  }
   114  
   115  type handler struct {
   116  	fromsvc chan error
   117  	s       *server.Server
   118  	done    chan struct{} // Indicates back to app main to quit
   119  }
   120  
   121  func getServicePath() (string, error) {
   122  	p, err := exec.LookPath(os.Args[0])
   123  	if err != nil {
   124  		return "", err
   125  	}
   126  	return filepath.Abs(p)
   127  }
   128  
   129  func registerService() error {
   130  	p, err := getServicePath()
   131  	if err != nil {
   132  		return err
   133  	}
   134  	m, err := mgr.Connect()
   135  	if err != nil {
   136  		return err
   137  	}
   138  	defer m.Disconnect()
   139  
   140  	c := mgr.Config{
   141  		ServiceType:  windows.SERVICE_WIN32_OWN_PROCESS,
   142  		StartType:    mgr.StartAutomatic,
   143  		ErrorControl: mgr.ErrorNormal,
   144  		DisplayName:  "Containerd",
   145  		Description:  "Container runtime",
   146  	}
   147  
   148  	// Configure the service to launch with the arguments that were just passed.
   149  	args := []string{"--run-service"}
   150  	for _, a := range os.Args[1:] {
   151  		if a != "--register-service" && a != "--unregister-service" {
   152  			args = append(args, a)
   153  		}
   154  	}
   155  
   156  	s, err := m.CreateService(serviceNameFlag, p, c, args...)
   157  	if err != nil {
   158  		return err
   159  	}
   160  	defer s.Close()
   161  
   162  	// See http://stackoverflow.com/questions/35151052/how-do-i-configure-failure-actions-of-a-windows-service-written-in-go
   163  	const (
   164  		scActionNone    = 0
   165  		scActionRestart = 1
   166  
   167  		serviceConfigFailureActions = 2
   168  	)
   169  
   170  	type serviceFailureActions struct {
   171  		ResetPeriod  uint32
   172  		RebootMsg    *uint16
   173  		Command      *uint16
   174  		ActionsCount uint32
   175  		Actions      uintptr
   176  	}
   177  
   178  	type scAction struct {
   179  		Type  uint32
   180  		Delay uint32
   181  	}
   182  	t := []scAction{
   183  		{Type: scActionRestart, Delay: uint32(15 * time.Second / time.Millisecond)},
   184  		{Type: scActionRestart, Delay: uint32(15 * time.Second / time.Millisecond)},
   185  		{Type: scActionNone},
   186  	}
   187  	lpInfo := serviceFailureActions{ResetPeriod: uint32(24 * time.Hour / time.Second), ActionsCount: uint32(3), Actions: uintptr(unsafe.Pointer(&t[0]))}
   188  	err = windows.ChangeServiceConfig2(s.Handle, serviceConfigFailureActions, (*byte)(unsafe.Pointer(&lpInfo)))
   189  	if err != nil {
   190  		return err
   191  	}
   192  
   193  	return nil
   194  }
   195  
   196  func unregisterService() error {
   197  	m, err := mgr.Connect()
   198  	if err != nil {
   199  		return err
   200  	}
   201  	defer m.Disconnect()
   202  
   203  	s, err := m.OpenService(serviceNameFlag)
   204  	if err != nil {
   205  		return err
   206  	}
   207  	defer s.Close()
   208  
   209  	err = s.Delete()
   210  	if err != nil {
   211  		return err
   212  	}
   213  	return nil
   214  }
   215  
   216  // registerUnregisterService is an entrypoint early in the daemon startup
   217  // to handle (un-)registering against Windows Service Control Manager (SCM).
   218  // It returns an indication to stop on successful SCM operation, and an error.
   219  func registerUnregisterService(root string) (bool, error) {
   220  
   221  	if unregisterServiceFlag {
   222  		if registerServiceFlag {
   223  			return true, errors.Wrap(errdefs.ErrInvalidArgument, "--register-service and --unregister-service cannot be used together")
   224  		}
   225  		return true, unregisterService()
   226  	}
   227  
   228  	if registerServiceFlag {
   229  		return true, registerService()
   230  	}
   231  
   232  	if runServiceFlag {
   233  		// Allocate a conhost for containerd here. We don't actually use this
   234  		// at all in containerd, but it will be inherited by any processes
   235  		// containerd executes, so they won't need to allocate their own
   236  		// conhosts. This is important for two reasons:
   237  		// - Creating a conhost slows down process launch.
   238  		// - We have seen reliability issues when launching many processes.
   239  		//   Sometimes the process invocation will fail due to an error when
   240  		//   creating the conhost.
   241  		//
   242  		// This needs to be done before initializing the panic file, as
   243  		// AllocConsole sets the stdio handles to point to the new conhost,
   244  		// and we want to make sure stderr goes to the panic file.
   245  		r, _, err := allocConsole.Call()
   246  		if r == 0 && err != nil {
   247  			return true, fmt.Errorf("error allocating conhost: %s", err)
   248  		}
   249  
   250  		if err := initPanicFile(filepath.Join(root, "panic.log")); err != nil {
   251  			return true, err
   252  		}
   253  
   254  		logOutput := ioutil.Discard
   255  		if logFileFlag != "" {
   256  			f, err := os.OpenFile(logFileFlag, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
   257  			if err != nil {
   258  				return true, errors.Wrapf(err, "open log file %q", logFileFlag)
   259  			}
   260  			logOutput = f
   261  		}
   262  		logrus.SetOutput(logOutput)
   263  	}
   264  	return false, nil
   265  }
   266  
   267  // launchService is the entry point for running the daemon under SCM.
   268  func launchService(s *server.Server, done chan struct{}) error {
   269  
   270  	if !runServiceFlag {
   271  		return nil
   272  	}
   273  
   274  	h := &handler{
   275  		fromsvc: make(chan error),
   276  		s:       s,
   277  		done:    done,
   278  	}
   279  
   280  	interactive, err := svc.IsAnInteractiveSession()
   281  	if err != nil {
   282  		return err
   283  	}
   284  
   285  	service = h
   286  	go func() {
   287  		if interactive {
   288  			err = debug.Run(serviceNameFlag, h)
   289  		} else {
   290  			err = svc.Run(serviceNameFlag, h)
   291  		}
   292  		h.fromsvc <- err
   293  	}()
   294  
   295  	// Wait for the first signal from the service handler.
   296  	err = <-h.fromsvc
   297  	if err != nil {
   298  		return err
   299  	}
   300  	return nil
   301  }
   302  
   303  func (h *handler) Execute(_ []string, r <-chan svc.ChangeRequest, s chan<- svc.Status) (bool, uint32) {
   304  	s <- svc.Status{State: svc.StartPending, Accepts: 0}
   305  	// Unblock launchService()
   306  	h.fromsvc <- nil
   307  
   308  	s <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown | svc.Accepted(windows.SERVICE_ACCEPT_PARAMCHANGE)}
   309  
   310  Loop:
   311  	for c := range r {
   312  		switch c.Cmd {
   313  		case svc.Interrogate:
   314  			s <- c.CurrentStatus
   315  		case svc.Stop, svc.Shutdown:
   316  			s <- svc.Status{State: svc.StopPending, Accepts: 0}
   317  			h.s.Stop()
   318  			break Loop
   319  		}
   320  	}
   321  
   322  	removePanicFile()
   323  	close(h.done)
   324  	return false, 0
   325  }
   326  
   327  func initPanicFile(path string) error {
   328  	var err error
   329  	panicFile, err = os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0)
   330  	if err != nil {
   331  		return err
   332  	}
   333  
   334  	st, err := panicFile.Stat()
   335  	if err != nil {
   336  		return err
   337  	}
   338  
   339  	// If there are contents in the file already, move the file out of the way
   340  	// and replace it.
   341  	if st.Size() > 0 {
   342  		panicFile.Close()
   343  		os.Rename(path, path+".old")
   344  		panicFile, err = os.Create(path)
   345  		if err != nil {
   346  			return err
   347  		}
   348  	}
   349  
   350  	// Update STD_ERROR_HANDLE to point to the panic file so that Go writes to
   351  	// it when it panics. Remember the old stderr to restore it before removing
   352  	// the panic file.
   353  	sh := windows.STD_ERROR_HANDLE
   354  	h, err := windows.GetStdHandle(uint32(sh))
   355  	if err != nil {
   356  		return err
   357  	}
   358  
   359  	oldStderr = h
   360  
   361  	r, _, err := setStdHandle.Call(uintptr(sh), panicFile.Fd())
   362  	if r == 0 && err != nil {
   363  		return err
   364  	}
   365  
   366  	// Reset os.Stderr to the panic file (so fmt.Fprintf(os.Stderr,...) actually gets redirected)
   367  	os.Stderr = os.NewFile(panicFile.Fd(), "/dev/stderr")
   368  
   369  	// Force threads that panic to write to stderr (the panicFile handle now), otherwise it will go into the ether
   370  	log.SetOutput(os.Stderr)
   371  
   372  	return nil
   373  }
   374  
   375  func removePanicFile() {
   376  	if st, err := panicFile.Stat(); err == nil {
   377  		if st.Size() == 0 {
   378  			sh := windows.STD_ERROR_HANDLE
   379  			setStdHandle.Call(uintptr(sh), uintptr(oldStderr))
   380  			panicFile.Close()
   381  			os.Remove(panicFile.Name())
   382  		}
   383  	}
   384  }