Allow btcd to run as a Windows service.

This commit modifies btcd to run cleanly as a Windows service.  btcd is
intended to be a long running process that stays synchronized with the
bitcoin block chain and provides chain services to multiple users.  It
follows that a service is the best option on Windows for this
functionality.

A few key points are:

- Supports graceful shutdown via the service stop/shutdown commands
- Integrates cleanly with the Windows event log
- Adds a new /s flag that can be used to install/remove/start/stop the
  service

One outstanding issue is that the application data directory is currently
user specific which means, by default, if you start btcd as a user, the
same data won't be used as when it's running as a service.  This needs to
be resovled.  The most likely approach will be to put all data into the
common appdata directory Windows provides, but it will require some
additional work to deal with permissions properly as user processes can't
write there by default.

Closes #42.
This commit is contained in:
Dave Collins 2013-11-20 09:35:14 -06:00
parent 766aae5a72
commit 45732c99fb
4 changed files with 387 additions and 14 deletions

View file

@ -87,7 +87,6 @@ are currently under heavy development:
- Documentation
- Code cleanup
- Add remaining missing RPC calls
- Add option to allow btcd run as a daemon/service
- Complete several TODO items in the code
- Offer cross-compiled binaries for popular OSes (Fedora, Ubuntu, FreeBSD, OpenBSD)

27
btcd.go
View file

@ -5,6 +5,7 @@
package main
import (
"fmt"
"net"
"net/http"
_ "net/http/pprof"
@ -19,8 +20,11 @@ var (
)
// btcdMain is the real main function for btcd. It is necessary to work around
// the fact that deferred functions do not run when os.Exit() is called.
func btcdMain() error {
// the fact that deferred functions do not run when os.Exit() is called. The
// optional serverChan parameter is mainly used by the service code to be
// notified with the server once it is setup so it can gracefully stop it when
// requested from the service control manager.
func btcdMain(serverChan chan<- *server) error {
// Initialize logging at the default logging level.
setLogLevels(defaultLogLevel)
defer backendLog.Flush()
@ -87,6 +91,9 @@ func btcdMain() error {
return err
}
server.Start()
if serverChan != nil {
serverChan <- server
}
// Monitor for graceful server shutdown and signal the main goroutine
// when done. This is done in a separate goroutine rather than waiting
@ -115,8 +122,22 @@ func main() {
os.Exit(1)
}
// Call serviceMain on Windows to handle running as a service. When
// the return isService flag is true, exit now since we ran as a
// service. Otherwise, just fall through to normal operation.
if runtime.GOOS == "windows" {
isService, err := serviceMain()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
if isService {
os.Exit(0)
}
}
// Work around defer not working after os.Exit()
if err := btcdMain(); err != nil {
if err := btcdMain(nil); err != nil {
os.Exit(1)
}
}

View file

@ -14,6 +14,7 @@ import (
"net"
"os"
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"
@ -75,6 +76,12 @@ type config struct {
DebugLevel string `short:"d" long:"debuglevel" description:"Logging level for all subsystems {trace, debug, info, warn, error, critical} -- You may also specify <subsystem>=<level>,<subsystem2>=<level>,... to set the log level for individual subsystems -- Use show to list available subsystems"`
}
// serviceOptions defines the configuration options for btcd as a service on
// Windows.
type serviceOptions struct {
ServiceCommand string `short:"s" long:"service" description:"Service command {install, remove, start, stop}"`
}
// cleanAndExpandPath expands environement variables and leading ~ in the
// passed path, cleans the result, and returns it.
func cleanAndExpandPath(path string) string {
@ -228,6 +235,15 @@ func fileExists(name string) bool {
return true
}
// newConfigParser returns a new command line flags parser.
func newConfigParser(cfg *config, so *serviceOptions, options flags.Options) *flags.Parser {
parser := flags.NewParser(cfg, options)
if runtime.GOOS == "windows" {
parser.AddGroup("Service Options", "Service Options", so)
}
return parser
}
// loadConfig initializes and parses the config using a config file and command
// line options.
//
@ -253,6 +269,13 @@ func loadConfig() (*config, []string, error) {
RPCCert: defaultRPCCertFile,
}
// Service options which are only added on Windows.
serviceOpts := serviceOptions{}
var runServiceCommand func(string) error
if runtime.GOOS == "windows" {
runServiceCommand = performServiceCommand
}
// Create the home directory if it doesn't already exist.
err := os.MkdirAll(btcdHomeDir, 0700)
if err != nil {
@ -264,7 +287,7 @@ func loadConfig() (*config, []string, error) {
// file or the version flag was specified. Any errors can be ignored
// here since they will be caught be the final parse below.
preCfg := cfg
preParser := flags.NewParser(&preCfg, flags.None)
preParser := newConfigParser(&preCfg, &serviceOpts, flags.None)
preParser.Parse()
// Show the version and exit if the version flag was specified.
@ -275,9 +298,20 @@ func loadConfig() (*config, []string, error) {
os.Exit(0)
}
// Perform service command and exit if specified. Invalid service
// commands show an appropriate error. Only runs on Windows since
// the runServiceCommand function will be nil when not on Windows.
if serviceOpts.ServiceCommand != "" && runServiceCommand != nil {
err := runServiceCommand(serviceOpts.ServiceCommand)
if err != nil {
fmt.Fprintln(os.Stderr, err)
}
os.Exit(0)
}
// Load additional config from file.
var configFileError error
parser := flags.NewParser(&cfg, flags.Default)
parser := newConfigParser(&cfg, &serviceOpts, flags.Default)
if !preCfg.RegressionTest || preCfg.ConfigFile != defaultConfigFile {
err := flags.NewIniParser(parser).ParseFile(preCfg.ConfigFile)
if err != nil {
@ -304,14 +338,6 @@ func loadConfig() (*config, []string, error) {
return nil, nil, err
}
// Warn about missing config file after the final command line parse
// succeeds. This prevents the warning on help messages and invalid
// options.
if configFileError != nil {
btcdLog.Warnf("%v", configFileError)
}
// The two test networks can't be selected simultaneously.
if cfg.TestNet3 && cfg.RegressionTest {
str := "%s: The testnet and regtest params can't be used " +
@ -459,5 +485,12 @@ func loadConfig() (*config, []string, error) {
cfg.ConnectPeers = normalizeAddresses(cfg.ConnectPeers,
activeNetParams.peerPort)
// Warn about missing config file after the final command line parse
// succeeds. This prevents the warning on help messages and invalid
// options.
if configFileError != nil {
btcdLog.Warnf("%v", configFileError)
}
return &cfg, remainingArgs, nil
}

320
service_windows.go Normal file
View file

@ -0,0 +1,320 @@
// Copyright (c) 2013 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package main
import (
"fmt"
"github.com/conformal/winsvc/eventlog"
"github.com/conformal/winsvc/mgr"
"github.com/conformal/winsvc/svc"
"os"
"path/filepath"
"time"
)
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}
// Stop the main server gracefully when it is
// already setup or just break out and allow
// the service to exit immediately if it's not
// setup yet. Note that calling Stop will cause
// btcdMain to exit in the goroutine above which
// will in turn send a signal (and a potential
// error) to doneChan.
if mainServer != nil {
mainServer.Stop()
} else {
break loop
}
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
}