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: ®isterServiceFlag, 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 }