// 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 } // Set windows specific functions to real functions. func init() { runServiceCommand = performServiceCommand winServiceMain = serviceMain }