From 4bcb32753f4efb766fee617a8baf42f33c8471ee Mon Sep 17 00:00:00 2001 From: Martin Habovstiak Date: Sat, 17 Jul 2021 13:54:41 +0200 Subject: [PATCH] lnd+signal: implement systemd notify This adds support for notifying systemd about the state of LND. It notifies systemd just before waiting for wallet password or, if `wallet-password-file` was specified, right after unlocking the wallet. This means that "ready" represents RPC being available for intended use. It's intentional, so that client services can use `After=` in `systemd` configuration to avoid misleading error messages about missing files or refused connections. Part of #4470 --- docs/release-notes/release-notes-0.14.0.md | 6 ++ go.mod | 1 + lnd.go | 8 +++ signal/signal.go | 83 +++++++++++++++++++++- 4 files changed, 96 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/release-notes-0.14.0.md b/docs/release-notes/release-notes-0.14.0.md index 4cf561112..8ee55f4db 100644 --- a/docs/release-notes/release-notes-0.14.0.md +++ b/docs/release-notes/release-notes-0.14.0.md @@ -25,6 +25,12 @@ for more information. * [Stub code for interacting with `lnrpc` from a WASM context through JSON messages was added](https://github.com/lightningnetwork/lnd/pull/5601). +* LND now [reports to systemd](https://github.com/lightningnetwork/lnd/pull/5536) + that RPC is ready (port bound, certificate generated, macaroons created, + in case of `wallet-unlock-password-file` wallet unlocked). This can be used to + avoid misleading error messages from dependent services if they use `After` + systemd option. + ## Wallet * It is now possible to fund a psbt [without specifying any diff --git a/go.mod b/go.mod index 61bbe5319..bdf24e3ed 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/btcsuite/btcwallet/wallet/txrules v1.0.0 github.com/btcsuite/btcwallet/walletdb v1.3.6-0.20210803004036-eebed51155ec github.com/btcsuite/btcwallet/wtxmgr v1.3.1-0.20210803004036-eebed51155ec + github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e github.com/davecgh/go-spew v1.1.1 github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/go-errors/errors v1.0.1 diff --git a/lnd.go b/lnd.go index d730fe0fe..f8cc9fa66 100644 --- a/lnd.go +++ b/lnd.go @@ -537,6 +537,10 @@ func Main(cfg *Config, lisCfg ListenerCfg, interceptor signal.Interceptor) error // to the default behavior of waiting for the wallet creation/unlocking // over RPC. default: + if err := interceptor.Notifier.NotifyReady(false); err != nil { + return err + } + params, err := waitForWalletPassword( cfg, pwService, []btcwallet.LoaderOption{dbs.walletDB}, interceptor.ShutdownChannel(), @@ -893,6 +897,10 @@ func Main(cfg *Config, lisCfg ListenerCfg, interceptor signal.Interceptor) error // We transition the RPC state to Active, as the RPC server is up. interceptorChain.SetRPCActive() + if err := interceptor.Notifier.NotifyReady(true); err != nil { + return err + } + // If we're not in regtest or simnet mode, We'll wait until we're fully // synced to continue the start up of the remainder of the daemon. This // ensures that we don't accept any possibly invalid state transitions, or diff --git a/signal/signal.go b/signal/signal.go index f256c5ec8..a9fcaf70f 100644 --- a/signal/signal.go +++ b/signal/signal.go @@ -7,10 +7,13 @@ package signal import ( "errors" + "fmt" "os" "os/signal" "sync/atomic" "syscall" + + "github.com/coreos/go-systemd/daemon" ) var ( @@ -19,8 +22,78 @@ var ( started int32 ) +// systemdNotifyReady notifies systemd about LND being ready, logs the result of +// the operation or possible error. Besides logging, systemd being unavailable +// is ignored. +func systemdNotifyReady() error { + notified, err := daemon.SdNotify(false, daemon.SdNotifyReady) + if err != nil { + err := fmt.Errorf("failed to notify systemd %v (if you aren't "+ + "running systemd clear the environment variable "+ + "NOTIFY_SOCKET)", err) + log.Error(err) + + // The SdNotify doc says it's common to ignore the + // error. We don't want to ignore it because if someone + // set up systemd to wait for initialization other + // processes would get stuck. + return err + } + if notified { + log.Info("Systemd was notified about our readiness") + } else { + log.Info("We're not running within systemd") + } + return nil +} + +// systemdNotifyStop notifies systemd that LND is stopping and logs error if +// the notification failed. It also logs if the notification was actually sent. +// Systemd being unavailable is intentionally ignored. +func systemdNotifyStop() { + notified, err := daemon.SdNotify(false, daemon.SdNotifyStopping) + + // Just log - we're stopping anyway. + if err != nil { + log.Errorf("Failed to notify systemd: %v", err) + } + if notified { + log.Infof("Systemd was notified about stopping") + } +} + +// Notifier handles notifications about status of LND. +type Notifier struct { + // notifiedReady remembers whether Ready was sent to avoid sending it + // multiple times. + notifiedReady bool +} + +// NotifyReady notifies other applications that RPC is ready. +func (notifier *Notifier) NotifyReady(walletUnlocked bool) error { + if !notifier.notifiedReady { + err := systemdNotifyReady() + if err != nil { + return err + } + notifier.notifiedReady = true + } + if walletUnlocked { + _, _ = daemon.SdNotify(false, "STATUS=Wallet unlocked") + } else { + _, _ = daemon.SdNotify(false, "STATUS=Wallet locked") + } + + return nil +} + +// notifyStop notifies other applications that LND is stopping. +func (notifier *Notifier) notifyStop() { + systemdNotifyStop() +} + // Interceptor contains channels and methods regarding application shutdown -// and interrupt signals +// and interrupt signals. type Interceptor struct { // interruptChannel is used to receive SIGINT (Ctrl+C) signals. interruptChannel chan os.Signal @@ -33,11 +106,16 @@ type Interceptor struct { shutdownRequestChannel chan struct{} // quit is closed when instructing the main interrupt handler to exit. + // Note that to avoid losing notifications, only shutdown func may + // close this channel. quit chan struct{} + + // Notifier handles sending shutdown notifications. + Notifier Notifier } // Intercept starts the interception of interrupt signals and returns an `Interceptor` instance. -// Note that any previous active interceptor must be stopped before a new one can be created +// Note that any previous active interceptor must be stopped before a new one can be created. func Intercept() (Interceptor, error) { if !atomic.CompareAndSwapInt32(&started, 0, 1) { return Interceptor{}, errors.New("intercept already started") @@ -85,6 +163,7 @@ func (c *Interceptor) mainInterruptHandler() { } isShutdown = true log.Infof("Shutting down...") + c.Notifier.notifyStop() // Signal the main interrupt handler to exit, and stop accept // post-facto requests.