mirror of
https://github.com/LBRYFoundation/lbcd.git
synced 2025-08-23 17:47:24 +00:00
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.
320 lines
8.8 KiB
Go
320 lines
8.8 KiB
Go
// 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
|
|
}
|