btcd/service_windows.go
Dave Collins 044a11c9fc btcd: Simplify shutdown signal handling logic. (#733)
This rewrites the shutdown logic to simplify the shutdown signalling.
All cleanup is now run from deferred functions in the main function and
channels are used to signal shutdown either from OS signals or from
other subsystems such as the RPC server and windows service controller.

The RPC server has been modified to use a new channel for signalling
shutdown that is exposed via the RequestedProcessShutdown function
instead of directly calling Stop on the server as it previously did.

Finally, it adds a few checks for early termination during the main
start sequence so the process can be stopped without starting all the
subsystems if desired.

This is a backport of the equivalent logic from Decred with a few slight
modifications.  Credits go to @jrick.
2016-08-11 13:39:23 -05:00

318 lines
8.6 KiB
Go

// Copyright (c) 2013-2016 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package main
import (
"fmt"
"os"
"path/filepath"
"time"
"github.com/btcsuite/winsvc/eventlog"
"github.com/btcsuite/winsvc/mgr"
"github.com/btcsuite/winsvc/svc"
)
const (
// svcName is the name of btcd service.
svcName = "btcdsvc"
// svcDisplayName is the service name that will be shown in the windows
// services list. Not the svcName is the "real" name which is used
// to control the service. This is only for display purposes.
svcDisplayName = "Btcd Service"
// svcDesc is the description of the service.
svcDesc = "Downloads and stays synchronized with the bitcoin block " +
"chain and provides chain services to applications."
)
// elog is used to send messages to the Windows event log.
var elog *eventlog.Log
// logServiceStartOfDay logs information about btcd when the main server has
// been started to the Windows event log.
func logServiceStartOfDay(srvr *server) {
var message string
message += fmt.Sprintf("Version %s\n", version())
message += fmt.Sprintf("Configuration directory: %s\n", btcdHomeDir)
message += fmt.Sprintf("Configuration file: %s\n", cfg.ConfigFile)
message += fmt.Sprintf("Data directory: %s\n", cfg.DataDir)
elog.Info(1, message)
}
// btcdService houses the main service handler which handles all service
// updates and launching btcdMain.
type btcdService struct{}
// Execute is the main entry point the winsvc package calls when receiving
// information from the Windows service control manager. It launches the
// long-running btcdMain (which is the real meat of btcd), handles service
// change requests, and notifies the service control manager of changes.
func (s *btcdService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) {
// Service start is pending.
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown
changes <- svc.Status{State: svc.StartPending}
// Start btcdMain in a separate goroutine so the service can start
// quickly. Shutdown (along with a potential error) is reported via
// doneChan. serverChan is notified with the main server instance once
// it is started so it can be gracefully stopped.
doneChan := make(chan error)
serverChan := make(chan *server)
go func() {
err := btcdMain(serverChan)
doneChan <- err
}()
// Service is now started.
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
var mainServer *server
loop:
for {
select {
case c := <-r:
switch c.Cmd {
case svc.Interrogate:
changes <- c.CurrentStatus
case svc.Stop, svc.Shutdown:
// Service stop is pending. Don't accept any
// more commands while pending.
changes <- svc.Status{State: svc.StopPending}
// Signal the main function to exit.
shutdownRequestChannel <- struct{}{}
default:
elog.Error(1, fmt.Sprintf("Unexpected control "+
"request #%d.", c))
}
case srvr := <-serverChan:
mainServer = srvr
logServiceStartOfDay(mainServer)
case err := <-doneChan:
if err != nil {
elog.Error(1, err.Error())
}
break loop
}
}
// Service is now stopped.
changes <- svc.Status{State: svc.Stopped}
return false, 0
}
// installService attempts to install the btcd service. Typically this should
// be done by the msi installer, but it is provided here since it can be useful
// for development.
func installService() error {
// Get the path of the current executable. This is needed because
// os.Args[0] can vary depending on how the application was launched.
// For example, under cmd.exe it will only be the name of the app
// without the path or extension, but under mingw it will be the full
// path including the extension.
exePath, err := filepath.Abs(os.Args[0])
if err != nil {
return err
}
if filepath.Ext(exePath) == "" {
exePath += ".exe"
}
// Connect to the windows service manager.
serviceManager, err := mgr.Connect()
if err != nil {
return err
}
defer serviceManager.Disconnect()
// Ensure the service doesn't already exist.
service, err := serviceManager.OpenService(svcName)
if err == nil {
service.Close()
return fmt.Errorf("service %s already exists", svcName)
}
// Install the service.
service, err = serviceManager.CreateService(svcName, exePath, mgr.Config{
DisplayName: svcDisplayName,
Description: svcDesc,
})
if err != nil {
return err
}
defer service.Close()
// Support events to the event log using the standard "standard" Windows
// EventCreate.exe message file. This allows easy logging of custom
// messges instead of needing to create our own message catalog.
eventlog.Remove(svcName)
eventsSupported := uint32(eventlog.Error | eventlog.Warning | eventlog.Info)
err = eventlog.InstallAsEventCreate(svcName, eventsSupported)
if err != nil {
return err
}
return nil
}
// removeService attempts to uninstall the btcd service. Typically this should
// be done by the msi uninstaller, but it is provided here since it can be
// useful for development. Not the eventlog entry is intentionally not removed
// since it would invalidate any existing event log messages.
func removeService() error {
// Connect to the windows service manager.
serviceManager, err := mgr.Connect()
if err != nil {
return err
}
defer serviceManager.Disconnect()
// Ensure the service exists.
service, err := serviceManager.OpenService(svcName)
if err != nil {
return fmt.Errorf("service %s is not installed", svcName)
}
defer service.Close()
// Remove the service.
err = service.Delete()
if err != nil {
return err
}
return nil
}
// startService attempts to start the btcd service.
func startService() error {
// Connect to the windows service manager.
serviceManager, err := mgr.Connect()
if err != nil {
return err
}
defer serviceManager.Disconnect()
service, err := serviceManager.OpenService(svcName)
if err != nil {
return fmt.Errorf("could not access service: %v", err)
}
defer service.Close()
err = service.Start(os.Args)
if err != nil {
return fmt.Errorf("could not start service: %v", err)
}
return nil
}
// controlService allows commands which change the status of the service. It
// also waits for up to 10 seconds for the service to change to the passed
// state.
func controlService(c svc.Cmd, to svc.State) error {
// Connect to the windows service manager.
serviceManager, err := mgr.Connect()
if err != nil {
return err
}
defer serviceManager.Disconnect()
service, err := serviceManager.OpenService(svcName)
if err != nil {
return fmt.Errorf("could not access service: %v", err)
}
defer service.Close()
status, err := service.Control(c)
if err != nil {
return fmt.Errorf("could not send control=%d: %v", c, err)
}
// Send the control message.
timeout := time.Now().Add(10 * time.Second)
for status.State != to {
if timeout.Before(time.Now()) {
return fmt.Errorf("timeout waiting for service to go "+
"to state=%d", to)
}
time.Sleep(300 * time.Millisecond)
status, err = service.Query()
if err != nil {
return fmt.Errorf("could not retrieve service "+
"status: %v", err)
}
}
return nil
}
// performServiceCommand attempts to run one of the supported service commands
// provided on the command line via the service command flag. An appropriate
// error is returned if an invalid command is specified.
func performServiceCommand(command string) error {
var err error
switch command {
case "install":
err = installService()
case "remove":
err = removeService()
case "start":
err = startService()
case "stop":
err = controlService(svc.Stop, svc.Stopped)
default:
err = fmt.Errorf("invalid service command [%s]", command)
}
return err
}
// serviceMain checks whether we're being invoked as a service, and if so uses
// the service control manager to start the long-running server. A flag is
// returned to the caller so the application can determine whether to exit (when
// running as a service) or launch in normal interactive mode.
func serviceMain() (bool, error) {
// Don't run as a service if we're running interactively (or that can't
// be determined due to an error).
isInteractive, err := svc.IsAnInteractiveSession()
if err != nil {
return false, err
}
if isInteractive {
return false, nil
}
elog, err = eventlog.Open(svcName)
if err != nil {
return false, err
}
defer elog.Close()
err = svc.Run(svcName, &btcdService{})
if err != nil {
elog.Error(1, fmt.Sprintf("Service start failed: %v", err))
return true, err
}
return true, nil
}
// Set windows specific functions to real functions.
func init() {
runServiceCommand = performServiceCommand
winServiceMain = serviceMain
}