Allow running Nimbus as a Windows service (--run-as-service)

This commit is contained in:
Zahary Karadjov 2022-02-27 13:02:45 +02:00
parent cdeae90806
commit e6723ddb24
No known key found for this signature in database
GPG Key ID: C8936F8A3073D609
5 changed files with 252 additions and 119 deletions

View File

@ -44,6 +44,11 @@ const
defaultAdminListenAddress* = (static ValidIpAddress.init("127.0.0.1"))
defaultSigningNodeRequestTimeout* = 60
when defined(windows):
{.pragma: windowsOnly.}
else:
{.pragma: windowsOnly, hidden.}
type
BNStartUpCmd* {.pure.} = enum
noCommand
@ -66,9 +71,6 @@ type
# status = "Displays status information about all deposits"
exit = "Submits a validator voluntary exit"
VCStartUpCmd* = enum
VCNoCommand
SNStartUpCmd* = enum
SNNoCommand
@ -193,6 +195,12 @@ type
defaultValue: BNStartUpCmd.noCommand }: BNStartUpCmd
of BNStartUpCmd.noCommand:
runAsServiceFlag* {.
windowsOnly
defaultValue: false,
desc: "Run as a Windows service"
name: "run-as-service" }: bool
bootstrapNodes* {.
desc: "Specifies one or more bootstrap nodes to use when connecting to the network"
abbr: "b"
@ -743,25 +751,20 @@ type
desc: "A file specifying the authorizition token required for accessing the keymanager API"
name: "keymanager-token-file" }: Option[InputFile]
case cmd* {.
command
defaultValue: VCNoCommand }: VCStartUpCmd
graffiti* {.
desc: "The graffiti value that will appear in proposed blocks. " &
"You can use a 0x-prefixed hex encoded string to specify " &
"raw bytes"
name: "graffiti" }: Option[GraffitiBytes]
of VCNoCommand:
graffiti* {.
desc: "The graffiti value that will appear in proposed blocks. " &
"You can use a 0x-prefixed hex encoded string to specify " &
"raw bytes"
name: "graffiti" }: Option[GraffitiBytes]
stopAtEpoch* {.
desc: "A positive epoch selects the epoch at which to stop"
defaultValue: 0
name: "stop-at-epoch" }: uint64
stopAtEpoch* {.
desc: "A positive epoch selects the epoch at which to stop"
defaultValue: 0
name: "stop-at-epoch" }: uint64
beaconNodes* {.
desc: "URL addresses to one or more beacon node HTTP REST APIs",
name: "beacon-node" }: seq[string]
beaconNodes* {.
desc: "URL addresses to one or more beacon node HTTP REST APIs",
name: "beacon-node" }: seq[string]
SigningNodeConf* = object
configFile* {.
@ -812,35 +815,30 @@ type
defaultValue: defaultSigningNodeRequestTimeout
name: "request-timeout" }: int
case cmd* {.
command
defaultValue: SNNoCommand }: SNStartUpCmd
bindPort* {.
desc: "Port for the REST (BETA version) HTTP server"
defaultValue: DefaultEth2RestPort
defaultValueDesc: "5052"
name: "bind-port" }: Port
of SNNoCommand:
bindPort* {.
desc: "Port for the REST (BETA version) HTTP server"
defaultValue: DefaultEth2RestPort
defaultValueDesc: "5052"
name: "bind-port" }: Port
bindAddress* {.
desc: "Listening address of the REST (BETA version) HTTP server"
defaultValue: defaultAdminListenAddress
defaultValueDesc: "127.0.0.1"
name: "bind-address" }: ValidIpAddress
bindAddress* {.
desc: "Listening address of the REST (BETA version) HTTP server"
defaultValue: defaultAdminListenAddress
defaultValueDesc: "127.0.0.1"
name: "bind-address" }: ValidIpAddress
tlsEnabled* {.
desc: "Use secure TLS communication for REST (BETA version) server"
defaultValue: false
name: "tls" }: bool
tlsEnabled* {.
desc: "Use secure TLS communication for REST (BETA version) server"
defaultValue: false
name: "tls" }: bool
tlsCertificate* {.
desc: "Path to SSL certificate file"
name: "tls-cert" }: Option[InputFile]
tlsCertificate* {.
desc: "Path to SSL certificate file"
name: "tls-cert" }: Option[InputFile]
tlsPrivateKey* {.
desc: "Path to SSL ceritificate's private key"
name: "tls-key" }: Option[InputFile]
tlsPrivateKey* {.
desc: "Path to SSL ceritificate's private key"
name: "tls-key" }: Option[InputFile]
AnyConf* = BeaconNodeConf | ValidatorClientConf | SigningNodeConf
@ -1006,6 +1004,9 @@ func outWalletFile*(config: BeaconNodeConf): Option[OutFile] =
func databaseDir*(config: AnyConf): string =
config.dataDir / "db"
func runAsService*(config: BeaconNodeConf): bool =
config.cmd == noCommand and config.runAsServiceFlag
template writeValue*(writer: var JsonWriter,
value: TypedInputFile|InputFile|InputDir|OutPath|OutDir|OutFile) =
writer.writeValue(string value)

