Skip to main content

radicle_node/
runtime.rs

1pub mod handle;
2pub mod thread;
3
4use std::fmt::Debug;
5use std::path::PathBuf;
6use std::str::FromStr as _;
7use std::sync::mpsc;
8use std::{fs, io, net};
9
10#[cfg(unix)]
11use std::os::unix::net::UnixListener;
12#[cfg(windows)]
13use uds_windows::UnixListener;
14
15use handle::Handle;
16use localtime::LocalTime;
17use protocol::service;
18use protocol::service::gossip;
19use radicle::cob::migrate;
20use radicle::crypto::{Signer as _, SigningKey};
21use radicle::node;
22use radicle::node::Event;
23use radicle::node::UserAgent;
24use radicle::node::address;
25use radicle::node::address::Store as _;
26use radicle::node::events::Emitter;
27use radicle::node::notifications;
28use radicle::node::policy::config as policy;
29use radicle::node::{NodeId, routing};
30use radicle::profile::Home;
31use radicle::{Storage, cob, git, storage};
32use radicle_signals::Signal;
33use thiserror::Error;
34
35use crate::control;
36use crate::reactor;
37use crate::reactor::Reactor;
38use crate::wire::Wire;
39use crate::worker;
40
41/// Maximum pending worker tasks allowed.
42pub const MAX_PENDING_TASKS: usize = 1024;
43
44/// A client error.
45#[derive(Error, Debug)]
46pub enum Error {
47    /// A routing database error.
48    #[error("routing database error: {0}")]
49    Routing(#[from] routing::Error),
50    /// A cobs cache database error.
51    #[error("cobs cache database error: {0}")]
52    CobsCache(#[from] cob::cache::Error),
53    /// A node database error.
54    #[error("node database error: {0}")]
55    Database(#[from] node::db::Error),
56    /// A storage error.
57    #[error("storage error: {0}")]
58    Storage(#[from] storage::Error),
59    /// A policies database error.
60    #[error("policies database error: {0}")]
61    Policy(#[from] policy::Error),
62    /// A notifications database error.
63    #[error("notifications database error: {0}")]
64    Notifications(#[from] notifications::Error),
65    /// A gossip database error.
66    #[error("gossip database error: {0}")]
67    Gossip(#[from] gossip::Error),
68    /// An address database error.
69    #[error("address database error: {0}")]
70    Address(#[from] address::Error),
71    /// A service error.
72    #[error("service error: {0}")]
73    Service(Box<service::Error>),
74    /// An I/O error.
75    #[error("i/o error: {0}")]
76    Io(#[from] io::Error),
77    /// A control socket error.
78    #[error("control socket error: {0}")]
79    Control(#[from] control::Error),
80    /// Another node is already running.
81    #[error(
82        "another node appears to be running; \
83        if this isn't the case, delete the socket file at '{0}' \
84        and restart the node"
85    )]
86    AlreadyRunning(PathBuf),
87    /// A git version error.
88    #[error("git version error: {0}")]
89    GitVersion(#[from] git::VersionError),
90}
91
92impl From<service::Error> for Error {
93    fn from(e: service::Error) -> Self {
94        Self::Service(Box::new(e))
95    }
96}
97
98/// Wraps a [`UnixListener`] but tracks its origin.
99pub enum ControlSocket {
100    /// The listener was created by binding to it.
101    Bound(UnixListener, PathBuf),
102    /// The listener was received via socket activation.
103    Received(UnixListener),
104}
105
106/// Holds join handles to the client threads, as well as a client handle.
107pub struct Runtime {
108    pub id: NodeId,
109    pub home: Home,
110    pub control: ControlSocket,
111    pub handle: Handle,
112    pub storage: Storage,
113    pub reactor: Reactor,
114    pub pool: worker::Pool,
115    pub local_addrs: Vec<net::SocketAddr>,
116    pub signals: mpsc::Receiver<Signal>,
117}
118
119impl Runtime {
120    /// Initialize the runtime.
121    ///
122    /// This function spawns threads.
123    pub fn init(
124        home: Home,
125        config: radicle::node::Config,
126        socket: PathBuf,
127        listen: Vec<net::SocketAddr>,
128        signals: mpsc::Receiver<Signal>,
129        secret_key: SigningKey,
130    ) -> Result<Runtime, Error> {
131        let id = NodeId::from(*secret_key.public_key());
132        let alias = config.alias.clone();
133        let network = config.network;
134        let rng = fastrand::Rng::new();
135        let clock = LocalTime::now();
136        let timestamp = clock.into();
137        let storage = Storage::open(home.storage(), git::UserInfo { alias, key: id })?;
138        let policy = config.seeding_policy.into();
139
140        for (key, _) in &config.extra {
141            log::warn!(target: "node", "Unused or deprecated configuration attribute {key:?}");
142        }
143
144        log::info!(target: "node", "Opening policy database..");
145        let policies = home.policies_mut()?;
146        let policies = policy::Config::new(policy, policies);
147        let notifications = home.notifications_mut()?;
148        let mut cobs_cache = cob::cache::Store::open(home.cobs().join(cob::cache::COBS_DB_FILE))?;
149
150        match cobs_cache.check_version() {
151            Ok(()) => {}
152            Err(cob::cache::Error::OutOfDate) => {
153                log::info!(target: "node", "Migrating COBs cache..");
154                let version = cobs_cache.migrate(migrate::log)?;
155                log::info!(target: "node", "Migration of COBs cache complete (version={version})..");
156            }
157            Err(e) => return Err(e.into()),
158        }
159
160        log::info!(target: "node", "Default seeding policy set to '{}'", policy);
161        log::info!(target: "node", "Initializing service ({network:?})..");
162
163        let announcement = service::gossip::node(&config, timestamp)
164            .solve(Default::default())
165            .expect("Runtime::init: unable to solve proof-of-work puzzle");
166
167        log::info!(target: "node", "Opening node database..");
168        let db = home.database_mut(config.database)?.init(
169            &id,
170            announcement.features,
171            &announcement.alias,
172            &announcement.agent,
173            announcement.timestamp,
174            announcement.addresses.iter(),
175        )?;
176        let mut stores: service::Stores<_> = db.clone().into();
177
178        if config.connect.is_empty() && stores.addresses().is_empty()? {
179            log::info!(target: "node", "Address book is empty. Adding bootstrap nodes..");
180
181            for (alias, version, addrs) in config.network.bootstrap() {
182                for addr in addrs {
183                    let (id, addr) = addr.into();
184
185                    stores.addresses_mut().insert(
186                        &id,
187                        version,
188                        radicle::node::Features::SEED,
189                        &alias,
190                        0,
191                        &UserAgent::from_str("/radicle/runtime/bootstrap/")
192                            .expect("valid user agent"),
193                        clock.into(),
194                        [node::KnownAddress::new(addr, address::Source::Bootstrap)],
195                    )?;
196                }
197            }
198            log::info!(target: "node", "{} nodes added to address book", stores.addresses().len()?);
199        }
200
201        let emitter: Emitter<Event> = Default::default();
202        let mut service = service::Service::new(
203            config.clone(),
204            stores,
205            storage.clone(),
206            policies,
207            secret_key.clone(),
208            rng,
209            announcement,
210            emitter.clone(),
211        );
212        service.initialize(clock)?;
213
214        let (worker_send, worker_recv) =
215            crossbeam_channel::bounded::<worker::Task>(MAX_PENDING_TASKS);
216        let mut wire = Wire::new(service, worker_send, secret_key.clone());
217        let mut local_addrs = Vec::new();
218
219        for addr in listen {
220            let listener = reactor::Listener::bind(addr)?;
221            let local_addr = listener.local_addr();
222
223            local_addrs.push(local_addr);
224            wire.listen(listener);
225        }
226        let reactor = Reactor::new(wire, thread::name(&id, "service"))?;
227        let handle = Handle::new(home.clone(), socket.clone(), reactor.controller(), emitter);
228
229        let nid = NodeId::from(*secret_key.public_key());
230        let fetch = worker::FetchConfig {
231            local: nid,
232            expiry: worker::garbage::Expiry::default(),
233        };
234        let pool = worker::Pool::with(
235            worker_recv,
236            nid,
237            handle.clone(),
238            notifications,
239            cobs_cache,
240            db,
241            worker::Config {
242                capacity: config.workers.into(),
243                storage: storage.clone(),
244                fetch,
245                policy,
246                policies_db: home.node().join(node::POLICIES_DB_FILE),
247            },
248        )?;
249        let control = Self::bind(socket)?;
250
251        Ok(Runtime {
252            id,
253            home,
254            control,
255            storage,
256            reactor,
257            handle,
258            pool,
259            signals,
260            local_addrs,
261        })
262    }
263
264    pub fn run(self) -> Result<(), Error> {
265        let home = self.home;
266        let (listener, remove) = match self.control {
267            ControlSocket::Bound(listener, path) => (listener, Some(path)),
268            ControlSocket::Received(listener) => (listener, None),
269        };
270
271        log::info!(target: "node", "Running node {} in {}..", self.id, home.path().display());
272
273        thread::spawn(&self.id, "control", {
274            let handle = self.handle.clone();
275            || control::listen(listener, handle)
276        });
277
278        let _signals = thread::spawn(&self.id, "signals", move || {
279            loop {
280                use radicle::node::Handle as _;
281
282                match self.signals.recv() {
283                    Ok(Signal::Terminate | Signal::Interrupt) => {
284                        log::info!(target: "node", "Termination signal received; shutting down..");
285                        self.handle.shutdown().ok();
286                        break;
287                    }
288                    Ok(Signal::Hangup) => {
289                        log::debug!(target: "node", "Hangup signal (SIGHUP) received; ignoring..");
290                    }
291                    Ok(Signal::WindowChanged) => {}
292                    Err(e) => {
293                        log::warn!(target: "node", "Signal notifications channel error: {e}");
294                        break;
295                    }
296                }
297            }
298        });
299
300        self.pool.run().unwrap();
301        self.reactor.join().unwrap();
302
303        // Nb. We don't join the control thread here, as we have no way of notifying it that the
304        // node is shutting down.
305
306        // Remove control socket file, but don't freak out if it's not there anymore.
307        remove.map(|path| fs::remove_file(path).ok());
308
309        log::debug!(target: "node", "Node shutdown completed for {}", self.id);
310
311        Ok(())
312    }
313
314    #[cfg(all(feature = "socket2", feature = "systemd", target_os = "linux"))]
315    fn receive_listener() -> Option<UnixListener> {
316        // SAFETY: When `fd` is called, no other threads are spawned (yet).
317        let fd = match unsafe { radicle_systemd::listen::fd("control") } {
318            Ok(Some(fd)) => fd,
319            Ok(None) => return None,
320            Err(err) => {
321                log::error!(target: "node", "Error receiving listener from systemd: {err}");
322                return None;
323            }
324        };
325
326        let socket: socket2::Socket = unsafe { std::os::fd::FromRawFd::from_raw_fd(fd) };
327
328        let domain = match socket.domain() {
329            Ok(domain) => domain,
330            Err(err) => {
331                log::error!(target: "node", "Error receiving listener from systemd when inspecting domain of socket: {err}");
332                return None;
333            }
334        };
335
336        if domain != socket2::Domain::UNIX {
337            log::error!(target: "node", "Dropping listener received from systemd: Domain is not AF_UNIX.");
338            return None;
339        }
340
341        Some(UnixListener::from(socket))
342    }
343
344    fn bind(path: PathBuf) -> Result<ControlSocket, Error> {
345        #[cfg(all(feature = "socket2", feature = "systemd", target_os = "linux"))]
346        {
347            if let Some(listener) = Self::receive_listener() {
348                log::info!(target: "node", "Received control socket.");
349                return Ok(ControlSocket::Received(listener));
350            }
351        }
352
353        log::info!(target: "node", "Binding control socket {}..", path.display());
354        match UnixListener::bind(&path) {
355            Ok(sock) => Ok(ControlSocket::Bound(sock, path)),
356            Err(err) if err.kind() == io::ErrorKind::AddrInUse => Err(Error::AlreadyRunning(path)),
357            Err(err) => Err(err.into()),
358        }
359    }
360}