Skip to content

Commit b7fd1ca

Browse files
committed
daemon: Add socket activation via /run/rpm-ostreed.socket
For historical reasons, the daemon ends up doing a lot of initialization before even claiming the DBus name. For example, it calls `ostree_sysroot_load()`. We also end up scanning the RPM database, and actually parse all the GPG keys in `/etc/pki/rpm-gpg` etc. This causes two related problems: - By doing all this work before claiming the bus name, we race against the (pretty low) DBus service timeout of 25s. - If something hard fails at initialization, the client can't easily see the error because it just appears in the systemd journal. The client will just see a service timeout. The solution to this is to adopt systemd socket activation, which drops out DBus as an intermediary. On daemon startup, we now do the process-global initialization (like ostree sysroot) and if that fails, the daemon just sticks around (but without claiming the bus name), ready to return the error message to each client. After this patch: ``` $ systemctl stop rpm-ostreed $ umount /boot $ rpm-ostree status error: Couldn't start daemon: Error setting up sysroot: loading sysroot: Unexpected state: /run/ostree-booted found, but no /boot/loader directory ```
1 parent bfe0869 commit b7fd1ca

File tree

7 files changed

+169
-20
lines changed

7 files changed

+169
-20
lines changed

Makefile-daemon.am

+4-3
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,15 @@ systemdunit_service_in_files = \
6363
$(NULL)
6464

6565
systemdunit_service_files = $(systemdunit_service_in_files:.service.in=.service)
66-
systemdunit_timer_files = \
66+
systemdunit_other_files = \
67+
$(srcdir)/src/daemon/rpm-ostreed.socket \
6768
$(srcdir)/src/daemon/rpm-ostreed-automatic.timer \
6869
$(srcdir)/src/daemon/rpm-ostree-countme.timer \
6970
$(NULL)
7071

7172
systemdunit_DATA = \
7273
$(systemdunit_service_files) \
73-
$(systemdunit_timer_files) \
74+
$(systemdunit_other_files) \
7475
$(NULL)
7576

7677
systemdunitdir = $(prefix)/lib/systemd/system/
@@ -110,7 +111,7 @@ EXTRA_DIST += \
110111
$(sysconf_DATA) \
111112
$(service_in_files) \
112113
$(systemdunit_service_in_files) \
113-
$(systemdunit_timer_files) \
114+
$(systemdunit_other_files) \
114115
$(NULL)
115116

116117
CLEANFILES += \

rust/src/daemon.rs

+106
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
// SPDX-License-Identifier: Apache-2.0 OR MIT
66

77
use crate::{cxxrsutil::*, variant_utils};
8+
use anyhow::{anyhow, Context, Result};
89
use openat_ext::OpenatDirExt;
10+
use std::io::prelude::*;
11+
use std::os::unix::net::{UnixListener, UnixStream};
12+
use std::os::unix::prelude::FromRawFd;
913
use std::pin::Pin;
1014

1115
/// Validate basic assumptions on daemon startup.
@@ -26,6 +30,108 @@ pub(crate) fn daemon_sanitycheck_environment(
2630
Ok(())
2731
}
2832

33+
/// Connect to the client socket and ensure the daemon is initialized;
34+
/// this avoids DBus and ensures that we get any early startup errors
35+
/// returned cleanly.
36+
pub(crate) fn start_daemon_via_socket() -> CxxResult<()> {
37+
let address = "/run/rpm-ostree/client.sock";
38+
let s = UnixStream::connect(address)
39+
.with_context(|| anyhow!("Failed to connect to {}", address))?;
40+
let mut s = std::io::BufReader::new(s);
41+
let mut r = String::new();
42+
s.read_to_string(&mut r)
43+
.context("Reading from client socket")?;
44+
if r.is_empty() {
45+
Ok(())
46+
} else {
47+
Err(anyhow!("{}", r).into())
48+
}
49+
}
50+
51+
fn send_init_result_to_client(client: &UnixStream, err: &Result<()>) {
52+
let mut client = std::io::BufWriter::new(client);
53+
match err {
54+
Ok(_) => {
55+
// On successwe close the stream without writing anything,
56+
// which acknowledges successful startup to the client.
57+
}
58+
Err(e) => {
59+
let msg = e.to_string();
60+
match client
61+
.write_all(msg.as_bytes())
62+
.and_then(|_| client.flush())
63+
{
64+
Ok(_) => {}
65+
Err(inner_err) => {
66+
eprintln!(
67+
"Failed to write error message to client socket (original error: {}): {}",
68+
e, inner_err
69+
);
70+
}
71+
}
72+
}
73+
}
74+
}
75+
76+
fn process_clients(listener: UnixListener, res: &Result<()>) {
77+
for stream in listener.incoming() {
78+
match stream {
79+
Ok(stream) => send_init_result_to_client(&stream, res),
80+
Err(e) => {
81+
// This shouldn't be fatal, we continue to start up.
82+
eprintln!("Failed to listen for client stream: {}", e);
83+
}
84+
}
85+
if res.is_err() {
86+
break;
87+
}
88+
}
89+
}
90+
91+
/// Perform initialization steps required by systemd service activation.
92+
///
93+
/// This ensures that the system is running under systemd, then receives the
94+
/// socket-FD for main IPC logic, and notifies systemd about ready-state.
95+
pub(crate) fn daemon_main(debug: bool) -> Result<()> {
96+
if !systemd::daemon::booted()? {
97+
return Err(anyhow!("not running as a systemd service"));
98+
}
99+
100+
let init_res: Result<()> = crate::ffi::daemon_init_inner(debug).map_err(|e| e.into());
101+
102+
let mut fds = systemd::daemon::listen_fds(false)?.iter();
103+
let listener = match fds.next() {
104+
None => {
105+
// If started directly via `systemctl start` or DBus activation, we
106+
// directly propagate the error back to our exit code.
107+
init_res?;
108+
UnixListener::bind("/run/rpmostreed.socket")?
109+
}
110+
Some(fd) => {
111+
if fds.next().is_some() {
112+
return Err(anyhow!("Expected exactly 1 fd from systemd activation"));
113+
}
114+
let listener = unsafe { UnixListener::from_raw_fd(fd) };
115+
match init_res {
116+
Ok(_) => listener,
117+
Err(e) => {
118+
let err_copy = Err(anyhow!("{}", e));
119+
process_clients(listener, &err_copy);
120+
return Err(e);
121+
}
122+
}
123+
}
124+
};
125+
126+
// On success, we spawn a helper thread that just responds with
127+
// sucess to clients that connect via the socket. In the future,
128+
// perhaps we'll expose an API here.
129+
std::thread::spawn(move || process_clients(listener, &Ok(())));
130+
131+
// And now, enter the main loop.
132+
Ok(crate::ffi::daemon_main_inner()?)
133+
}
134+
29135
/// Get a currently unique (for this host) identifier for the
30136
/// deployment; TODO - adding the deployment timestamp would make it
31137
/// persistently unique, needs API in libostree.

rust/src/lib.rs

+8
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,8 @@ pub mod ffi {
181181
// daemon.rs
182182
extern "Rust" {
183183
fn daemon_sanitycheck_environment(sysroot: Pin<&mut OstreeSysroot>) -> Result<()>;
184+
fn daemon_main(debug: bool) -> Result<()>;
185+
fn start_daemon_via_socket() -> Result<()>;
184186
fn deployment_generate_id(deployment: Pin<&mut OstreeDeployment>) -> String;
185187
fn deployment_populate_variant(
186188
mut sysroot: Pin<&mut OstreeSysroot>,
@@ -468,6 +470,12 @@ pub mod ffi {
468470
fn main_print_error(msg: &str);
469471
}
470472

473+
unsafe extern "C++" {
474+
include!("rpmostreed-daemon.h");
475+
fn daemon_init_inner(debug: bool) -> Result<()>;
476+
fn daemon_main_inner() -> Result<()>;
477+
}
478+
471479
unsafe extern "C++" {
472480
include!("rpmostree-clientlib.h");
473481
fn client_require_root() -> Result<()>;

src/app/libmain.cxx

+2
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,8 @@ rpmostree_option_context_parse (GOptionContext *context,
296296
}
297297
}
298298

299+
rpmostreecxx::start_daemon_via_socket();
300+
299301
/* root never needs to auth */
300302
if (getuid () != 0)
301303
/* ignore errors; we print out a warning if we fail to spawn pkttyagent */

src/app/rpmostree-builtin-start-daemon.cxx

+36-17
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
#include "rpmostree-builtins.h"
3535
#include "rpmostree-util.h"
36+
#include "rpmostreed-utils.h"
3637
#include "rpmostreed-daemon.h"
3738
#include "rpmostree-libbuiltin.h"
3839

@@ -53,6 +54,7 @@ static GOptionEntry opt_entries[] =
5354
{ NULL }
5455
};
5556

57+
static GDBusConnection *bus = NULL;
5658
static RpmostreedDaemon *rpm_ostree_daemon = NULL;
5759

5860
static void
@@ -225,20 +227,14 @@ on_log_handler (const gchar *log_domain,
225227
sd_journal_print (priority, "%s", message);
226228
}
227229

228-
gboolean
229-
rpmostree_builtin_start_daemon (int argc,
230-
char **argv,
231-
RpmOstreeCommandInvocation *invocation,
232-
GCancellable *cancellable,
233-
GError **error)
230+
namespace rpmostreecxx {
231+
// This function is always called from the Rust side. Hopefully
232+
// soon we'll move more of this code into daemon.rs.
233+
void
234+
daemon_init_inner (bool debug)
234235
{
235-
g_autoptr(GOptionContext) opt_context = g_option_context_new (" - start the daemon process");
236-
g_option_context_add_main_entries (opt_context, opt_entries, NULL);
237-
238-
if (!g_option_context_parse (opt_context, &argc, &argv, error))
239-
return FALSE;
240-
241-
if (opt_debug)
236+
g_autoptr(GError) local_error = NULL;
237+
if (debug)
242238
{
243239
g_autoptr(GIOChannel) channel = NULL;
244240
g_log_set_handler (G_LOG_DOMAIN, (GLogLevelFlags)(G_LOG_LEVEL_DEBUG | G_LOG_LEVEL_INFO), on_log_debug, NULL);
@@ -258,15 +254,21 @@ rpmostree_builtin_start_daemon (int argc,
258254
g_unix_signal_add (SIGTERM, on_sigint, NULL);
259255

260256
/* Get an explicit ref to the bus so we can use it later */
261-
g_autoptr(GDBusConnection) bus = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, error);
257+
g_autoptr(GDBusConnection) bus = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &local_error);
262258
if (!bus)
263-
return FALSE;
264-
if (!start_daemon (bus, error))
265-
return FALSE;
259+
util::throw_gerror(local_error);
260+
if (!start_daemon (bus, &local_error))
261+
util::throw_gerror(local_error);
262+
}
266263

264+
// Called from rust side to enter mainloop.
265+
void
266+
daemon_main_inner ()
267+
{
267268
state_transition (APPSTATE_RUNNING);
268269

269270
g_debug ("Entering main event loop");
271+
g_assert (rpm_ostree_daemon);
270272
rpmostreed_daemon_run_until_idle_exit (rpm_ostree_daemon);
271273

272274
if (bus)
@@ -300,6 +302,23 @@ rpmostree_builtin_start_daemon (int argc,
300302
g_autoptr(GMainContext) mainctx = g_main_context_default ();
301303
while (appstate == APPSTATE_FLUSHING)
302304
g_main_context_iteration (mainctx, TRUE);
305+
}
306+
} /* namespace */
307+
308+
gboolean
309+
rpmostree_builtin_start_daemon (int argc,
310+
char **argv,
311+
RpmOstreeCommandInvocation *invocation,
312+
GCancellable *cancellable,
313+
GError **error)
314+
{
315+
g_autoptr(GOptionContext) opt_context = g_option_context_new (" - start the daemon process");
316+
g_option_context_add_main_entries (opt_context, opt_entries, NULL);
317+
318+
if (!g_option_context_parse (opt_context, &argc, &argv, error))
319+
return FALSE;
320+
321+
rpmostreecxx::daemon_main (opt_debug);
303322

304323
return TRUE;
305324
}

src/daemon/rpm-ostreed.socket

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[Socket]
2+
ListenStream=/run/rpm-ostree/client.sock
3+
ConditionKernelCommandLine=ostree
4+
SocketMode=0600
5+
6+
[Install]
7+
WantedBy=sockets.target

src/daemon/rpmostreed-daemon.h

+6
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ G_BEGIN_DECLS
3636
#define RPMOSTREE_DRIVER_SD_UNIT "driver-sd-unit"
3737
#define RPMOSTREE_DRIVER_NAME "driver-name"
3838

39+
/* Note: Currently actually defined in rpmostree-builtin-start-daemon.cxx for historical reasons */
40+
namespace rpmostreecxx {
41+
void daemon_init_inner (bool debug);
42+
void daemon_main_inner ();
43+
}
44+
3945
GType rpmostreed_daemon_get_type (void) G_GNUC_CONST;
4046
RpmostreedDaemon * rpmostreed_daemon_get (void);
4147
GDBusConnection * rpmostreed_daemon_connection (void);

0 commit comments

Comments
 (0)