From 31bcdff4bb94e8a30ff67024342807bd83a38a7e Mon Sep 17 00:00:00 2001 From: ajaysehwal Date: Mon, 9 Nov 2026 15:55:27 +0530 Subject: [PATCH] Set up TLS, the gRPC listener and signals before starting the node We loaded the TLS config, bound the gRPC port and registered the signal handlers only after `node.start()`. If the port was already in use we panicked, which with `panic = "abort"` kills a node that is already running without `node.stop()`. A TLS setup failure did the same, and a SIGTERM or Ctrl-C received during `node.start()` killed the process without a graceful shutdown. Now we load the TLS config and bind the listener before starting the node, exiting with an error if either fails, and register the SIGHUP, SIGTERM and SIGINT handlers up front. Signals received during startup are handled by the main loop once the node is running. This still happens after building the node, so a restarting instance keeps waiting for a Postgres lease held by the previous one. --- e2e-tests/src/lib.rs | 8 ++++- e2e-tests/tests/config.rs | 14 +++++++- ldk-server/src/main.rs | 69 ++++++++++++++++++++------------------- 3 files changed, 56 insertions(+), 35 deletions(-) diff --git a/e2e-tests/src/lib.rs b/e2e-tests/src/lib.rs index e6c48970..33a131d2 100644 --- a/e2e-tests/src/lib.rs +++ b/e2e-tests/src/lib.rs @@ -255,6 +255,12 @@ impl TestConfigBuilder { self } + /// Set the gRPC service address. + pub fn grpc_service_address(mut self, address: &str) -> Self { + self.grpc_service_address = address.to_string(); + self + } + /// Set the node alias, or `None` to omit it entirely. pub fn alias(mut self, alias: Option<&str>) -> Self { self.alias = alias.map(str::to_string); @@ -596,7 +602,7 @@ pub fn start_expect_failure( } /// Allocate distinct ports outside the default Linux/macOS ephemeral ranges. /// Allocate each port only once per test process, so stopped servers can restart. -fn reserve_port() -> TcpListener { +pub fn reserve_port() -> TcpListener { static NEXT_PORT: AtomicU32 = AtomicU32::new(10_000); loop { let port = NEXT_PORT.fetch_add(1, Ordering::Relaxed); diff --git a/e2e-tests/tests/config.rs b/e2e-tests/tests/config.rs index 412766d4..3b6dd4b5 100644 --- a/e2e-tests/tests/config.rs +++ b/e2e-tests/tests/config.rs @@ -8,7 +8,8 @@ // licenses. use e2e_tests::{ - start_expect_failure, ChainSource, LdkServerHandle, TestBitcoind, TestConfigBuilder, + reserve_port, start_expect_failure, ChainSource, LdkServerHandle, TestBitcoind, + TestConfigBuilder, }; use ldk_server_grpc::api::GetNodeInfoRequest; @@ -98,6 +99,17 @@ async fn test_config_with_tls_hosts() { assert!(info.current_best_block.is_some()); } +#[test] +fn test_config_fail_grpc_address_in_use() { + let bitcoind = TestBitcoind::new(); + let blocker = reserve_port(); + let address = blocker.local_addr().unwrap().to_string(); + let stderr = start_expect_failure(&bitcoind, |params| { + TestConfigBuilder::new(params).grpc_service_address(&address).build() + }); + assert!(stderr.contains("Failed to bind gRPC service address"), "Unexpected stderr: {stderr}"); +} + #[test] fn test_config_fail_log_file_matches_storage_dir() { let bitcoind = TestBitcoind::new(); diff --git a/ldk-server/src/main.rs b/ldk-server/src/main.rs index 27450b54..077d3da4 100644 --- a/ldk-server/src/main.rs +++ b/ldk-server/src/main.rs @@ -38,7 +38,7 @@ use log::{debug, error, info, warn}; use prost::Message; use tokio::net::TcpListener; use tokio::select; -use tokio::signal::unix::SignalKind; +use tokio::signal::unix::{Signal, SignalKind}; use tokio::sync::broadcast; use crate::api::node_to_proto_custom_tlv; @@ -322,6 +322,30 @@ fn main() { let (event_sender, _) = broadcast::channel::(1024); let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false); + // Set up everything that can fail locally before the node starts, so that a failure never + // leaves a running node behind, and handle signals from the start. + let server_config = + match get_or_generate_tls_config(config_file.tls_config, storage_dir.to_str().unwrap()) { + Ok(config) => config, + Err(e) => { + error!("Failed to set up TLS: {e}"); + std::process::exit(-1); + }, + }; + let tls_acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(server_config)); + + let grpc_listener = match runtime.block_on(TcpListener::bind(config_file.grpc_service_addr)) { + Ok(listener) => listener, + Err(e) => { + error!("Failed to bind gRPC service address {}: {e}", config_file.grpc_service_addr); + std::process::exit(-1); + }, + }; + + let mut sighup_stream = register_signal(&runtime, SignalKind::hangup()); + let mut sigterm_stream = register_signal(&runtime, SignalKind::terminate()); + let mut sigint_stream = register_signal(&runtime, SignalKind::interrupt()); + info!("Starting ldk-server version {FULL_VERSION}"); match node.start() { Ok(()) => {}, @@ -343,22 +367,6 @@ fn main() { } runtime.block_on(async { - // Register SIGHUP handler for log rotation - let mut sighup_stream = match tokio::signal::unix::signal(SignalKind::hangup()) { - Ok(stream) => stream, - Err(e) => { - error!("Failed to register SIGHUP handler: {e}"); - std::process::exit(-1); - } - }; - - let mut sigterm_stream = match tokio::signal::unix::signal(SignalKind::terminate()) { - Ok(stream) => stream, - Err(e) => { - error!("Failed to register for SIGTERM stream: {e}"); - std::process::exit(-1); - } - }; let event_node = Arc::clone(&node); let mut ready_channel_ids: HashSet = event_node .list_channels() @@ -398,21 +406,6 @@ fn main() { None }; - let grpc_listener = TcpListener::bind(config_file.grpc_service_addr) - .await - .expect("Failed to bind listening port"); - - let server_config = match get_or_generate_tls_config( - config_file.tls_config, - storage_dir.to_str().unwrap(), - ) { - Ok(config) => config, - Err(e) => { - error!("Failed to set up TLS: {e}"); - std::process::exit(-1); - } - }; - let tls_acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(server_config)); info!("gRPC service listening on {}", config_file.grpc_service_addr); systemd::notify_ready(); @@ -753,7 +746,7 @@ fn main() { Err(e) => error!("Failed to accept connection: {}", e), } } - _ = tokio::signal::ctrl_c() => { + _ = sigint_stream.recv() => { info!("Received CTRL-C, shutting down.."); let _ = shutdown_tx.send(true); break; @@ -779,6 +772,16 @@ fn main() { log::logger().flush(); } +fn register_signal(runtime: &tokio::runtime::Runtime, kind: SignalKind) -> Signal { + match runtime.block_on(async { tokio::signal::unix::signal(kind) }) { + Ok(signal) => signal, + Err(e) => { + error!("Failed to register signal handler for {kind:?}: {e}"); + std::process::exit(-1); + }, + } +} + fn ensure_no_unsupported_storage_migration( network_dir: &Path, ldk_node_storage: &LdkNodeStorageConfig, ) -> Result<(), String> {