vspd/cmd/vspd/main.go
jholdstock 1a2b02466c vspd: Wrap RPC connection details in a struct.
Returning a single struct which contains multiple named fields reduces
the chance of a mistake in the calling code, as compared to returning
multiple unnamed values which are all of the same type.
2024-05-30 08:41:10 +01:00

192 lines
5.1 KiB
Go

// Copyright (c) 2020-2024 The Decred 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"
"runtime"
"sync"
"time"
"github.com/decred/dcrd/wire"
"github.com/decred/slog"
"github.com/decred/vspd/database"
"github.com/decred/vspd/internal/config"
"github.com/decred/vspd/internal/signal"
"github.com/decred/vspd/internal/version"
"github.com/decred/vspd/internal/vspd"
"github.com/decred/vspd/internal/webapi"
"github.com/decred/vspd/rpc"
)
const (
// maxVoteChangeRecords defines how many vote change records will be stored
// for each ticket. The limit is in place to mitigate DoS attacks on server
// storage space. When storing a new record breaches this limit, the oldest
// record in the database is deleted.
maxVoteChangeRecords = 10
)
func main() {
os.Exit(run())
}
// initLogging uses the provided vspd config to create a logging backend, and
// returns a function which can be used to create ready-to-use subsystem
// loggers.
func initLogging(cfg *vspd.Config) (func(subsystem string) slog.Logger, error) {
backend, err := newLogBackend(cfg.LogDir(), "vspd", cfg.MaxLogSize, cfg.LogsToKeep)
if err != nil {
return nil, fmt.Errorf("failed to initialize logger: %w", err)
}
var ok bool
level, ok := slog.LevelFromString(cfg.LogLevel)
if !ok {
return nil, fmt.Errorf("unknown log level: %q", cfg.LogLevel)
}
return func(subsystem string) slog.Logger {
log := backend.Logger(subsystem)
log.SetLevel(level)
return log
}, nil
}
// run is the real main function for vspd. It is necessary to work around the
// fact that deferred functions do not run when os.Exit() is called.
func run() int {
// Load config file and parse CLI args.
cfg, err := vspd.LoadConfig()
if err != nil {
fmt.Fprintf(os.Stderr, "loadConfig error: %v\n", err)
return 1
}
makeLogger, err := initLogging(cfg)
if err != nil {
fmt.Fprintf(os.Stderr, "initLogging error: %v\n", err)
return 1
}
log := makeLogger("VSP")
// Create a context that is canceled when a shutdown request is received
// through an interrupt signal such as SIGINT (Ctrl+C).
ctx := signal.ShutdownListener(log)
defer log.Criticalf("Shutdown complete")
log.Criticalf("Version %s (Go version %s %s/%s)", version.String(),
runtime.Version(), runtime.GOOS, runtime.GOARCH)
network := cfg.Network()
if network == &config.MainNet && version.IsPreRelease() {
log.Warnf("")
log.Warnf("\tWARNING: This is a pre-release version of vspd which should not be used on mainnet")
log.Warnf("")
}
if cfg.VspClosed {
log.Warnf("")
log.Warnf("\tWARNING: Config --vspclosed is set. This will prevent vspd from accepting new tickets")
log.Warnf("")
}
if cfg.ConfigFile != "" {
log.Warnf("")
log.Warnf("\tWARNING: Config --configfile is set. This is a deprecated option which has no effect and will be removed in a future release")
log.Warnf("")
}
// Open database.
db, err := database.Open(cfg.DatabaseFile(), makeLogger(" DB"), maxVoteChangeRecords)
if err != nil {
log.Errorf("Failed to open database: %v", err)
return 1
}
const writeBackup = true
defer db.Close(writeBackup)
rpcLog := makeLogger("RPC")
// Create a channel to receive blockConnected notifications from dcrd.
blockNotifChan := make(chan *wire.BlockHeader)
// Create RPC client for local dcrd instance (used for broadcasting and
// checking the status of fee transactions).
dd := cfg.DcrdDetails()
dcrd := rpc.SetupDcrd(dd.User, dd.Password, dd.Host, dd.Cert, network.Params, rpcLog, blockNotifChan)
defer dcrd.Close()
// Create RPC client for remote dcrwallet instances (used for voting).
wd := cfg.WalletDetails()
wallets := rpc.SetupWallet(wd.Users, wd.Passwords, wd.Hosts, wd.Certs, network.Params, rpcLog)
defer wallets.Close()
// Create webapi server.
apiCfg := webapi.Config{
Listen: cfg.Listen,
VSPFee: cfg.VSPFee,
Network: network,
SupportEmail: cfg.SupportEmail,
VspClosed: cfg.VspClosed,
VspClosedMsg: cfg.VspClosedMsg,
AdminPass: cfg.AdminPass,
Debug: cfg.WebServerDebug,
Designation: cfg.Designation,
MaxVoteChangeRecords: maxVoteChangeRecords,
VspdVersion: version.String(),
}
api, err := webapi.New(db, makeLogger("API"), dcrd, wallets, apiCfg)
if err != nil {
log.Errorf("Failed to initialize webapi: %v", err)
return 1
}
// WaitGroup for services to signal when they have shutdown cleanly.
var wg sync.WaitGroup
// Start the webapi server.
wg.Add(1)
go func() {
api.Run(ctx)
wg.Done()
}()
// Start vspd.
vspd := vspd.New(network, log, db, dcrd, wallets, blockNotifChan)
wg.Add(1)
go func() {
vspd.Run(ctx)
wg.Done()
}()
// Periodically write a database backup file.
wg.Add(1)
go func() {
for {
select {
case <-ctx.Done():
wg.Done()
return
case <-time.After(cfg.BackupInterval):
err := db.WriteHotBackupFile()
if err != nil {
log.Errorf("Failed to write database backup: %v", err)
}
}
}
}()
// Wait for shutdown tasks to complete before running deferred tasks and
// returning.
wg.Wait()
return 0
}