diff --git a/README.md b/README.md index fa375763..f82acd1c 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/btcd.go b/btcd.go index a7e05a60..5b5121cd 100644 --- a/btcd.go +++ b/btcd.go @@ -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) } } diff --git a/config.go b/config.go index 5f941a17..79aa7891 100644 --- a/config.go +++ b/config.go @@ -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 =,=,... 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 } diff --git a/service_windows.go b/service_windows.go new file mode 100644 index 00000000..f81920e7 --- /dev/null +++ b/service_windows.go @@ -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 +}