View File

@ -29,6 +29,64 @@ from
import
TopicParams, validateParameters, init
when defined(windows):
import winlean
type
LPCSTR* = cstring
LPSTR* = cstring
SERVICE_STATUS* {.final, pure.} = object
dwServiceType*: DWORD
dwCurrentState*: DWORD
dwControlsAccepted*: DWORD
dwWin32ExitCode*: DWORD
dwServiceSpecificExitCode*: DWORD
dwCheckPoint*: DWORD
dwWaitHint*: DWORD
SERVICE_STATUS_HANDLE* = DWORD
LPSERVICE_STATUS* = ptr SERVICE_STATUS
LPSERVICE_MAIN_FUNCTION* = proc (para1: DWORD, para2: LPSTR) {.stdcall.}
SERVICE_TABLE_ENTRY* {.final, pure.} = object
lpServiceName*: LPSTR
lpServiceProc*: LPSERVICE_MAIN_FUNCTION
LPSERVICE_TABLE_ENTRY* = ptr SERVICE_TABLE_ENTRY
LPHANDLER_FUNCTION* = proc (para1: DWORD): WINBOOL{.stdcall.}
const
SERVICE_WIN32_OWN_PROCESS = 16
SERVICE_RUNNING = 4
SERVICE_STOPPED = 1
SERVICE_START_PENDING = 2
SERVICE_STOP_PENDING = 3
SERVICE_CONTROL_STOP = 1
SERVICE_CONTROL_PAUSE = 2
SERVICE_CONTROL_CONTINUE = 3
SERVICE_CONTROL_INTERROGATE = 4
SERVICE_ACCEPT_STOP = 1
NO_ERROR = 0
SERVICE_NAME = LPCSTR "NIMBUS_BEACON_NODE"
var
gSvcStatusHandle: SERVICE_STATUS_HANDLE
gSvcStatus: SERVICE_STATUS
proc reportServiceStatus*(dwCurrentState, dwWin32ExitCode, dwWaitHint: DWORD) {.gcsafe.}
proc StartServiceCtrlDispatcher*(lpServiceStartTable: LPSERVICE_TABLE_ENTRY): WINBOOL{.
stdcall, dynlib: "advapi32", importc: "StartServiceCtrlDispatcherA".}
proc SetServiceStatus*(hServiceStatus: SERVICE_STATUS_HANDLE,
lpServiceStatus: LPSERVICE_STATUS): WINBOOL{.stdcall,
dynlib: "advapi32", importc: "SetServiceStatus".}
proc RegisterServiceCtrlHandler*(lpServiceName: LPCSTR,
lpHandlerProc: LPHANDLER_FUNCTION): SERVICE_STATUS_HANDLE{.
stdcall, dynlib: "advapi32", importc: "RegisterServiceCtrlHandlerA".}
type
RpcServer = RpcHttpServer
@ -1088,6 +1146,10 @@ proc onSlotStart(
# Check before any re-scheduling of onSlotStart()
checkIfShouldStopAtEpoch(wallSlot, node.config.stopAtEpoch)
when defined(windows):
if node.config.runAsService:
reportServiceStatus(SERVICE_RUNNING, NO_ERROR, 0)
beacon_slot.set wallSlot.toGaugeValue
beacon_current_epoch.set wallSlot.epoch.toGaugeValue
@ -1727,7 +1789,96 @@ proc doSlashingInterchange(conf: BeaconNodeConf) {.raises: [Defect, CatchableErr
of SlashProtCmd.`import`:
conf.doSlashingImport()
proc handleStartUpCmd(config: var BeaconNodeConf) {.raises: [Defect, CatchableError].} =
# Single RNG instance for the application - will be seeded on construction
# and avoid using system resources (such as urandom) after that
let rng = keys.newRng()
case config.cmd
of BNStartUpCmd.createTestnet: doCreateTestnet(config, rng[])
of BNStartUpCmd.noCommand: doRunBeaconNode(config, rng)
of BNStartUpCmd.deposits: doDeposits(config, rng[])
of BNStartUpCmd.wallets: doWallets(config, rng[])
of BNStartUpCmd.record: doRecord(config, rng[])
of BNStartUpCmd.web3: doWeb3Cmd(config)
of BNStartUpCmd.slashingdb: doSlashingInterchange(config)
of BNStartupCmd.trustedNodeSync:
let
network = loadEth2Network(config)
cfg = network.cfg
genesis =
if network.genesisData.len > 0:
newClone(readSszForkedHashedBeaconState(
cfg,
network.genesisData.toOpenArrayByte(0, network.genesisData.high())))
else: nil
waitFor doTrustedNodeSync(
cfg,
config.databaseDir,
config.trustedNodeUrl,
config.blockId,
config.backfillBlocks,
genesis)
{.pop.} # TODO moduletests exceptions
when defined(windows):
proc reportServiceStatus*(dwCurrentState, dwWin32ExitCode, dwWaitHint: DWORD) {.gcsafe.} =
gSvcStatus.dwCurrentState = dwCurrentState
gSvcStatus.dwWin32ExitCode = dwWin32ExitCode
gSvcStatus.dwWaitHint = dwWaitHint
if dwCurrentState == SERVICE_START_PENDING:
gSvcStatus.dwControlsAccepted = 0
else:
gSvcStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP
# TODO
# We can use non-zero values for the `dwCheckPoint` parameter to report
# progress during lengthy operations such as start-up and shut down.
gSvcStatus.dwCheckPoint = 0
# Report the status of the service to the SCM.
let status = SetServiceStatus(gSvcStatusHandle, addr gSvcStatus)
debug "Service status updated", status
proc serviceControlHandler(dwCtrl: DWORD): WINBOOL {.stdcall.} =
case dwCtrl
of SERVICE_CONTROL_STOP:
# We re reporting that we plan stop the service in 10 seconds
reportServiceStatus(SERVICE_STOP_PENDING, NO_ERROR, 10_000)
bnStatus = BeaconNodeStatus.Stopping
of SERVICE_CONTROL_PAUSE, SERVICE_CONTROL_CONTINUE:
warn "The Nimbus service cannot be paused and resimed"
of SERVICE_CONTROL_INTERROGATE:
# The default behavior is correct.
# The service control manager will report our last status.
discard
else:
debug "Service received an unexpected user-defined control message",
msg = dwCtrl
proc serviceMainFunction(dwArgc: DWORD, lpszArgv: LPSTR) {.stdcall.} =
# The service is launched in a fresh thread created by Windows, so
# we must initialize the Nim GC here
setupForeignThreadGc()
gSvcStatusHandle = RegisterServiceCtrlHandler(
SERVICE_NAME,
serviceControlHandler)
gSvcStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS
gSvcStatus.dwServiceSpecificExitCode = 0
reportServiceStatus(SERVICE_RUNNING, NO_ERROR, 0)
info "Service thread started"
var config = makeBannerAndConfig(clientId, BeaconNodeConf)
handleStartUpCmd(config)
info "Service thread stopped"
reportServiceStatus(SERVICE_STOPPED, NO_ERROR, 0) # we have to report back when we stopped!
programMain:
var
config = makeBannerAndConfig(clientId, BeaconNodeConf)
@ -1762,33 +1913,18 @@ programMain:
quit 0
c_signal(SIGTERM, exitImmediatelyOnSIGTERM)
# Single RNG instance for the application - will be seeded on construction
# and avoid using system resources (such as urandom) after that
let rng = keys.newRng()
when defined(windows):
if config.runAsService:
var dispatchTable = [
SERVICE_TABLE_ENTRY(lpServiceName: SERVICE_NAME, lpServiceProc: serviceMainFunction),
SERVICE_TABLE_ENTRY(lpServiceName: nil, lpServiceProc: nil) # last entry must be nil
]
case config.cmd
of BNStartUpCmd.createTestnet: doCreateTestnet(config, rng[])
of BNStartUpCmd.noCommand: doRunBeaconNode(config, rng)
of BNStartUpCmd.deposits: doDeposits(config, rng[])
of BNStartUpCmd.wallets: doWallets(config, rng[])
of BNStartUpCmd.record: doRecord(config, rng[])
of BNStartUpCmd.web3: doWeb3Cmd(config)
of BNStartUpCmd.slashingdb: doSlashingInterchange(config)
of BNStartupCmd.trustedNodeSync:
let
network = loadEth2Network(config)
cfg = network.cfg
genesis =
if network.genesisData.len > 0:
newClone(readSszForkedHashedBeaconState(
cfg,
network.genesisData.toOpenArrayByte(0, network.genesisData.high())))
else: nil
waitFor doTrustedNodeSync(
cfg,
config.databaseDir,
config.trustedNodeUrl,
config.blockId,
config.backfillBlocks,
genesis)
let status = StartServiceCtrlDispatcher(LPSERVICE_TABLE_ENTRY(addr dispatchTable[0]))
if status == 0:
fatal "Failed to start Windows service", errorCode = getLastError()
quit 1
else:
handleStartUpCmd(config)
else:
handleStartUpCmd(config)

View File

@ -332,17 +332,15 @@ programMain:
SigningNodeConf)
setupLogging(config.logLevel, config.logStdout, config.logFile)
case config.cmd
of SNNoCommand:
var sn = SigningNode.init(config)
notice "Launching signing node", version = fullVersionStr,
cmdParams = commandLineParams(), config,
validators_count = sn.attachedValidators.count()
sn.installApiHandlers()
sn.start()
try:
runForever()
finally:
waitFor sn.stop()
waitFor sn.close()
discard sn.stop()
var sn = SigningNode.init(config)
notice "Launching signing node", version = fullVersionStr,
cmdParams = commandLineParams(), config,
validators_count = sn.attachedValidators.count()
sn.installApiHandlers()
sn.start()
try:
runForever()
finally:
waitFor sn.stop()
waitFor sn.close()
discard sn.stop()

View File

@ -165,37 +165,35 @@ programMain:
setupLogging(config.logLevel, config.logStdout, config.logFile)
case config.cmd
of VCNoCommand:
let beaconNodes =
block:
var servers: seq[BeaconNodeServerRef]
let flags = {RestClientFlag.CommaSeparatedArray}
for url in config.beaconNodes:
let res = RestClientRef.new(url, flags = flags)
if res.isErr():
warn "Unable to resolve remote beacon node server's hostname",
url = url
else:
servers.add(BeaconNodeServerRef(client: res.get(), endpoint: url))
servers
let beaconNodes =
block:
var servers: seq[BeaconNodeServerRef]
let flags = {RestClientFlag.CommaSeparatedArray}
for url in config.beaconNodes:
let res = RestClientRef.new(url, flags = flags)
if res.isErr():
warn "Unable to resolve remote beacon node server's hostname",
url = url
else:
servers.add(BeaconNodeServerRef(client: res.get(), endpoint: url))
servers
if len(beaconNodes) == 0:
fatal "Not enough beacon nodes in command line"
quit 1
if len(beaconNodes) == 0:
fatal "Not enough beacon nodes in command line"
quit 1
notice "Launching validator client", version = fullVersionStr,
cmdParams = commandLineParams(),
config,
beacon_nodes_count = len(beaconNodes)
notice "Launching validator client", version = fullVersionStr,
cmdParams = commandLineParams(),
config,
beacon_nodes_count = len(beaconNodes)
var vc = ValidatorClientRef(
config: config,
beaconNodes: beaconNodes,
graffitiBytes: config.graffiti.get(defaultGraffitiBytes()),
nodesAvailable: newAsyncEvent(),
forksAvailable: newAsyncEvent()
)
var vc = ValidatorClientRef(
config: config,
beaconNodes: beaconNodes,
graffitiBytes: config.graffiti.get(defaultGraffitiBytes()),
nodesAvailable: newAsyncEvent(),
forksAvailable: newAsyncEvent()
)
waitFor asyncInit(vc)
waitFor asyncRun(vc)
waitFor asyncInit(vc)
waitFor asyncRun(vc)

@ -1 +1 @@
Subproject commit 0fc26c5b25a931fdd15a74f2d9028112ffd621ba
Subproject commit 0a88d30e0035ad761b41886251149d54e65a916a