Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Installers for Windows and macOS #3441

Merged
merged 3 commits into from
Mar 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,7 @@ build/
/benchmark_results
/.update.timestamp
resttest0_data

# OSX
.DS_Store
**/.DS_Store
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,14 @@ clean-gnosis-chain:
### Other
###

nimbus-msi: | nimbus_beacon_node
"$(WIX)/bin/candle" -ext WixUIExtension -ext WixUtilExtension installer/windows/*.wxs -o installer/windows/obj/
"$(WIX)/bin/light" -ext WixUIExtension -ext WixUtilExtension -cultures:"en-us;en-uk;neutral" installer/windows/obj/*.wixobj -out build/NimbusBeaconNode.msi
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is "WIX" coming from? The environment?

You can do a Wix binary access check, like in the various sanity checks already in place.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WIX is an environment variable created by the WIX installer. Since our installer uses only relatively basic and old WIX functionalities, it should work with any WIX version.


nimbus-pkg: | nimbus_beacon_node
xcodebuild -project installer/macos/nimbus-pkg.xcodeproj -scheme nimbus-pkg build
packagesbuild installer/macos/nimbus-pkg.pkgproj

ctail: | build deps
mkdir -p vendor/.nimble/bin/
+ $(ENV_SCRIPT) nim -d:danger -o:vendor/.nimble/bin/ctail c vendor/nim-chronicles-tail/ctail.nim
Expand All @@ -480,7 +488,7 @@ ntu: | build deps
+ $(ENV_SCRIPT) nim -d:danger -o:vendor/.nimble/bin/ntu c vendor/nim-testutils/ntu.nim

clean: | clean-common
rm -rf build/{$(TOOLS_CSV),all_tests,test_*,proto_array,fork_choice,*.a,*.so,*_node,*ssz*,nimbus_*,beacon_node*,block_sim,state_sim,transition*,generate_makefile}
rm -rf build/{$(TOOLS_CSV),all_tests,test_*,proto_array,fork_choice,*.a,*.so,*_node,*ssz*,nimbus_*,beacon_node*,block_sim,state_sim,transition*,generate_makefile,nimbus-wix/obj}
ifneq ($(USE_LIBBACKTRACE), 0)
+ "$(MAKE)" -C vendor/nim-libbacktrace clean $(HANDLE_OUTPUT)
endif
Expand Down
91 changes: 46 additions & 45 deletions beacon_chain/conf.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -66,9 +71,6 @@ type
# status = "Displays status information about all deposits"
exit = "Submits a validator voluntary exit"

VCStartUpCmd* = enum
VCNoCommand

SNStartUpCmd* = enum
SNNoCommand

Expand Down Expand Up @@ -193,6 +195,12 @@ type
defaultValue: BNStartUpCmd.noCommand }: BNStartUpCmd

of BNStartUpCmd.noCommand:
runAsServiceFlag* {.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this flag would ideally not be exposed on linux - even on windows, perhaps it should be hidden given that it shouldn't be used from the command line

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hidden on non-windows targets through conditional compilation. At the moment, it's not easy to completely remove the option, because the compile-time reflection of Nim doesn't provide a good way to resolve when statements during the examination of the configuration record type. Furthermore, accepting the option on non-windows targets may still be beneficial because it simplifies the portability of Nimbus-launching scripts.

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"
Expand Down Expand Up @@ -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* {.
Expand Down Expand Up @@ -812,35 +815,30 @@ type
defaultValue: defaultSigningNodeRequestTimeout
name: "request-timeout" }: int

case cmd* {.
command
defaultValue: SNNoCommand }: SNStartUpCmd

of SNNoCommand:
bindPort* {.
desc: "Port for the REST (BETA version) HTTP server"
defaultValue: DefaultEth2RestPort
defaultValueDesc: "5052"
name: "bind-port" }: Port
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

Expand Down Expand Up @@ -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)
Expand Down
194 changes: 165 additions & 29 deletions beacon_chain/nimbus_beacon_node.nim
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,64 @@ from
import
TopicParams, validateParameters, init

when defined(windows):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we move the special windows sauce to a separate module? nimbus_beacon_node is already quite hard to follow because it does a lot of things, and slowly we've been refactoring it to become more of a delegator than a module for actual implementations (see deposits, trusted node sync etc) - the windows support would ideally reach a similar level of isolation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, no, because the service code needs to refer and call functions from nimbus_beacon_node (not the other way around). You can work-around this with function pointers perhaps, but it would arguably make the code more convoluted and harder to follow. A more reasonable alternative might be to introduce a new module called entry_point or something along those that wraps nimbus_beacon_node and sets up the service when necessary.

import winlean

type
LPCSTR* = cstring
LPSTR* = cstring

SERVICE_STATUS* {.final, pure.} = object
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well, stuff like this surely does not need to pollute nimbus_beacon_node

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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()

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
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
]

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)
Loading