Introduce support for running code in fibers (#39771)

Traditionally, asynchronous programming in systemd has been achieved
using
sd-event along with the asynchronous interfaces of sd-bus and
sd-varlink.
This works well when the system is reacting to events and all code
triggered
by those events can run without blocking. In these scenarios, the global
Manager object is passed as userdata to the callback, and the callback
can
use the stack as usual, declaring local state and ensuring proper
cleanup via
_cleanup_. Control flow structures, such as loops, work as expected, and
everything runs smoothly.

However, challenges arise when the code needs to perform long-running
operations within these callbacks. Since the system cannot block
execution
within the callback, we can't directly invoke a long-running operation
and
wait for its result without introducing complexities. Instead, we need
to
initiate the long-running task, register for completion with sd-event,
sd-bus, or sd-varlink, and provide a callback to be invoked when the
operation completes.

This callback, however, only receives a single userdata pointer, which
forces us to bundle all local variables into a struct and pass it along
as
part of the callback. On top of that, after queuing the asynchronous
operation, the caller continues executing. As the caller's stack unwinds
when the function exits, the resources and state within the local scope
may
be prematurely cleaned up. Therefore, the struct must store copies of
the
local variables or ensure proper reference counting to prevent premature
resource cleanup.

When multiple long-running operations need to be initiated within a
loop,
the complexity grows further. We must introduce additional shared state
to
track the completion of all operations before we can run any code that
depends on their results.

Furthermore, since the daemon may be shut down at any time, we must
track
the lifecycle of each long-running operation in the global Manager
struct,
ensuring proper cleanup even when stack unwinding can no longer manage
the
resources for us.

Fibers, or green threads, provide a more natural way of handling
asynchronous operations. By enabling cooperative multitasking within a
single thread, fibers allow us to write code that looks like it’s
running
synchronously, but with the ability to yield control at predefined
points,
such as when waiting for long-running tasks to complete.

With fibers, we can simplify the control flow by running asynchronous
operations within a fiber, allowing us to "pause" execution while
waiting
for the long-running operation to finish and then "resume" the operation
once
it's complete. This eliminates the need for multiple callback chains,
extensive state tracking, and the potential pitfalls of stack unwinding.

This commit introduces the ability to execute long-running operations in
a
non-blocking manner while maintaining the simplicity and readability of
synchronous code. The fiber-based approach will significantly improve
the
handling of complex workflows, making the code easier to write and
maintain.

The implementation is based on ucontext.h's makecontext() (with a
fallback
to the venerable sigaltstack() approach on musl),
sigsetjmp()/siglongjmp()
and sd-event. ucontext.h provides us with alternate stacks that we can
switch
between. We use sigsetjmp()/siglongjmp() instead of swapcontext()
because the
latter forcibly saves/restores a per context signal mask every time it
is called.
Using sigsetjmp()/siglongjmp(), we can avoid the unnecessary syscall and
maintain
a per thread signal mask, which makes much more sense than having a per
fiber
signal mask.

The default stack size is the same as a regular thread. Because we 
use mmap() to allocate the stack, the memory won't actually be used
until it
is paged in by the kernel, so we don't actually use 8MB per fiber.

To integrate fibers with the event loop, each fiber is assigned a
deferred
event source which resumes the fiber when enabled. The deferred event
source
is oneshot by default so the fiber will run immediately until it yields
or
suspends. If it yields, the deferred event source is enabled again
(oneshot)
immediately. If it suspends, before it suspends, one or more event
sources
are registered with sd-event that will enable the deferred event source
(oneshot) to resume the fiber once the operation it is waiting for
completes.

Yielding or suspending the fiber is done by calling sd_fiber_yield() or
sd_fiber_suspend() respectively. Both of these return zero on success or
any
error value from the async operation that caused the fiber to resume.

This is also how fiber cancellation is implemented. When a fiber is
cancelled,
sd_fiber_yield() and sd_fiber_suspend() will return ECANCELED when the
fiber
is resumed, allowing the fiber to unwind its stack (which allows cleanup
to
happen automatically) and finish.

Instead of having applications work directly with fibers, we hide them
behind
a generic futures interface to represent long-running operations,
regardless of
whether those operations are running on a fiber or not. Aside from
fibers, the
futures library (sd-future) will for example allow waiting for sd-event
sources
and doing sd-bus calls in the background as well. Fibers can suspend
until a
future is ready with sd_fiber_await() or by having the future wake up
the fiber
explicitly in its callback. A future always defaults to waking up the
current
fiber.

Each future kind plugs into the library by providing an sd_future_ops
vtable
(alloc, free, cancel, set_priority). The library treats the impl pointer
returned by alloc() as a black box. Future Implementations retrieve it
via
sd_future_get_private().

A future starts in SD_FUTURE_PENDING and transitions exactly once to
SD_FUTURE_RESOLVED, carrying an integer result. Consumers can react to
that
transition either by installing a one-shot callback with
sd_future_set_callback() (callback-style code) or by waiting on it from
a
fiber via sd_fiber_await() (synchronous-looking fiber code).
sd_fiber_await()
is itself built on a "wait future" that resolves when its target
resolves;
sd_future_new_wait() exposes the same primitive directly so non-fiber
callers
can chain futures without involving a fiber.

Cancellation is cooperative: sd_future_cancel() invokes the future
impl's
cancel callback, which is responsible for tearing down its work and
ultimately
resolving the promise with -ECANCELED. For fiber futures this is what
surfaces as the ECANCELED return from
sd_fiber_yield()/sd_fiber_suspend()
mentioned above.

Fire-and-forget fibers — created by passing a NULL ret to sd_fiber_new()
—
take a self-reference on their future so they outlive the caller's
scope.
The self-ref is dropped when the fiber resolves. This floating mechanism
(sd_fiber_set_floating()) is restricted to fiber futures because they
uniquely guarantee resolution; allowing it for arbitrary future kinds
would
risk silent leaks for kinds that may never resolve.

Note that fiber cleanup depends on the runtime operating normally. Each
fiber's _cleanup_-style cleanups live on the fiber's own stack and run
only when the fiber is resumed and allowed to unwind, which requires a
working event loop to drive it to completion. The exit event source
registered for top-level fibers ensures unwind on a normal
sd_event_exit(),
but if the event loop itself terminates abnormally (e.g. an
unrecoverable
allocation failure mid-dispatch) before all fibers have resolved, their
stacks never unwind and any resources they own leak.

The code lives in libsystemd as sd-future (not exported) for the
following reasons:
- We may want to make this a public libsystemd API in the future
- The code can't live in src/basic as it makes heavy use of sd-event
- The code can't live in src/shared as sd-bus and sd-event make use of
it

The log and log-context headers are updated with functions to allow
fibers to have their own log prefix and log context.
This commit is contained in:
Daan De Meyer
2026-05-21 16:21:13 +02:00
committed by GitHub
51 changed files with 7554 additions and 950 deletions
+11 -1
View File
@@ -1018,6 +1018,14 @@ else
endif
conf.set10('HAVE_LIBCRYPT', have)
# musl declares the ucontext.h functions but does not implement them; the fiber bootstrap in
# src/libsystemd/sd-future/fiber.c relies on them, so on musl we have to link to libucontext.
if get_option('libc') == 'musl'
libucontext = dependency('libucontext')
else
libucontext = []
endif
bpf_framework = get_option('bpf-framework')
bpf_compiler = get_option('bpf-compiler')
libbpf = dependency('libbpf',
@@ -1721,6 +1729,7 @@ libsystemd_includes = [include_directories(
'src/libsystemd/sd-common',
'src/libsystemd/sd-device',
'src/libsystemd/sd-event',
'src/libsystemd/sd-future',
'src/libsystemd/sd-hwdb',
'src/libsystemd/sd-id128',
'src/libsystemd/sd-journal',
@@ -1761,7 +1770,7 @@ libsystemd = shared_library(
link_with : [libc_wrapper_static,
libbasic_static],
link_whole : [libsystemd_static],
dependencies : [userspace],
dependencies : [libucontext, userspace],
link_depends : libsystemd_sym,
install : true,
install_tag: 'libsystemd',
@@ -1783,6 +1792,7 @@ if static_libsystemd != 'false'
dependencies : [libgcrypt_cflags,
liblz4_cflags,
libm,
libucontext,
libxz_cflags,
libzstd_cflags,
userspace],
+6
View File
@@ -242,4 +242,10 @@ Architecture uname_architecture(void);
# error "Please register your architecture here!"
#endif
#if defined(__hppa__) || defined(__hppa64__)
# define STACK_GROWS_UP 1
#else
# define STACK_GROWS_UP 0
#endif
DECLARE_STRING_TABLE_LOOKUP(architecture, Architecture);
+3
View File
@@ -112,10 +112,13 @@ typedef enum UnitNameMangle UnitNameMangle;
typedef enum UnitType UnitType;
typedef enum WaitFlags WaitFlags;
typedef struct Fiber Fiber;
typedef struct FiberOps FiberOps;
typedef struct Hashmap Hashmap;
typedef struct HashmapBase HashmapBase;
typedef struct IteratedCache IteratedCache;
typedef struct Iterator Iterator;
typedef struct LogContext LogContext;
typedef struct OrderedHashmap OrderedHashmap;
typedef struct OrderedSet OrderedSet;
typedef struct Set Set;
+51
View File
@@ -0,0 +1,51 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <poll.h>
#include <threads.h>
#include <unistd.h>
#include "errno-util.h"
#include "fiber-ops.h"
static thread_local const FiberOps *fiber_ops = NULL;
bool fiber_ops_is_set(void) {
return fiber_ops != NULL;
}
void fiber_ops_set(const FiberOps *ops) {
fiber_ops = ops;
}
int fiber_ops_ppoll(struct pollfd *fds, size_t n_fds, const struct timespec *timeout, const sigset_t *sigmask) {
if (fiber_ops)
return fiber_ops->ppoll(fds, n_fds, timeout, sigmask);
return RET_NERRNO(ppoll(fds, n_fds, timeout, sigmask));
}
ssize_t fiber_ops_read(int fd, void *buf, size_t count) {
if (fiber_ops)
return fiber_ops->read(fd, buf, count);
ssize_t n = read(fd, buf, count);
return n < 0 ? -errno : n;
}
ssize_t fiber_ops_write(int fd, const void *buf, size_t count) {
if (fiber_ops)
return fiber_ops->write(fd, buf, count);
return RET_NERRNO(write(fd, buf, count));
}
sd_future* fiber_ops_timeout(uint64_t timeout) {
assert(fiber_ops);
return fiber_ops->timeout(timeout);
}
sd_future* fiber_ops_cancel_wait_unref(sd_future *f) {
assert(fiber_ops);
return fiber_ops->cancel_wait_unref(f);
}
+34
View File
@@ -0,0 +1,34 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include "basic-forward.h"
typedef struct sd_future sd_future;
/* Hooks installed on a fiber so that functions in src/basic can transparently defer to the suspending
* variants in sd-future when invoked from a running fiber. Populated by sd_fiber_new() with pointers to the
* implementations in fiber-ops.c. */
typedef struct FiberOps {
int (*ppoll)(struct pollfd *fds, size_t n_fds, const struct timespec *timeout, const sigset_t *sigmask);
ssize_t (*read)(int fd, void *buf, size_t count);
ssize_t (*write)(int fd, const void *buf, size_t count);
sd_future* (*timeout)(uint64_t timeout);
sd_future* (*cancel_wait_unref)(sd_future *f);
} FiberOps;
bool fiber_ops_is_set(void);
void fiber_ops_set(const FiberOps *fiber_ops);
int fiber_ops_ppoll(struct pollfd *fds, size_t n_fds, const struct timespec *timeout, const sigset_t *sigmask);
ssize_t fiber_ops_read(int fd, void *buf, size_t count);
ssize_t fiber_ops_write(int fd, const void *buf, size_t count);
/* Mirror of SD_FIBER_TIMEOUT() for code under src/basic that doesn't include sd-future.h: dispatches
* through FiberOps so the actual sd_fiber_timeout() implementation lives in libsystemd. */
sd_future* fiber_ops_timeout(uint64_t timeout);
sd_future* fiber_ops_cancel_wait_unref(sd_future *f);
DEFINE_TRIVIAL_CLEANUP_FUNC(sd_future*, fiber_ops_cancel_wait_unref);
#define FIBER_OPS_TIMEOUT(timeout) _FIBER_OPS_TIMEOUT(UNIQ, (timeout))
#define _FIBER_OPS_TIMEOUT(uniq, timeout) \
_unused_ _cleanup_(fiber_ops_cancel_wait_unrefp) sd_future *UNIQ_T(_fot_, uniq) = fiber_ops_timeout(timeout)
+76 -17
View File
@@ -1,15 +1,26 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <fcntl.h>
#include <poll.h>
#include <stdio.h>
#include <string.h>
#include <sys/epoll.h> /* IWYU pragma: keep */
#include <time.h>
#include <unistd.h>
#include "errno-util.h"
#include "fiber-ops.h"
#include "io-util.h"
#include "time-util.h"
/* EPOLL_POLL_COMMON_MASK in io-util.h treats POLL* and EPOLL* as interchangeable; verify it. */
assert_cc((uint32_t) POLLIN == EPOLLIN);
assert_cc((uint32_t) POLLOUT == EPOLLOUT);
assert_cc((uint32_t) POLLERR == EPOLLERR);
assert_cc((uint32_t) POLLHUP == EPOLLHUP);
assert_cc((uint32_t) POLLPRI == EPOLLPRI);
assert_cc((uint32_t) POLLRDHUP == EPOLLRDHUP);
int flush_fd(int fd) {
int count = 0;
@@ -60,25 +71,44 @@ ssize_t loop_read(int fd, void *buf, size_t nbytes, bool do_poll) {
if (nbytes > (size_t) SSIZE_MAX)
return -EINVAL;
/* do_poll == false means "don't wait, return what we have if EAGAIN". If the fd is already
* non-blocking, read() can't block the thread, so the non-fiber path satisfies that semantic
* correctly even from a fiber. Only use the fiber path when the fd is blocking (where read()
* would otherwise block the entire event loop). */
int flags = 0;
if (fiber_ops_is_set() && !do_poll) {
flags = fcntl(fd, F_GETFL);
if (flags < 0)
return -errno;
}
do {
ssize_t k;
k = read(fd, p, nbytes);
if (k < 0) {
if (errno == EINTR)
continue;
if (fiber_ops_is_set() && (do_poll || !FLAGS_SET(flags, O_NONBLOCK))) {
/* On a fiber the read op suspends on EAGAIN until data is available, so we don't
* need a separate poll step or the do_poll knob. */
k = fiber_ops_read(fd, p, nbytes);
if (k < 0)
return n > 0 ? n : k;
} else {
k = read(fd, p, nbytes);
if (k < 0) {
if (errno == EINTR)
continue;
if (errno == EAGAIN && do_poll) {
if (errno == EAGAIN && do_poll) {
/* We knowingly ignore any return value here,
* and expect that any error/EOF is reported
* via read() */
/* We knowingly ignore any return value here,
* and expect that any error/EOF is reported
* via read() */
(void) fd_wait_for_event(fd, POLLIN, USEC_INFINITY);
continue;
(void) fd_wait_for_event(fd, POLLIN, USEC_INFINITY);
continue;
}
return n > 0 ? n : -errno;
}
return n > 0 ? n : -errno;
}
if (k == 0)
@@ -128,6 +158,37 @@ int loop_write_full(int fd, const void *buf, size_t nbytes, usec_t timeout) {
p = buf;
}
/* timeout == 0 means "don't wait, return -EAGAIN if not ready". If the fd is already
* non-blocking, write() can't block the thread, so the non-fiber path satisfies that
* semantic correctly even from a fiber. Only use the fiber path when the fd is blocking
* (where write() would otherwise block the entire event loop). */
int flags = 0;
if (fiber_ops_is_set() && timeout == 0) {
flags = fcntl(fd, F_GETFL);
if (flags < 0)
return -errno;
}
if (fiber_ops_is_set() && !FLAGS_SET(flags, O_NONBLOCK)) {
/* On a fiber the write op suspends on EAGAIN until the fd is writable; honor the
* caller's timeout via a deadline scope. */
FIBER_OPS_TIMEOUT(timestamp_is_set(timeout) ? timeout : USEC_INFINITY);
while (nbytes > 0) {
ssize_t k = fiber_ops_write(fd, p, nbytes);
if (k < 0)
return (int) k;
if (_unlikely_(nbytes > 0 && k == 0)) /* Can't really happen */
return -EIO;
assert((size_t) k <= nbytes);
p += k;
nbytes -= k;
}
return 0;
}
/* When timeout is 0 or USEC_INFINITY this is not used. But we initialize it to a sensible value. */
end = timestamp_is_set(timeout) ? usec_add(now(CLOCK_MONOTONIC), timeout) : USEC_INFINITY;
@@ -211,11 +272,9 @@ int ppoll_usec_full(struct pollfd *fds, size_t n_fds, usec_t timeout, const sigs
if (n_fds == 0 && timeout == 0)
return 0;
r = ppoll(fds, n_fds, timeout == USEC_INFINITY ? NULL : TIMESPEC_STORE(timeout), ss);
if (r < 0)
return -errno;
if (r == 0)
return 0;
r = fiber_ops_ppoll(fds, n_fds, timeout == USEC_INFINITY ? NULL : TIMESPEC_STORE(timeout), ss);
if (r <= 0)
return r;
for (size_t i = 0, n = r; i < n_fds && n > 0; i++) {
if (fds[i].revents == 0)
+6
View File
@@ -3,6 +3,12 @@
#include "basic-forward.h"
/* The intersection of poll() and epoll_wait() event masks. Linux defines POLL* and EPOLL* with the
* same numeric values for these — see the assert_cc()s in io-util.c — so this mask can be used
* interchangeably as a `revents` (poll) or `events` (epoll) bitset. */
#define EPOLL_POLL_COMMON_MASK \
(EPOLLIN | EPOLLOUT | EPOLLERR | EPOLLHUP | EPOLLPRI | EPOLLRDHUP)
int flush_fd(int fd);
ssize_t loop_read(int fd, void *buf, size_t nbytes, bool do_poll);
+8
View File
@@ -177,6 +177,14 @@ size_t log_context_num_fields(void) {
return _log_context_num_fields;
}
void log_context_swap(LogContext **log_context, size_t *num_fields) {
assert(log_context);
assert(num_fields);
SWAP_TWO(_log_context, *log_context);
SWAP_TWO(_log_context_num_fields, *num_fields);
}
void _reset_log_level(int *saved_log_level) {
assert(saved_log_level);
+2
View File
@@ -66,6 +66,8 @@ size_t log_context_num_contexts(void);
/* Returns the number of fields in all attached log contexts. */
size_t log_context_num_fields(void);
void log_context_swap(LogContext **log_context, size_t *num_fields);
void _reset_log_level(int *saved_log_level);
#define _LOG_CONTEXT_SET_LOG_LEVEL(level, l) \
+6
View File
@@ -87,6 +87,12 @@ bool _log_message_dummy = false; /* Always false */
} \
} while (false)
void log_prefix_swap(const char **prefix) {
assert(prefix);
SWAP_TWO(log_prefix, *prefix);
}
static void log_close_console(void) {
/* See comment in log_close_journal() */
(void) safe_close_above_stdio(TAKE_FD(console_fd));
+2
View File
@@ -380,6 +380,8 @@ int log_syntax_parse_error_internal(
void log_setup(void);
const char* _log_set_prefix(const char *prefix, bool force);
void log_prefix_swap(const char **prefix);
static inline const char* _log_unset_prefixp(const char **p) {
assert(p);
_log_set_prefix(*p, true);
+1
View File
@@ -36,6 +36,7 @@ basic_sources = files(
'ether-addr-util.c',
'extract-word.c',
'fd-util.c',
'fiber-ops.c',
'fileio.c',
'filesystems.c',
'format-ifname.c',
+18 -5
View File
@@ -7,6 +7,7 @@
#include "alloc-util.h"
#include "errno-util.h"
#include "fd-util.h"
#include "fiber-ops.h"
#include "format-util.h"
#include "hash-funcs.h"
#include "io-util.h"
@@ -466,16 +467,28 @@ int pidref_wait_for_terminate_full(PidRef *pidref, usec_t timeout, siginfo_t *re
if (pidref->pid == 1 || pidref_is_self(pidref))
return -ECHILD;
if (timeout != USEC_INFINITY && pidref->fd < 0)
if (pidref->fd < 0 && (timeout != USEC_INFINITY || fiber_ops_is_set()))
return -ENOMEDIUM;
usec_t ts = timeout == USEC_INFINITY ? USEC_INFINITY : usec_add(now(CLOCK_MONOTONIC), timeout);
/* Poll the pidfd before waitid() if either there's a finite timeout (so we can honor it) or
* we're on a fiber (so fd_wait_for_event() can suspend us instead of blocking the event loop
* inside waitid()). Otherwise let waitid() block directly. The precondition above guarantees
* pidref->fd >= 0 in both cases. */
bool poll_first = ts != USEC_INFINITY || fiber_ops_is_set();
for (;;) {
if (ts != USEC_INFINITY) {
usec_t left = usec_sub_unsigned(ts, now(CLOCK_MONOTONIC));
if (left == 0)
return -ETIMEDOUT;
if (poll_first) {
usec_t left;
if (ts == USEC_INFINITY)
left = USEC_INFINITY;
else {
left = usec_sub_unsigned(ts, now(CLOCK_MONOTONIC));
if (left == 0)
return -ETIMEDOUT;
}
r = fd_wait_for_event(pidref->fd, POLLIN, left);
if (r == 0)
+7
View File
@@ -18,3 +18,10 @@ static_assert(MFD_NOEXEC_SEAL == 0x0008U, "");
#else
static_assert(MFD_EXEC == 0x0010U, "");
#endif
/* since Linux 6.13 / glibc-2.42 */
#ifndef MADV_GUARD_INSTALL
# define MADV_GUARD_INSTALL 102
#else
static_assert(MADV_GUARD_INSTALL == 102, "");
#endif
+19 -2
View File
@@ -33,6 +33,7 @@ sd_daemon_sources = files('sd-daemon/sd-daemon.c')
############################################################
sd_event_sources = files(
'sd-event/event-future.c',
'sd-event/event-util.c',
'sd-event/sd-event.c',
)
@@ -48,6 +49,7 @@ sd_bus_sources = files(
'sd-bus/bus-dump.c',
'sd-bus/bus-dump-json.c',
'sd-bus/bus-error.c',
'sd-bus/bus-future.c',
'sd-bus/bus-internal.c',
'sd-bus/bus-introspect.c',
'sd-bus/bus-kernel.c',
@@ -75,6 +77,14 @@ sd_device_sources = files(
############################################################
sd_future_sources = files(
'sd-future/fiber-io.c',
'sd-future/fiber.c',
'sd-future/sd-future.c',
)
############################################################
sd_login_sources = files('sd-login/sd-login.c')
############################################################
@@ -135,8 +145,9 @@ libsystemd_sources = files(
'sd-resolve/sd-resolve.c',
) + sd_journal_sources + sd_id128_sources + sd_daemon_sources \
+ sd_event_sources + sd_bus_sources + sd_device_sources \
+ sd_login_sources + sd_json_sources + sd_varlink_sources \
+ sd_path_sources + sd_netlink_sources + sd_network_sources
+ sd_future_sources + sd_login_sources + sd_json_sources \
+ sd_varlink_sources + sd_path_sources + sd_netlink_sources \
+ sd_network_sources
sources += libsystemd_sources
@@ -151,6 +162,7 @@ libsystemd_static = static_library(
link_with : [libc_wrapper_static,
libbasic_static],
dependencies : [libm,
libucontext,
userspace],
build_by_default : false)
@@ -174,11 +186,16 @@ libsystemd_pc = custom_target(
simple_tests += files(
'sd-bus/test-bus-creds.c',
'sd-bus/test-bus-fiber.c',
'sd-bus/test-bus-introspect.c',
'sd-bus/test-bus-match.c',
'sd-bus/test-bus-vtable.c',
'sd-device/test-device-util.c',
'sd-device/test-sd-device-monitor.c',
'sd-event/test-event-future.c',
'sd-future/test-fiber.c',
'sd-future/test-fiber-io.c',
'sd-future/test-fiber-ops.c',
'sd-hwdb/test-sd-hwdb.c',
'sd-id128/test-id128.c',
'sd-journal/test-audit-type.c',
+135
View File
@@ -0,0 +1,135 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "sd-bus.h"
#include "sd-future.h"
#include "alloc-util.h"
#include "bus-future.h"
#include "bus-internal.h"
#include "bus-message.h"
typedef struct BusFuture {
sd_bus_slot *slot;
sd_bus_message *reply;
} BusFuture;
static void* bus_future_alloc(void) {
return new0(BusFuture, 1);
}
static void bus_future_free(sd_future *f) {
BusFuture *bf = ASSERT_PTR(sd_future_get_private(f));
sd_bus_slot_unref(bf->slot);
sd_bus_message_unref(bf->reply);
free(bf);
}
static int bus_future_cancel(sd_future *f) {
BusFuture *bf = ASSERT_PTR(sd_future_get_private(ASSERT_PTR(f)));
bf->slot = sd_bus_slot_unref(bf->slot);
return sd_future_resolve(f, -ECANCELED);
}
static const sd_future_ops bus_future_ops = {
.size = sizeof(sd_future_ops),
.alloc = bus_future_alloc,
.free = bus_future_free,
.cancel = bus_future_cancel,
};
static int bus_future_handler(sd_bus_message *m, void *userdata, sd_bus_error *reterr_error) {
sd_future *f = ASSERT_PTR(userdata);
BusFuture *bf = ASSERT_PTR(sd_future_get_private(f));
int r;
/* Resolve with 0 on a success reply and -errno (derived from the D-Bus error name) on a
* method error reply, so a caller awaiting the future learns about call failures from the
* resolution value alone. The reply itself is always stashed in bf->reply so
* future_get_bus_reply() can hand back the detailed sd_bus_error (name + message) on
* top of the bare errno. Cancellation surfaces as -ECANCELED via bus_future_cancel(),
* with bf->reply left NULL — callers can distinguish "got an error reply" from "no reply
* will arrive" by whether future_get_bus_reply() can produce a message. */
bf->slot = sd_bus_slot_unref(bf->slot);
bf->reply = sd_bus_message_ref(m);
r = sd_bus_message_is_method_error(m, NULL) ? -sd_bus_message_get_errno(m) : 0;
return sd_future_resolve(f, r);
}
int bus_call_future(sd_bus *bus, sd_bus_message *m, uint64_t usec, sd_future **ret) {
int r;
assert(bus);
assert(m);
assert(ret);
_cleanup_(sd_future_unrefp) sd_future *f = NULL;
r = sd_future_new(&bus_future_ops, &f);
if (r < 0)
return r;
BusFuture *bf = sd_future_get_private(f);
r = sd_bus_call_async(bus, &bf->slot, m, bus_future_handler, f, usec);
if (r < 0)
return r;
*ret = TAKE_PTR(f);
return 0;
}
int future_get_bus_reply(sd_future *f, sd_bus_error *reterr_error, sd_bus_message **ret_reply) {
BusFuture *bf = ASSERT_PTR(sd_future_get_private(ASSERT_PTR(f)));
sd_bus_message *reply = ASSERT_PTR(bf->reply);
assert(sd_future_get_ops(f) == &bus_future_ops);
assert(sd_future_state(f) == SD_FUTURE_RESOLVED);
if (sd_bus_message_is_method_error(reply, NULL)) {
if (reterr_error)
return sd_bus_error_copy(reterr_error, sd_bus_message_get_error(reply));
return -sd_bus_message_get_errno(reply);
}
if (reply->n_fds > 0 && !sd_bus_message_get_bus(reply)->accept_fd)
return sd_bus_error_set(reterr_error, SD_BUS_ERROR_INCONSISTENT_MESSAGE,
"Reply message contained file descriptors which I couldn't accept. Sorry.");
if (reterr_error)
*reterr_error = SD_BUS_ERROR_NULL;
if (ret_reply)
*ret_reply = sd_bus_message_ref(reply);
return 1;
}
int bus_call_suspend(
sd_bus *bus,
sd_bus_message *m,
uint64_t usec,
sd_bus_error *reterr_error,
sd_bus_message **ret_reply) {
int r;
assert(bus);
assert(m);
assert(sd_fiber_is_running());
_cleanup_(sd_future_cancel_wait_unrefp) sd_future *f = NULL;
r = bus_call_future(bus, m, usec, &f);
if (r < 0)
return sd_bus_error_set_errno(reterr_error, r);
r = sd_fiber_suspend();
/* If the future isn't resolved, the suspend was interrupted before a reply arrived (fiber
* cancelled, fiber-wide SD_FIBER_TIMEOUT scope expired, …). There's no reply to extract,
* so surface the resume error directly. When the future is resolved, future_get_bus_reply()
* recovers either the reply or the detailed sd_bus_error from the error reply. */
if (sd_future_state(f) != SD_FUTURE_RESOLVED)
return sd_bus_error_set_errno(reterr_error, r);
return future_get_bus_reply(f, reterr_error, ret_reply);
}
+14
View File
@@ -0,0 +1,14 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include "sd-forward.h"
int bus_call_future(sd_bus *bus, sd_bus_message *m, uint64_t usec, sd_future **ret);
int future_get_bus_reply(sd_future *f, sd_bus_error *reterr_error, sd_bus_message **ret_reply);
int bus_call_suspend(
sd_bus *bus,
sd_bus_message *m,
uint64_t usec,
sd_bus_error *reterr_error,
sd_bus_message **ret_reply);
+13
View File
@@ -17,6 +17,13 @@
#define DEFAULT_SYSTEM_BUS_ADDRESS "unix:path=/run/dbus/system_bus_socket"
#define DEFAULT_USER_BUS_ADDRESS_FMT "unix:path=%s/bus"
/* Private vtable flag: dispatch the method handler on its own fiber, so it can use suspending
* primitives (sd_bus_call() on a fiber, sd_fiber_sleep(), loop_read_suspend(), ...) without
* blocking the event loop for other connections or method calls. Kept out of the public
* sd-bus-vtable.h so the fiber runtime stays an implementation detail of systemd. The bit value is
* reserved in sd-bus-vtable.h to make sure it never collides with a future public flag. */
#define SD_BUS_VTABLE_METHOD_FIBER (UINT64_C(1) << 10)
typedef struct BusReplyCallback {
sd_bus_message_handler_t callback;
usec_t timeout_usec; /* this is a relative timeout until we reach the BUS_HELLO state, and an absolute one right after */
@@ -222,6 +229,12 @@ typedef struct sd_bus {
Set *vtable_methods;
Set *vtable_properties;
/* Futures for outstanding SD_BUS_VTABLE_METHOD_FIBER dispatches. Entries are added as the
* dispatcher spawns each fiber and removed when the fiber resolves. On bus_enter_closing()
* we cancel everything in here and then wait in process_closing() until the set drains,
* before tearing down the rest of the bus. */
Set *fiber_futures;
union sockaddr_union sockaddr;
socklen_t sockaddr_size;
+109
View File
@@ -3,6 +3,7 @@
#include <linux/capability.h>
#include "sd-bus.h"
#include "sd-future.h"
#include "alloc-util.h"
#include "bus-internal.h"
@@ -337,6 +338,67 @@ static int check_access(sd_bus *bus, sd_bus_message *m, BusVTableMember *c, sd_b
return sd_bus_error_setf(reterr_error, SD_BUS_ERROR_ACCESS_DENIED, "Access to %s.%s() not permitted.", c->interface, c->member);
}
typedef struct BusFiberData {
sd_bus *bus;
sd_bus_message *message;
sd_bus_slot *slot;
sd_bus_message_handler_t handler;
void *userdata;
} BusFiberData;
static BusFiberData* bus_fiber_data_free(BusFiberData *d) {
if (!d)
return NULL;
sd_bus_slot_unref(d->slot);
sd_bus_message_unref(d->message);
sd_bus_unref(d->bus);
return mfree(d);
}
DEFINE_TRIVIAL_CLEANUP_FUNC(BusFiberData*, bus_fiber_data_free);
static void bus_fiber_data_destroy(void *userdata) {
bus_fiber_data_free(userdata);
}
static void bus_fiber_future_unref(void *p) {
sd_future_unref(p);
}
DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR(
bus_fiber_future_hash_ops,
void,
trivial_hash_func,
trivial_compare_func,
bus_fiber_future_unref);
static int bus_fiber_resolved(sd_future *f) {
sd_bus *bus = ASSERT_PTR(sd_future_get_userdata(f));
assert_se(set_remove(bus->fiber_futures, f) == f);
sd_future_unref(f);
return 0;
}
static int bus_fiber_entry(void *userdata) {
BusFiberData *d = ASSERT_PTR(userdata);
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
int r;
/* Note: unlike the synchronous dispatch path, we deliberately do NOT set
* bus->current_slot/handler/userdata around the callback. Those fields track the slot of the
* message currently being dispatched inline and must be NULL at each entry into
* bus_process_internal(). Because a fiber handler can yield and let the event loop dispatch
* other messages before it resumes, leaving current_slot non-NULL across yields would trip
* that invariant. Fiber handlers receive their slot's userdata via the handler argument, so
* sd_bus_get_current_slot()/handler()/userdata() simply aren't meaningful inside them — the
* handler should use the message/userdata parameters directly instead. */
r = d->handler(d->message, d->userdata, &error);
return bus_maybe_reply_error(d->message, r, &error);
}
static int method_callbacks_run(
sd_bus *bus,
sd_bus_message *m,
@@ -407,6 +469,53 @@ static int method_callbacks_run(
slot = container_of(c->parent, sd_bus_slot, node_vtable);
if (FLAGS_SET(c->vtable->flags, SD_BUS_VTABLE_METHOD_FIBER)) {
/* A fiber-dispatched method requires an event loop to spawn the fiber on.
* By the time a method call actually arrives the bus is running, so the
* event loop should already be attached — if not, the caller set up the bus
* wrong and there's no meaningful recovery. */
assert(bus->event);
_cleanup_(bus_fiber_data_freep) BusFiberData *d = new(BusFiberData, 1);
if (!d)
return -ENOMEM;
*d = (BusFiberData) {
.bus = sd_bus_ref(bus),
.message = sd_bus_message_ref(m),
.slot = sd_bus_slot_ref(slot),
.handler = c->vtable->x.method.handler,
.userdata = u,
};
_cleanup_(sd_future_unrefp) sd_future *f = NULL;
r = sd_fiber_new(bus->event, c->member, bus_fiber_entry, d, bus_fiber_data_destroy, &f);
if (r < 0)
return bus_maybe_reply_error(m, r, NULL);
/* The fiber now owns d via bus_fiber_data_destroy. Drop our cleanup before any
* further fallible calls, so a later failure unwinding f doesn't double-free d. */
TAKE_PTR(d);
r = set_ensure_put(&bus->fiber_futures, &bus_fiber_future_hash_ops, f);
if (r < 0)
return bus_maybe_reply_error(m, r, NULL);
assert(r > 0);
/* Track the future on the bus so shutdown can cancel it and wait for it. */
r = sd_future_set_callback(f, bus_fiber_resolved, bus);
if (r < 0) {
/* TAKE_PTR(f) hasn't run yet, so our cleanup attribute still owns the
* ref; set_remove() returns the raw pointer without firing the hash_ops
* destructor, and the cleanup will unref f on return. */
assert_se(set_remove(bus->fiber_futures, f) == f);
return bus_maybe_reply_error(m, r, NULL);
}
TAKE_PTR(f);
return 1;
}
bus->current_slot = sd_bus_slot_ref(slot);
bus->current_handler = c->vtable->x.method.handler;
bus->current_userdata = u;
+44 -6
View File
@@ -10,12 +10,14 @@
#include "sd-bus.h"
#include "sd-event.h"
#include "sd-future.h"
#include "af-list.h"
#include "alloc-util.h"
#include "bus-container.h"
#include "bus-control.h"
#include "bus-error.h"
#include "bus-future.h"
#include "bus-internal.h"
#include "bus-kernel.h"
#include "bus-label.h"
@@ -222,6 +224,12 @@ static sd_bus* bus_free(sd_bus *b) {
ordered_hashmap_free(b->reply_callbacks);
prioq_free(b->reply_callbacks_prioq);
/* Outstanding fiber handlers pin the bus via their BusFiberData ref, so by the time refcount
* reaches zero and bus_free() runs, every fiber has already resolved and removed itself from
* this set. */
assert(set_isempty(b->fiber_futures));
set_free(b->fiber_futures);
assert(b->match_callbacks.type == BUS_MATCH_ROOT);
bus_match_free(&b->match_callbacks);
@@ -1809,6 +1817,9 @@ _public_ sd_bus* sd_bus_flush_close_unref(sd_bus *bus) {
}
void bus_enter_closing(sd_bus *bus, int exit_code) {
sd_future *f;
int r;
assert(bus);
if (!IN_SET(bus->state, BUS_WATCH_BIND, BUS_OPENING, BUS_AUTHENTICATING, BUS_HELLO, BUS_RUNNING))
@@ -1816,6 +1827,19 @@ void bus_enter_closing(sd_bus *bus, int exit_code) {
bus_set_state(bus, BUS_CLOSING);
bus->exit_code = exit_code;
/* Cancel all outstanding fiber-dispatched method handlers. Most cancellations are scheduled
* asynchronously (fibers resolve with -ECANCELED the next time they run), but a fiber still
* in FIBER_STATE_INITIAL resolves synchronously, which fires bus_fiber_resolved() and
* removes f from this set mid-iteration. That's safe because SET_FOREACH permits removal of
* exactly the current entry — see the assertion in hashmap_iterate_entry(). Either way this
* doesn't block here: process_closing() waits for the fiber_futures set to drain before it
* continues tearing down the rest of the bus. */
SET_FOREACH(f, bus->fiber_futures) {
r = sd_future_cancel(f);
if (r < 0)
log_debug_errno(r, "Failed to cancel outstanding fiber method handler, ignoring: %m");
}
}
/* Define manually so we can add the PID check */
@@ -2388,23 +2412,30 @@ _public_ int sd_bus_call(
sd_bus_error *reterr_error,
sd_bus_message **ret_reply) {
_cleanup_(sd_bus_message_unrefp) sd_bus_message *m = sd_bus_message_ref(_m);
usec_t timeout;
uint64_t cookie;
size_t i;
int r;
bus_assert_return(m, -EINVAL, reterr_error);
bus_assert_return(m->header->type == SD_BUS_MESSAGE_METHOD_CALL, -EINVAL, reterr_error);
bus_assert_return(!(m->header->flags & BUS_MESSAGE_NO_REPLY_EXPECTED), -EINVAL, reterr_error);
bus_assert_return(_m, -EINVAL, reterr_error);
bus_assert_return(_m->header->type == SD_BUS_MESSAGE_METHOD_CALL, -EINVAL, reterr_error);
bus_assert_return(!(_m->header->flags & BUS_MESSAGE_NO_REPLY_EXPECTED), -EINVAL, reterr_error);
bus_assert_return(!bus_error_is_dirty(reterr_error), -EINVAL, reterr_error);
if (bus)
assert_return(bus = bus_resolve(bus), -ENOPKG);
else
assert_return(bus = m->bus, -ENOTCONN);
assert_return(bus = _m->bus, -ENOTCONN);
bus_assert_return(!bus_origin_changed(bus), -ECHILD, reterr_error);
/* If the current fiber and the bus share their event loop, we can use sd_bus_call_suspend()
* instead which does an async method call. This allows multiple invocations of sd_bus_call() to
* happen across multiple fibers at once. */
if (sd_fiber_is_running() && bus->event == sd_fiber_get_event())
return bus_call_suspend(bus, _m, usec, reterr_error, ret_reply);
_cleanup_(sd_bus_message_unrefp) sd_bus_message *m = sd_bus_message_ref(_m);
if (!BUS_IS_OPEN(bus->state)) {
r = -ENOTCONN;
goto fail;
@@ -3177,7 +3208,14 @@ static int process_closing(sd_bus *bus, sd_bus_message **ret) {
assert(bus);
assert(bus->state == BUS_CLOSING);
/* First, fail all outstanding method calls */
/* Wait for any still-running fiber method handlers to finish unwinding their cancellation
* before tearing down the rest of the bus. bus_enter_closing() scheduled the cancel; each
* fiber resolves asynchronously and bus_fiber_resolved() removes it from the set. Returning
* 1 here keeps the bus in CLOSING state so the event loop drives the fibers to completion. */
if (!set_isempty(bus->fiber_futures))
return 1;
/* Then, fail all outstanding method calls */
c = ordered_hashmap_first(bus->reply_callbacks);
if (c)
return process_closing_reply_callback(bus, c);
+28 -23
View File
@@ -1,11 +1,11 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <fcntl.h>
#include <pthread.h>
#include <sys/resource.h>
#include <unistd.h>
#include "sd-bus.h"
#include "sd-future.h"
#include "alloc-util.h"
#include "bus-error.h"
@@ -102,7 +102,8 @@ static int server_init(sd_bus **ret) {
return 0;
}
static int server(sd_bus *bus) {
static int server(void *userdata) {
sd_bus *bus = ASSERT_PTR(userdata);
bool client1_gone = false, client2_gone = false;
int r;
@@ -178,7 +179,9 @@ static int server(sd_bus *bus) {
client2_gone = true;
} else if (sd_bus_message_is_method_call(m, "org.freedesktop.systemd.test", "Slow")) {
sleep(1);
r = sd_fiber_sleep(1 * USEC_PER_SEC);
if (r < 0)
return r;
r = sd_bus_reply_method_return(m, NULL);
if (r < 0)
@@ -194,10 +197,10 @@ static int server(sd_bus *bus) {
log_info("Received fd=%d", fd);
if (write(fd, &x, 1) < 0) {
r = log_error_errno(errno, "Failed to write to fd: %m");
ssize_t n = sd_fiber_write(fd, &x, 1);
if (n < 0) {
safe_close(fd);
return r;
return log_error_errno(n, "Failed to write to fd: %m");
}
r = sd_bus_reply_method_return(m, NULL);
@@ -217,7 +220,7 @@ static int server(sd_bus *bus) {
return 0;
}
static void* client1(void *p) {
static int client1(void *userdata) {
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
@@ -277,9 +280,9 @@ static void* client1(void *p) {
goto finish;
}
errno = 0;
if (read(pp[0], &x, 1) <= 0) {
log_error("Failed to read from pipe: %s", STRERROR_OR_EOF(errno));
ssize_t n = sd_fiber_read(pp[0], &x, 1);
if (n <= 0) {
log_error("Failed to read from pipe: %s", STRERROR_OR_EOF(n));
goto finish;
}
@@ -303,7 +306,7 @@ finish:
}
return INT_TO_PTR(r);
return r;
}
static int quit_callback(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) {
@@ -315,7 +318,7 @@ static int quit_callback(sd_bus_message *m, void *userdata, sd_bus_error *ret_er
return 1;
}
static void* client2(void *p) {
static int client2(void *userdata) {
_cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL;
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
@@ -494,7 +497,7 @@ finish:
(void) sd_bus_send(bus, q, NULL);
}
return INT_TO_PTR(r);
return r;
}
static ino_t get_inode(int fd) {
@@ -626,9 +629,9 @@ TEST(ctrunc) {
}
TEST(chat) {
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
_cleanup_(sd_future_unrefp) sd_future *f_server = NULL, *f_client1 = NULL, *f_client2 = NULL;
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
pthread_t c1, c2;
void *p;
int r;
test_setup_logging(LOG_INFO);
@@ -639,16 +642,18 @@ TEST(chat) {
log_info("Initialized...");
ASSERT_OK(-pthread_create(&c1, NULL, client1, NULL));
ASSERT_OK(-pthread_create(&c2, NULL, client2, NULL));
ASSERT_OK(sd_event_new(&e));
ASSERT_OK(sd_event_set_exit_on_idle(e, true));
r = server(bus);
ASSERT_OK(sd_fiber_new(e, "client-1", client1, NULL, /* destroy= */ NULL, &f_client1));
ASSERT_OK(sd_fiber_new(e, "client-2", client2, NULL, /* destroy= */ NULL, &f_client2));
ASSERT_OK(sd_fiber_new(e, "server", server, bus, /* destroy= */ NULL, &f_server));
ASSERT_OK(-pthread_join(c1, &p));
ASSERT_OK(PTR_TO_INT(p));
ASSERT_OK(-pthread_join(c2, &p));
ASSERT_OK(PTR_TO_INT(p));
ASSERT_OK(r);
ASSERT_OK(sd_event_loop(e));
ASSERT_OK(sd_future_result(f_client1));
ASSERT_OK(sd_future_result(f_client2));
ASSERT_OK(sd_future_result(f_server));
}
DEFINE_TEST_MAIN(LOG_INFO);
+207
View File
@@ -0,0 +1,207 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <sys/socket.h>
#include "sd-bus.h"
#include "sd-event.h"
#include "sd-future.h"
#include "bus-internal.h"
#include "tests.h"
#include "time-util.h"
typedef struct Context {
/* Counters for the concurrency check: every Concurrent invocation bumps in_flight on entry
* and drops it on exit, and tracks the maximum observed concurrency. If fiber dispatch
* works, two overlapping client calls must both be inside the handler at the same time,
* giving a max of at least 2. */
int in_flight;
int max_in_flight;
sd_future *waiter;
} Context;
static int method_concurrent(sd_bus_message *m, void *userdata, sd_bus_error *reterr_error) {
Context *c = ASSERT_PTR(userdata);
ASSERT_OK_POSITIVE(sd_fiber_is_running());
c->in_flight++;
if (c->in_flight > c->max_in_flight)
c->max_in_flight = c->in_flight;
/* Synchronize on the peer instead of sleeping: the first handler to enter stashes its
* fiber and suspends; the second resumes it and then yields, so the first runs to
* completion (sending its reply) before the second proceeds. Both are therefore in
* flight at the same time regardless of how long any individual dispatch takes, and
* the reply order matches the request order. */
if (c->in_flight < 2) {
assert(!c->waiter);
c->waiter = sd_fiber_get_current();
ASSERT_OK(sd_fiber_suspend());
} else {
ASSERT_OK(sd_fiber_resume(TAKE_PTR(c->waiter), 0));
ASSERT_OK(sd_fiber_yield());
}
c->in_flight--;
return sd_bus_reply_method_return(m, NULL);
}
static int method_fail_errno(sd_bus_message *m, void *userdata, sd_bus_error *reterr_error) {
ASSERT_OK_POSITIVE(sd_fiber_is_running());
/* Yielding first exercises the deferred-error path in the fiber entry: the handler returns
* a negative errno after suspending, and bus_maybe_reply_error() must still turn that into
* a matching sd_bus error reply. */
ASSERT_OK(sd_fiber_sleep(1 * USEC_PER_MSEC));
return -EACCES;
}
static int method_fail_error(sd_bus_message *m, void *userdata, sd_bus_error *reterr_error) {
ASSERT_OK_POSITIVE(sd_fiber_is_running());
ASSERT_OK(sd_fiber_sleep(1 * USEC_PER_MSEC));
return sd_bus_error_set(reterr_error, SD_BUS_ERROR_INVALID_ARGS, "bad arguments from fiber");
}
static const sd_bus_vtable vtable[] = {
SD_BUS_VTABLE_START(0),
SD_BUS_METHOD("Concurrent", NULL, NULL, method_concurrent,
SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_METHOD_FIBER),
SD_BUS_METHOD("FailErrno", NULL, NULL, method_fail_errno,
SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_METHOD_FIBER),
SD_BUS_METHOD("FailError", NULL, NULL, method_fail_error,
SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_METHOD_FIBER),
SD_BUS_VTABLE_END,
};
typedef struct Setup {
int fds[2];
Context *c;
} Setup;
static int attach_pair(Setup *s, sd_bus **ret_server, sd_bus **ret_client) {
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *server = NULL, *client = NULL;
sd_id128_t id;
assert(ret_server);
assert(ret_client);
ASSERT_OK(sd_id128_randomize(&id));
ASSERT_OK(sd_bus_new(&server));
ASSERT_OK(sd_bus_set_description(server, "server"));
ASSERT_OK(sd_bus_set_fd(server, s->fds[0], s->fds[0]));
ASSERT_OK(sd_bus_set_server(server, true, id));
ASSERT_OK(sd_bus_attach_event(server, sd_fiber_get_event(), 0));
ASSERT_OK(sd_bus_add_object_vtable(server, NULL, "/test", "test.Fiber", vtable, s->c));
ASSERT_OK(sd_bus_start(server));
ASSERT_OK(sd_bus_new(&client));
ASSERT_OK(sd_bus_set_description(client, "client"));
ASSERT_OK(sd_bus_set_fd(client, s->fds[1], s->fds[1]));
ASSERT_OK(sd_bus_attach_event(client, sd_fiber_get_event(), 0));
ASSERT_OK(sd_bus_start(client));
*ret_server = TAKE_PTR(server);
*ret_client = TAKE_PTR(client);
return 0;
}
static int call_concurrent_fiber(void *userdata) {
sd_bus *client = ASSERT_PTR(userdata);
/* A plain suspending sd_bus_call() — on a fiber this goes through sd_bus_call_suspend()
* which multiplexes onto the single client connection, so multiple caller fibers can have
* calls in flight at the same time. */
return sd_bus_call_method(client, NULL, "/test", "test.Fiber", "Concurrent",
NULL, NULL, NULL);
}
static int concurrency_fiber(void *userdata) {
Setup *s = ASSERT_PTR(userdata);
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *server = NULL, *client = NULL;
_cleanup_(sd_future_cancel_wait_unrefp) sd_future *f_a = NULL, *f_b = NULL;
ASSERT_OK(attach_pair(s, &server, &client));
/* Two concurrent calls on the shared client bus. Each lands in method_concurrent which
* blocks on the peer; if fiber dispatch works the second is entered while the first is
* suspended, so max_in_flight on the context reaches 2. */
ASSERT_OK(sd_fiber_new(sd_fiber_get_event(), "call-a", call_concurrent_fiber, client,
/* destroy= */ NULL, &f_a));
ASSERT_OK(sd_fiber_new(sd_fiber_get_event(), "call-b", call_concurrent_fiber, client,
/* destroy= */ NULL, &f_b));
ASSERT_OK(sd_fiber_await(f_a));
ASSERT_OK(sd_fiber_await(f_b));
ASSERT_OK(sd_future_result(f_a));
ASSERT_OK(sd_future_result(f_b));
return 0;
}
TEST(fiber_method_concurrency) {
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
_cleanup_(sd_future_unrefp) sd_future *f = NULL;
Context c = {};
Setup s = { .c = &c };
ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM, 0, s.fds));
ASSERT_OK(sd_event_new(&e));
ASSERT_OK(sd_event_set_exit_on_idle(e, true));
ASSERT_OK(sd_fiber_new(e, "concurrency", concurrency_fiber, &s, /* destroy= */ NULL, &f));
ASSERT_OK(sd_event_loop(e));
ASSERT_OK(sd_future_result(f));
ASSERT_GE(c.max_in_flight, 2);
}
static int errors_fiber(void *userdata) {
Setup *s = ASSERT_PTR(userdata);
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *server = NULL, *client = NULL;
ASSERT_OK(attach_pair(s, &server, &client));
/* A fiber handler that returns a negative errno gets turned into a matching sd_bus error
* reply (bus_maybe_reply_error → sd_bus_reply_method_errno). */
_cleanup_(sd_bus_error_free) sd_bus_error e1 = SD_BUS_ERROR_NULL;
ASSERT_ERROR(sd_bus_call_method(client, NULL, "/test", "test.Fiber", "FailErrno",
&e1, NULL, NULL),
EACCES);
ASSERT_TRUE(sd_bus_error_has_name(&e1, SD_BUS_ERROR_ACCESS_DENIED));
/* A fiber handler that populates sd_bus_error directly propagates both name and message. */
_cleanup_(sd_bus_error_free) sd_bus_error e2 = SD_BUS_ERROR_NULL;
ASSERT_FAIL(sd_bus_call_method(client, NULL, "/test", "test.Fiber", "FailError",
&e2, NULL, NULL));
ASSERT_TRUE(sd_bus_error_has_name(&e2, SD_BUS_ERROR_INVALID_ARGS));
ASSERT_STREQ(e2.message, "bad arguments from fiber");
return 0;
}
TEST(fiber_method_errors) {
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
_cleanup_(sd_future_unrefp) sd_future *f = NULL;
Context c = {};
Setup s = { .c = &c };
ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM, 0, s.fds));
ASSERT_OK(sd_event_new(&e));
ASSERT_OK(sd_event_set_exit_on_idle(e, true));
ASSERT_OK(sd_fiber_new(e, "errors", errors_fiber, &s, /* destroy= */ NULL, &f));
ASSERT_OK(sd_event_loop(e));
ASSERT_OK(sd_future_result(f));
}
DEFINE_TEST_MAIN(LOG_DEBUG);
+21 -41
View File
@@ -1,8 +1,7 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <pthread.h>
#include "sd-bus.h"
#include "sd-future.h"
#include "alloc-util.h"
#include "bus-internal.h"
@@ -211,9 +210,9 @@ static int enumerator3_callback(sd_bus *bus, const char *path, void *userdata, c
return 1;
}
static void* server(void *p) {
struct context *c = p;
sd_bus *bus = NULL;
static int server(void *userdata) {
struct context *c = ASSERT_PTR(userdata);
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
sd_id128_t id;
int r;
@@ -242,36 +241,25 @@ static void* server(void *p) {
log_error("Loop!");
r = sd_bus_process(bus, NULL);
if (r < 0) {
log_error_errno(r, "Failed to process requests: %m");
goto fail;
}
if (r < 0)
return log_error_errno(r, "Failed to process requests: %m");
if (r == 0) {
r = sd_bus_wait(bus, UINT64_MAX);
if (r < 0) {
log_error_errno(r, "Failed to wait: %m");
goto fail;
}
if (r < 0)
return log_error_errno(r, "Failed to wait: %m");
continue;
}
}
r = 0;
fail:
if (bus) {
sd_bus_flush(bus);
sd_bus_unref(bus);
}
return INT_TO_PTR(r);
return 0;
}
static int client(struct context *c) {
static int client(void *p) {
struct context *c = ASSERT_PTR(p);
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
_cleanup_(sd_bus_unrefp) sd_bus *bus = NULL;
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
_cleanup_strv_free_ char **lines = NULL;
const char *s;
@@ -575,16 +563,13 @@ static int client(struct context *c) {
ASSERT_OK(sd_bus_call_method(bus, "org.freedesktop.systemd.test", "/foo", "org.freedesktop.systemd.test", "Exit", &error, NULL, NULL));
sd_bus_flush(bus);
return 0;
}
int main(int argc, char *argv[]) {
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
_cleanup_(sd_future_unrefp) sd_future *f_server = NULL, *f_client = NULL;
struct context c = {};
pthread_t s;
void *p;
int r, q;
test_setup_logging(LOG_DEBUG);
@@ -593,21 +578,16 @@ int main(int argc, char *argv[]) {
ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM, 0, c.fds));
r = pthread_create(&s, NULL, server, &c);
if (r != 0)
return -r;
ASSERT_OK(sd_event_new(&e));
ASSERT_OK(sd_event_set_exit_on_idle(e, true));
r = client(&c);
ASSERT_OK(sd_fiber_new(e, "server", server, &c, /* destroy= */ NULL, &f_server));
ASSERT_OK(sd_fiber_new(e, "client", client, &c, /* destroy= */ NULL, &f_client));
q = pthread_join(s, &p);
if (q != 0)
return -q;
ASSERT_OK(sd_event_loop(e));
if (r < 0)
return r;
if (PTR_TO_INT(p) < 0)
return PTR_TO_INT(p);
ASSERT_OK(sd_future_result(f_server));
ASSERT_OK(sd_future_result(f_client));
free(c.something);
free(c.automatic_string_property);
+20 -13
View File
@@ -1,9 +1,9 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <pthread.h>
#include <unistd.h>
#include "sd-bus.h"
#include "sd-future.h"
#include "bus-dump.h"
#include "fd-util.h"
@@ -38,9 +38,9 @@ static bool gid_list_same(const gid_t *a, size_t n, const gid_t *b, size_t m) {
gid_list_contained(b, m, a, n);
}
static void* server(void *p) {
static int server(void *userdata) {
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
_cleanup_close_ int listen_fd = PTR_TO_INT(p), fd = -EBADF;
_cleanup_close_ int listen_fd = PTR_TO_INT(userdata), fd = -EBADF;
_cleanup_(sd_bus_creds_unrefp) sd_bus_creds *c = NULL;
_cleanup_free_ char *our_comm = NULL;
sd_id128_t id;
@@ -48,7 +48,7 @@ static void* server(void *p) {
ASSERT_OK(sd_id128_randomize(&id));
ASSERT_OK_ERRNO(fd = accept4(listen_fd, NULL, NULL, SOCK_CLOEXEC|SOCK_NONBLOCK));
ASSERT_OK(fd = sd_fiber_accept(listen_fd, NULL, NULL, SOCK_CLOEXEC|SOCK_NONBLOCK));
ASSERT_OK(sd_bus_new(&bus));
ASSERT_OK(sd_bus_set_fd(bus, fd, fd));
@@ -114,17 +114,18 @@ static void* server(void *p) {
}
}
return NULL;
return 0;
}
static void* client(void *p) {
static int client(void *userdata) {
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
const char *z;
ASSERT_OK(sd_bus_new(&bus));
ASSERT_OK(sd_bus_set_description(bus, "wuffwuff"));
ASSERT_OK(sd_bus_set_address(bus, p));
ASSERT_OK(sd_bus_set_address(bus, userdata));
ASSERT_OK(sd_bus_attach_event(bus, sd_fiber_get_event(), 0));
ASSERT_OK(sd_bus_start(bus));
ASSERT_OK(sd_bus_call_method(bus, "foo.foo", "/foo", "foo.foo", "Foo", NULL, &reply, "s", "foo"));
@@ -132,17 +133,18 @@ static void* client(void *p) {
ASSERT_OK(sd_bus_message_read(reply, "s", &z));
ASSERT_STREQ(z, "bar");
return NULL;
return 0;
}
TEST(description) {
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
_cleanup_(sd_future_unrefp) sd_future *f_server = NULL, *f_client = NULL;
_cleanup_free_ char *a = NULL;
_cleanup_close_ int fd = -EBADF;
union sockaddr_union sa = {
.un.sun_family = AF_UNIX,
};
socklen_t salen;
pthread_t s, c;
ASSERT_OK_ERRNO(fd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0));
ASSERT_OK_ERRNO(bind(fd, &sa.sa, offsetof(struct sockaddr_un, sun_path))); /* force auto-bind */
@@ -155,13 +157,18 @@ TEST(description) {
ASSERT_OK(asprintf(&a, "unix:abstract=%s", sa.un.sun_path + 1));
ASSERT_OK(-pthread_create(&s, NULL, server, INT_TO_PTR(fd)));
ASSERT_OK(sd_event_new(&e));
ASSERT_OK(sd_event_set_exit_on_idle(e, true));
ASSERT_OK(sd_fiber_new(e, "server", server, INT_TO_PTR(fd), /* destroy= */ NULL, &f_server));
TAKE_FD(fd);
ASSERT_OK(-pthread_create(&c, NULL, client, a));
ASSERT_OK(sd_fiber_new(e, "client", client, a, /* destroy= */ NULL, &f_client));
ASSERT_OK(-pthread_join(s, NULL));
ASSERT_OK(-pthread_join(c, NULL));
ASSERT_OK(sd_event_loop(e));
ASSERT_OK(sd_future_result(f_server));
ASSERT_OK(sd_future_result(f_client));
}
DEFINE_TEST_MAIN(LOG_INFO);
+23 -25
View File
@@ -1,10 +1,12 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <pthread.h>
#include <sys/socket.h>
#include "sd-bus.h"
#include "sd-event.h"
#include "sd-future.h"
#include "errno-util.h"
#include "log.h"
#include "memory-util.h"
#include "string-util.h"
@@ -20,7 +22,8 @@ struct context {
bool server_anonymous_auth;
};
static int _server(struct context *c) {
static int server(void *userdata) {
struct context *c = ASSERT_PTR(userdata);
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
sd_id128_t id;
bool quit = false;
@@ -29,6 +32,7 @@ static int _server(struct context *c) {
ASSERT_OK(sd_id128_randomize(&id));
ASSERT_OK(sd_bus_new(&bus));
ASSERT_OK(sd_bus_set_description(bus, "server"));
ASSERT_OK(sd_bus_set_fd(bus, c->fds[0], c->fds[0]));
ASSERT_OK(sd_bus_set_server(bus, 1, id));
ASSERT_OK(sd_bus_set_anonymous(bus, c->server_anonymous_auth));
@@ -74,17 +78,16 @@ static int _server(struct context *c) {
return 0;
}
static void* server(void *p) {
return INT_TO_PTR(_server(p));
}
static int client(struct context *c) {
static int client(void *userdata) {
struct context *c = ASSERT_PTR(userdata);
_cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL;
_cleanup_(sd_bus_unrefp) sd_bus *bus = NULL;
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
ASSERT_OK(sd_bus_new(&bus));
ASSERT_OK(sd_bus_set_description(bus, "client"));
ASSERT_OK(sd_bus_set_fd(bus, c->fds[1], c->fds[1]));
ASSERT_OK(sd_bus_attach_event(bus, sd_fiber_get_event(), 0));
ASSERT_OK(sd_bus_negotiate_fds(bus, c->client_negotiate_unix_fds));
ASSERT_OK(sd_bus_set_anonymous(bus, c->client_anonymous_auth));
ASSERT_OK(sd_bus_start(bus));
@@ -103,10 +106,10 @@ static int client(struct context *c) {
static int test_one(bool client_negotiate_unix_fds, bool server_negotiate_unix_fds,
bool client_anonymous_auth, bool server_anonymous_auth) {
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
_cleanup_(sd_future_unrefp) sd_future *f_server = NULL, *f_client = NULL;
struct context c;
pthread_t s;
void *p;
int r, q;
int r = 0;
zero(c);
@@ -117,23 +120,18 @@ static int test_one(bool client_negotiate_unix_fds, bool server_negotiate_unix_f
c.client_anonymous_auth = client_anonymous_auth;
c.server_anonymous_auth = server_anonymous_auth;
r = pthread_create(&s, NULL, server, &c);
if (r != 0)
return -r;
ASSERT_OK(sd_event_new(&e));
ASSERT_OK(sd_event_set_exit_on_idle(e, true));
r = client(&c);
ASSERT_OK(sd_fiber_new(e, "server", server, &c, /* destroy= */ NULL, &f_server));
ASSERT_OK(sd_fiber_new(e, "client", client, &c, /* destroy= */ NULL, &f_client));
q = pthread_join(s, &p);
if (q != 0)
return -q;
ASSERT_OK(sd_event_loop(e));
if (r < 0)
return r;
RET_GATHER(r, sd_future_result(f_client));
RET_GATHER(r, sd_future_result(f_server));
if (PTR_TO_INT(p) < 0)
return PTR_TO_INT(p);
return 0;
return r;
}
int main(int argc, char *argv[]) {
@@ -145,7 +143,7 @@ int main(int argc, char *argv[]) {
ASSERT_OK(test_one(false, false, false, false));
ASSERT_OK(test_one(true, true, true, true));
ASSERT_OK(test_one(true, true, false, true));
ASSERT_ERROR(test_one(true, true, true, false), EPERM);
ASSERT_ERROR(test_one(true, true, true, false), EACCES);
return EXIT_SUCCESS;
}
+58 -44
View File
@@ -1,10 +1,8 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <pthread.h>
#include <unistd.h>
#include "sd-bus.h"
#include "sd-event.h"
#include "sd-future.h"
#include "sd-id128.h"
#include "alloc-util.h"
@@ -44,33 +42,33 @@ static const sd_bus_vtable vtable[] = {
SD_BUS_VTABLE_END,
};
static void* thread_server(void *p) {
static int server(void *userdata) {
_cleanup_free_ char *suffixed = NULL, *suffixed_basename = NULL, *suffixed2 = NULL, *d = NULL;
_cleanup_close_ int fd = -EBADF;
union sockaddr_union u;
const char *path = p;
const char *path = ASSERT_PTR(userdata);
int r;
log_debug("Initializing server");
/* Let's play some games, by slowly creating the socket directory, and renaming it in the middle */
usleep_safe(100 * USEC_PER_MSEC);
ASSERT_OK(sd_fiber_sleep(100 * USEC_PER_MSEC));
ASSERT_OK(mkdir_parents(path, 0755));
usleep_safe(100 * USEC_PER_MSEC);
ASSERT_OK(sd_fiber_sleep(100 * USEC_PER_MSEC));
ASSERT_OK(path_extract_directory(path, &d));
ASSERT_OK(asprintf(&suffixed, "%s.%" PRIx64, d, random_u64()));
ASSERT_OK_ERRNO(rename(d, suffixed));
usleep_safe(100 * USEC_PER_MSEC);
ASSERT_OK(sd_fiber_sleep(100 * USEC_PER_MSEC));
ASSERT_OK(asprintf(&suffixed2, "%s.%" PRIx64, d, random_u64()));
ASSERT_OK_ERRNO(symlink(suffixed2, d));
usleep_safe(100 * USEC_PER_MSEC);
ASSERT_OK(sd_fiber_sleep(100 * USEC_PER_MSEC));
ASSERT_OK(path_extract_filename(suffixed, &suffixed_basename));
ASSERT_OK_ERRNO(symlink(suffixed_basename, suffixed2));
usleep_safe(100 * USEC_PER_MSEC);
ASSERT_OK(sd_fiber_sleep(100 * USEC_PER_MSEC));
socklen_t sa_len;
r = sockaddr_un_set_path(&u.un, path);
@@ -81,13 +79,13 @@ static void* thread_server(void *p) {
ASSERT_OK_ERRNO(fd);
ASSERT_OK_ERRNO(bind(fd, &u.sa, sa_len));
usleep_safe(100 * USEC_PER_MSEC);
ASSERT_OK(sd_fiber_sleep(100 * USEC_PER_MSEC));
ASSERT_OK_ERRNO(listen(fd, SOMAXCONN_DELUXE));
usleep_safe(100 * USEC_PER_MSEC);
ASSERT_OK(sd_fiber_sleep(100 * USEC_PER_MSEC));
ASSERT_OK(touch(path));
usleep_safe(100 * USEC_PER_MSEC);
ASSERT_OK(sd_fiber_sleep(100 * USEC_PER_MSEC));
log_debug("Initialized server");
@@ -101,8 +99,7 @@ static void* thread_server(void *p) {
ASSERT_OK(sd_event_new(&event));
bus_fd = accept4(fd, NULL, NULL, SOCK_NONBLOCK|SOCK_CLOEXEC);
ASSERT_OK_ERRNO(bus_fd);
ASSERT_OK(bus_fd = sd_fiber_accept(fd, NULL, NULL, SOCK_NONBLOCK|SOCK_CLOEXEC));
log_debug("Accepted server connection");
@@ -129,13 +126,13 @@ static void* thread_server(void *p) {
log_debug("Server done");
return NULL;
return 0;
}
static void* thread_client1(void *p) {
static int client1(void *userdata) {
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
const char *path = p, *t;
const char *path = ASSERT_PTR(userdata), *t;
log_debug("Initializing client1");
@@ -151,59 +148,65 @@ static void* thread_client1(void *p) {
log_debug("Client1 done");
return NULL;
}
static int client2_callback(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) {
ASSERT_OK_ZERO(sd_bus_message_is_method_error(m, NULL));
ASSERT_OK(sd_event_exit(sd_bus_get_event(sd_bus_message_get_bus(m)), 0));
return 0;
}
static void* thread_client2(void *p) {
static int client2(void *userdata) {
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
_cleanup_(sd_event_unrefp) sd_event *event = NULL;
const char *path = p, *t;
const char *path = ASSERT_PTR(userdata), *t;
log_debug("Initializing client2");
ASSERT_OK(sd_event_new(&event));
ASSERT_OK(sd_bus_new(&bus));
ASSERT_OK(sd_bus_set_description(bus, "client2"));
t = strjoina("unix:path=", path);
ASSERT_OK(sd_bus_set_address(bus, t));
ASSERT_OK(sd_bus_set_watch_bind(bus, true));
ASSERT_OK(sd_bus_attach_event(bus, event, 0));
ASSERT_OK(sd_bus_attach_event(bus, sd_fiber_get_event(), 0));
ASSERT_OK(sd_bus_start(bus));
ASSERT_OK(sd_bus_call_method_async(bus, NULL, "foo.bar", "/foo", "foo.TestInterface", "Foobar", client2_callback, NULL, NULL));
ASSERT_OK(sd_event_loop(event));
_cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
ASSERT_OK(sd_bus_call_method(bus, "foo.bar", "/foo", "foo.TestInterface", "Foobar", NULL, &m, NULL));
ASSERT_OK_ZERO(sd_bus_message_is_method_error(m, NULL));
log_debug("Client2 done");
return NULL;
return 0;
}
static void request_exit(const char *path) {
typedef struct RequestExitArgs {
const char *path;
sd_future *client1;
sd_future *client2;
} RequestExitArgs;
static int request_exit(void *userdata) {
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
RequestExitArgs *args = ASSERT_PTR(userdata);
const char *t;
/* Wait for all client fibers to complete before requesting exit */
ASSERT_OK(sd_fiber_await(args->client1));
ASSERT_OK(sd_fiber_await(args->client2));
ASSERT_OK(sd_bus_new(&bus));
t = strjoina("unix:path=", path);
t = strjoina("unix:path=", args->path);
ASSERT_OK(sd_bus_set_address(bus, t));
ASSERT_OK(sd_bus_set_watch_bind(bus, true));
ASSERT_OK(sd_bus_set_description(bus, "request-exit"));
ASSERT_OK(sd_bus_start(bus));
ASSERT_OK(sd_bus_call_method(bus, "foo.bar", "/foo", "foo.TestInterface", "Exit", NULL, NULL, NULL));
return 0;
}
int main(int argc, char *argv[]) {
_cleanup_(rm_rf_physical_and_freep) char *d = NULL;
pthread_t server, client1, client2;
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
_cleanup_(sd_future_unrefp) sd_future *f_server = NULL, *f_client1 = NULL, *f_client2 = NULL, *f_exit = NULL;
char *path;
test_setup_logging(LOG_DEBUG);
@@ -214,16 +217,27 @@ int main(int argc, char *argv[]) {
path = strjoina(d, "/this/is/a/socket");
ASSERT_OK(-pthread_create(&server, NULL, thread_server, path));
ASSERT_OK(-pthread_create(&client1, NULL, thread_client1, path));
ASSERT_OK(-pthread_create(&client2, NULL, thread_client2, path));
ASSERT_OK(sd_event_new(&e));
ASSERT_OK(sd_event_set_exit_on_idle(e, true));
ASSERT_OK(-pthread_join(client1, NULL));
ASSERT_OK(-pthread_join(client2, NULL));
ASSERT_OK(sd_fiber_new(e, "server", server, path, /* destroy= */ NULL, &f_server));
request_exit(path);
ASSERT_OK(sd_fiber_new(e, "client-1", client1, path, /* destroy= */ NULL, &f_client1));
ASSERT_OK(sd_fiber_new(e, "client-2", client2, path, /* destroy= */ NULL, &f_client2));
ASSERT_OK(-pthread_join(server, NULL));
RequestExitArgs args = {
.path = path,
.client1 = f_client1,
.client2 = f_client2,
};
ASSERT_OK(sd_fiber_new(e, "request-exit", request_exit, &args, /* destroy= */ NULL, &f_exit));
return 0;
ASSERT_OK(sd_event_loop(e));
ASSERT_OK(sd_future_result(f_client1));
ASSERT_OK(sd_future_result(f_client2));
ASSERT_OK(sd_future_result(f_exit));
ASSERT_OK(sd_future_result(f_server));
return EXIT_SUCCESS;
}
+6
View File
@@ -127,3 +127,9 @@ typedef struct sd_resolve sd_resolve;
typedef struct sd_resolve_query sd_resolve_query;
typedef struct sd_hwdb sd_hwdb;
typedef struct sd_future sd_future;
typedef int (*sd_future_func_t)(sd_future *f);
typedef int (*sd_fiber_func_t)(void *userdata);
typedef _sd_destroy_t sd_fiber_destroy_t;
+308
View File
@@ -0,0 +1,308 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "sd-event.h"
#include "sd-future.h"
#include "alloc-util.h"
#include "errno-util.h"
#include "event-future.h"
#include "event-util.h"
#include "fd-util.h"
typedef struct IoFuture {
sd_event_source *source;
} IoFuture;
static void* io_future_alloc(void) {
return new0(IoFuture, 1);
}
static void io_future_free(sd_future *f) {
IoFuture *iof = ASSERT_PTR(sd_future_get_private(ASSERT_PTR(f)));
sd_event_source_unref(iof->source);
free(iof);
}
static int io_future_cancel(sd_future *f) {
IoFuture *iof = ASSERT_PTR(sd_future_get_private(ASSERT_PTR(f)));
int r = 0;
RET_GATHER(r, sd_event_source_set_enabled(iof->source, SD_EVENT_OFF));
RET_GATHER(r, sd_future_resolve(f, -ECANCELED));
return r;
}
static int io_future_set_priority(sd_future *f, int64_t priority) {
IoFuture *iof = ASSERT_PTR(sd_future_get_private(ASSERT_PTR(f)));
return sd_event_source_set_priority(iof->source, priority);
}
static const sd_future_ops io_future_ops = {
.size = sizeof(sd_future_ops),
.alloc = io_future_alloc,
.free = io_future_free,
.cancel = io_future_cancel,
.set_priority = io_future_set_priority,
};
static int io_handler(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
sd_future *f = ASSERT_PTR(userdata);
/* Resolve with the revents mask on success (matching io_uring poll_add's CQE convention) so
* callers can read it directly off the future result. EPOLLERR is the one exception: surface
* the actual socket error via SO_ERROR so callers like sd_fiber_connect() can return -errno
* directly without re-querying. */
if (FLAGS_SET(revents, EPOLLERR)) {
int error = 0;
socklen_t len = sizeof(error);
int r = RET_NERRNO(getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &len));
if (r == -ENOTSOCK)
return sd_future_resolve(f, (int) revents);
if (r >= 0 && error != 0)
return sd_future_resolve(f, -error);
if (r >= 0)
/* EPOLLERR was reported but SO_ERROR returned no pending error (e.g.
* already consumed elsewhere). Surface the revents mask so the caller
* still sees the error condition rather than mistaking it for success. */
return sd_future_resolve(f, (int) revents);
/* On any other getsockopt() error fall through and resolve the future with that
* error so the waiting fiber wakes up rather than hanging forever. */
return sd_future_resolve(f, r);
}
return sd_future_resolve(f, (int) revents);
}
int future_new_io(sd_event *e, int fd, uint32_t events, sd_future **ret) {
int r;
assert(e);
assert(fd >= 0);
assert(ret);
if (IN_SET(sd_event_get_state(e), SD_EVENT_EXITING, SD_EVENT_FINISHED))
return -ECANCELED;
_cleanup_(sd_future_unrefp) sd_future *f = NULL;
r = sd_future_new(&io_future_ops, &f);
if (r < 0)
return r;
IoFuture *iof = sd_future_get_private(f);
/* Duplicate fd to avoid EEXIST from epoll when adding the same fd multiple times */
_cleanup_close_ int fd_copy = fcntl(fd, F_DUPFD_CLOEXEC, 3);
if (fd_copy < 0)
return -errno;
r = sd_event_add_io(e, &iof->source, fd_copy, events, io_handler, f);
if (r < 0)
return r;
r = sd_event_source_set_io_fd_own(iof->source, true);
if (r < 0)
return r;
TAKE_FD(fd_copy);
r = sd_event_source_set_enabled(iof->source, SD_EVENT_ONESHOT);
if (r < 0)
return r;
if (sd_fiber_is_running()) {
int64_t priority;
r = sd_fiber_get_priority(&priority);
if (r < 0)
return r;
r = sd_event_source_set_priority(iof->source, priority);
if (r < 0)
return r;
}
*ret = TAKE_PTR(f);
return 0;
}
typedef struct TimeFuture {
sd_event_source *source;
/* Result the future resolves with on natural expiry (vs. cancellation). 0 for normal sleep,
* non-zero (e.g. -ETIMEDOUT) lets a fiber waiting on this future resume with that error. */
int result;
} TimeFuture;
static void* time_future_alloc(void) {
return new0(TimeFuture, 1);
}
static void time_future_free(sd_future *f) {
TimeFuture *tf = ASSERT_PTR(sd_future_get_private(ASSERT_PTR(f)));
sd_event_source_unref(tf->source);
free(tf);
}
static int time_future_cancel(sd_future *f) {
TimeFuture *tf = ASSERT_PTR(sd_future_get_private(ASSERT_PTR(f)));
int r;
r = sd_event_source_set_enabled(tf->source, SD_EVENT_OFF);
RET_GATHER(r, sd_future_resolve(f, -ECANCELED));
return r;
}
static int time_future_set_priority(sd_future *f, int64_t priority) {
TimeFuture *tf = ASSERT_PTR(sd_future_get_private(ASSERT_PTR(f)));
return sd_event_source_set_priority(tf->source, priority);
}
static const sd_future_ops time_future_ops = {
.size = sizeof(sd_future_ops),
.alloc = time_future_alloc,
.free = time_future_free,
.cancel = time_future_cancel,
.set_priority = time_future_set_priority,
};
static int time_handler(sd_event_source *s, usec_t usec, void *userdata) {
sd_future *f = ASSERT_PTR(userdata);
TimeFuture *tf = ASSERT_PTR(sd_future_get_private(f));
return sd_future_resolve(f, tf->result);
}
typedef int (*event_add_time_func)(
sd_event *e,
sd_event_source **ret,
clockid_t clock,
uint64_t usec,
uint64_t accuracy,
sd_event_time_handler_t callback,
void *userdata);
static int future_new_time_internal(
event_add_time_func add_time,
sd_event *e,
clockid_t clock,
uint64_t usec,
uint64_t accuracy,
int result,
sd_future **ret) {
int r;
assert(add_time);
assert(e);
assert(ret);
if (IN_SET(sd_event_get_state(e), SD_EVENT_EXITING, SD_EVENT_FINISHED))
return -ECANCELED;
_cleanup_(sd_future_unrefp) sd_future *f = NULL;
r = sd_future_new(&time_future_ops, &f);
if (r < 0)
return r;
TimeFuture *tf = sd_future_get_private(f);
tf->result = result;
r = add_time(e, &tf->source, clock, usec, accuracy, time_handler, f);
if (r < 0)
return r;
if (sd_fiber_is_running()) {
int64_t priority;
r = sd_fiber_get_priority(&priority);
if (r < 0)
return r;
r = sd_event_source_set_priority(tf->source, priority);
if (r < 0)
return r;
}
*ret = TAKE_PTR(f);
return 0;
}
int future_new_time(sd_event *e, clockid_t clock, uint64_t usec, uint64_t accuracy, int result, sd_future **ret) {
return future_new_time_internal(sd_event_add_time, e, clock, usec, accuracy, result, ret);
}
int future_new_time_relative(sd_event *e, clockid_t clock, uint64_t usec, uint64_t accuracy, int result, sd_future **ret) {
return future_new_time_internal(sd_event_add_time_relative, e, clock, usec, accuracy, result, ret);
}
int event_run_suspend(sd_event *e, uint64_t timeout) {
sd_event *outer = sd_fiber_get_event();
int r;
assert(e);
assert(sd_fiber_is_running());
assert(outer);
assert(e != outer);
/* Make sure that none of the preparation callbacks ends up freeing the event source under our feet */
PROTECT_EVENT(e);
r = sd_event_prepare(e);
if (r < 0)
return r;
if (r == 0) {
r = sd_event_wait(e, 0);
if (r < 0)
return r;
}
if (r > 0) {
r = sd_event_dispatch(e);
if (r < 0)
return r;
return 1;
}
if (timeout == 0)
return 0;
int fd = sd_event_get_fd(e);
if (fd < 0)
return fd;
_cleanup_(sd_future_cancel_wait_unrefp) sd_future *io = NULL;
r = future_new_io(outer, fd, EPOLLIN, &io);
if (r < 0)
return r;
_cleanup_(sd_future_cancel_wait_unrefp) sd_future *timer = NULL;
if (timeout != USEC_INFINITY) {
r = future_new_time_relative(
outer,
CLOCK_MONOTONIC,
timeout,
/* accuracy= */ 1,
/* result= */ 0,
&timer);
if (r < 0)
return r;
}
r = sd_fiber_suspend();
if (r < 0)
return r;
r = sd_event_prepare(e);
if (r == 0)
r = sd_event_wait(e, 0);
if (r > 0) {
r = sd_event_dispatch(e);
if (r < 0)
return r;
return 1;
}
return r;
}
+10
View File
@@ -0,0 +1,10 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include "sd-forward.h"
int future_new_io(sd_event *e, int fd, uint32_t events, sd_future **ret);
int future_new_time(sd_event *e, clockid_t clock, uint64_t usec, uint64_t accuracy, int result, sd_future **ret);
int future_new_time_relative(sd_event *e, clockid_t clock, uint64_t usec, uint64_t accuracy, int result, sd_future **ret);
int event_run_suspend(sd_event *e, uint64_t timeout);
+3
View File
@@ -5,6 +5,9 @@
#include "sd-forward.h"
#define PROTECT_EVENT(e) \
_unused_ _cleanup_(sd_event_unrefp) sd_event *_ref = sd_event_ref(e);
extern const struct hash_ops event_source_hash_ops;
int event_reset_time(
+10 -3
View File
@@ -10,12 +10,15 @@
#include "sd-daemon.h"
#include "sd-event.h"
#include "sd-future.h"
#include "sd-id128.h"
#include "sd-messages.h"
#include "alloc-util.h"
#include "errno-util.h"
#include "event-future.h"
#include "event-source.h"
#include "event-util.h"
#include "fd-util.h"
#include "format-util.h"
#include "glyph-util.h"
@@ -474,9 +477,6 @@ _public_ sd_event* sd_event_unref(sd_event *e) {
return event_free(e);
}
#define PROTECT_EVENT(e) \
_unused_ _cleanup_(sd_event_unrefp) sd_event *_ref = sd_event_ref(e);
_public_ sd_event_source* sd_event_source_disable_unref(sd_event_source *s) {
int r;
@@ -4932,6 +4932,13 @@ _public_ int sd_event_run(sd_event *e, uint64_t timeout) {
assert_return(e->state != SD_EVENT_FINISHED, -ESTALE);
assert_return(e->state == SD_EVENT_INITIAL, -EBUSY);
/* When running on a fiber, delegate to the suspending implementation. Note that the
* profile_delays accounting below is intentionally skipped on that path: the suspending variant
* drives the event loop via sd_event_prepare()/sd_event_wait()/sd_event_dispatch() itself, which
* are the same primitives profile_delays tracks when called directly. */
if (sd_fiber_is_running())
return event_run_suspend(e, timeout);
if (e->profile_delays && e->last_run_usec != 0) {
usec_t this_run;
unsigned l;
+358
View File
@@ -0,0 +1,358 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <fcntl.h>
#include <unistd.h>
#include "sd-event.h"
#include "sd-future.h"
#include "fd-util.h"
#include "tests.h"
#include "time-util.h"
static int timer_callback(sd_event_source *s, uint64_t usec, void *userdata) {
int *count = ASSERT_PTR(userdata);
int r;
(*count)++;
r = sd_event_source_set_time_relative(s, 5 * USEC_PER_MSEC);
if (r < 0)
return r;
if (sd_fiber_is_running() && *count >= 3)
return sd_event_exit(sd_event_source_get_event(s), 0);
return 0;
}
static int event_run_fiber_func(void *userdata) {
_cleanup_(sd_event_unrefp) sd_event *inner = NULL;
_cleanup_(sd_event_source_unrefp) sd_event_source *inner_timer = NULL;
int r;
/* Create inner event loop from within the fiber */
r = sd_event_new(&inner);
if (r < 0)
return r;
/* Add a timer to the inner event loop that fires every 5ms */
r = sd_event_add_time_relative(inner, &inner_timer, CLOCK_MONOTONIC,
5 * USEC_PER_MSEC, 0, timer_callback,
userdata);
if (r < 0)
return r;
r = sd_event_source_set_enabled(inner_timer, SD_EVENT_ON);
if (r < 0)
return r;
return sd_event_loop(inner);
}
TEST(sd_event_loop_fiber) {
/* Create outer event loop for the fiber scheduler */
_cleanup_(sd_event_unrefp) sd_event *outer = NULL;
ASSERT_OK(sd_event_new(&outer));
ASSERT_OK(sd_event_set_exit_on_idle(outer, true));
/* Add a timer to the outer event loop that fires every 5ms */
_cleanup_(sd_event_source_unrefp) sd_event_source *outer_timer = NULL;
int outer_timer_count = 0;
ASSERT_OK(sd_event_add_time_relative(outer, &outer_timer, CLOCK_MONOTONIC,
5 * USEC_PER_MSEC, 0, timer_callback,
&outer_timer_count));
/* Create a fiber that will create and run the inner event loop */
_cleanup_(sd_future_unrefp) sd_future *f = NULL;
int inner_timer_count = 0;
ASSERT_OK(sd_fiber_new(outer, "event-runner", event_run_fiber_func, &inner_timer_count, /* destroy= */ NULL, &f));
/* Run the outer event loop */
ASSERT_OK(sd_event_loop(outer));
/* Fiber should have completed successfully */
ASSERT_OK(sd_future_result(f));
/* Both timers should have fired at least once */
ASSERT_EQ(inner_timer_count, 3);
ASSERT_GT(outer_timer_count, 0);
}
static int event_run_fiber_timeout_func(void *userdata) {
_cleanup_(sd_event_unrefp) sd_event *inner = NULL;
int r;
/* Create inner event loop from within the fiber */
r = sd_event_new(&inner);
if (r < 0)
return r;
/* Run with a short timeout - should timeout since there are no events */
return sd_event_run(inner, 10 * USEC_PER_MSEC);
}
TEST(sd_event_run_fiber_timeout) {
/* Create outer event loop for the fiber scheduler */
_cleanup_(sd_event_unrefp) sd_event *outer = NULL;
ASSERT_OK(sd_event_new(&outer));
ASSERT_OK(sd_event_set_exit_on_idle(outer, true));
/* Create a fiber that will run sd_event_run() with timeout */
_cleanup_(sd_future_unrefp) sd_future *f = NULL;
ASSERT_OK(sd_fiber_new(outer, "event-timeout", event_run_fiber_timeout_func, NULL, /* destroy= */ NULL, &f));
/* Run the outer event loop */
ASSERT_OK(sd_event_loop(outer));
/* Fiber should have completed successfully (timeout returns 0) */
ASSERT_OK_ZERO(sd_future_result(f));
}
/* Test: sd_event_run() with zero timeout returns immediately */
static int sd_event_run_zero_timeout_fiber(void *userdata) {
_cleanup_(sd_event_unrefp) sd_event *inner = NULL;
int r;
r = sd_event_new(&inner);
if (r < 0)
return r;
/* With zero timeout on an empty event loop, should return 0 immediately */
r = sd_event_run(inner, 0);
if (r != 0)
return r < 0 ? r : -EIO;
return 0;
}
TEST(sd_event_run_zero_timeout) {
_cleanup_(sd_event_unrefp) sd_event *outer = NULL;
ASSERT_OK(sd_event_new(&outer));
ASSERT_OK(sd_event_set_exit_on_idle(outer, true));
_cleanup_(sd_future_unrefp) sd_future *f = NULL;
ASSERT_OK(sd_fiber_new(outer, "run-suspend-zero", sd_event_run_zero_timeout_fiber, NULL, /* destroy= */ NULL, &f));
ASSERT_OK(sd_event_loop(outer));
ASSERT_OK_ZERO(sd_future_result(f));
}
/* Test: sd_event_run() dispatches immediately pending IO */
static int io_callback(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
int *counter = ASSERT_PTR(userdata);
char buf[64];
(*counter)++;
/* Drain the fd */
(void) read(fd, buf, sizeof(buf));
return sd_event_exit(sd_event_source_get_event(s), 0);
}
static int sd_event_run_immediate_fiber(void *userdata) {
int *pipefd = ASSERT_PTR(userdata);
_cleanup_(sd_event_unrefp) sd_event *inner = NULL;
_cleanup_(sd_event_source_unrefp) sd_event_source *source = NULL;
int counter = 0, r;
r = sd_event_new(&inner);
if (r < 0)
return r;
/* Add IO source watching the read end of the pipe */
r = sd_event_add_io(inner, &source, pipefd[0], EPOLLIN, io_callback, &counter);
if (r < 0)
return r;
/* Data is already available on the pipe (written before fiber started), so
* sd_event_run() should dispatch immediately without suspending */
r = sd_event_run(inner, USEC_INFINITY);
if (r < 0)
return r;
/* The IO callback should have fired */
if (counter != 1)
return -EIO;
return 0;
}
TEST(sd_event_run_immediate) {
_cleanup_(sd_event_unrefp) sd_event *outer = NULL;
ASSERT_OK(sd_event_new(&outer));
ASSERT_OK(sd_event_set_exit_on_idle(outer, true));
_cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC | O_NONBLOCK));
/* Write data before starting the fiber so it's immediately available */
ASSERT_OK_EQ_ERRNO(write(pipefd[1], "X", 1), 1);
_cleanup_(sd_future_unrefp) sd_future *f = NULL;
ASSERT_OK(sd_fiber_new(outer, "run-suspend-immediate", sd_event_run_immediate_fiber, pipefd, /* destroy= */ NULL, &f));
ASSERT_OK(sd_event_loop(outer));
ASSERT_OK_ZERO(sd_future_result(f));
}
/* Test: sd_event_run() with IO arriving during suspension */
static int sd_event_run_io_fiber(void *userdata) {
int *pipefd = ASSERT_PTR(userdata);
_cleanup_(sd_event_unrefp) sd_event *inner = NULL;
_cleanup_(sd_event_source_unrefp) sd_event_source *source = NULL;
int counter = 0, r;
r = sd_event_new(&inner);
if (r < 0)
return r;
r = sd_event_add_io(inner, &source, pipefd[0], EPOLLIN, io_callback, &counter);
if (r < 0)
return r;
/* No data available yet, so this will suspend the fiber until IO arrives */
r = sd_event_run(inner, USEC_INFINITY);
if (r < 0)
return r;
if (counter != 1)
return -EIO;
return 0;
}
TEST(sd_event_run_io) {
_cleanup_(sd_event_unrefp) sd_event *outer = NULL;
ASSERT_OK(sd_event_new(&outer));
ASSERT_OK(sd_event_set_exit_on_idle(outer, true));
_cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC | O_NONBLOCK));
_cleanup_(sd_future_unrefp) sd_future *f = NULL;
ASSERT_OK(sd_fiber_new(outer, "run-suspend-io", sd_event_run_io_fiber, pipefd, /* destroy= */ NULL, &f));
/* First iteration: fiber runs, adds IO source, suspends because no data */
ASSERT_OK_POSITIVE(sd_event_run(outer, 0));
/* Write data to the pipe to wake the inner event loop */
ASSERT_OK_EQ_ERRNO(write(pipefd[1], "Y", 1), 1);
/* Complete: fiber resumes, dispatches IO, finishes */
ASSERT_OK(sd_event_loop(outer));
ASSERT_OK_ZERO(sd_future_result(f));
}
/* Test: event_run called in a loop keeps event loop state consistent.
* This is a regression test for a bug where error paths after sd_event_prepare()
* could leave the inner event loop stuck in SD_EVENT_ARMED state. */
static int sd_event_run_loop_fiber(void *userdata) {
int *pipefd = ASSERT_PTR(userdata);
_cleanup_(sd_event_unrefp) sd_event *inner = NULL;
_cleanup_(sd_event_source_unrefp) sd_event_source *source = NULL;
int counter = 0, r;
r = sd_event_new(&inner);
if (r < 0)
return r;
r = sd_event_add_io(inner, &source, pipefd[0], EPOLLIN, io_callback, &counter);
if (r < 0)
return r;
/* Call sd_event_run() multiple times with short timeouts.
* Each call should leave the inner event loop in a clean state for the next call. */
for (int i = 0; i < 5; i++) {
r = sd_event_run(inner, 10 * USEC_PER_MSEC);
if (r < 0)
return r;
if (r > 0)
break;
}
/* After multiple timeouts, the event loop should still be usable.
* Write data and do one more run to verify. */
if (counter == 0) {
/* Data wasn't written yet, do a final run with longer timeout */
r = sd_event_run(inner, USEC_INFINITY);
if (r < 0)
return r;
}
if (counter != 1)
return -EIO;
return 0;
}
TEST(sd_event_run_loop) {
_cleanup_(sd_event_unrefp) sd_event *outer = NULL;
ASSERT_OK(sd_event_new(&outer));
ASSERT_OK(sd_event_set_exit_on_idle(outer, true));
_cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC | O_NONBLOCK));
_cleanup_(sd_future_unrefp) sd_future *f = NULL;
ASSERT_OK(sd_fiber_new(outer, "run-suspend-loop", sd_event_run_loop_fiber, pipefd, /* destroy= */ NULL, &f));
/* Let the fiber run through a few timeout iterations */
for (int i = 0; i < 10; i++)
ASSERT_OK(sd_event_run(outer, 50 * USEC_PER_MSEC));
/* Write data to unblock the fiber */
ASSERT_OK_EQ_ERRNO(write(pipefd[1], "Z", 1), 1);
ASSERT_OK(sd_event_loop(outer));
ASSERT_OK_ZERO(sd_future_result(f));
}
/* Test: sd_event_run() with an inner timer that fires during suspension */
static int inner_timer_handler(sd_event_source *s, uint64_t usec, void *userdata) {
int *counter = ASSERT_PTR(userdata);
(*counter)++;
return sd_event_exit(sd_event_source_get_event(s), 0);
}
static int sd_event_run_timer_fiber(void *userdata) {
_cleanup_(sd_event_unrefp) sd_event *inner = NULL;
_cleanup_(sd_event_source_unrefp) sd_event_source *source = NULL;
int counter = 0, r;
r = sd_event_new(&inner);
if (r < 0)
return r;
/* Add a timer that fires after 10ms */
r = sd_event_add_time_relative(inner, &source, CLOCK_MONOTONIC,
10 * USEC_PER_MSEC, 0, inner_timer_handler,
&counter);
if (r < 0)
return r;
/* Should suspend, then resume when the timer fires */
r = sd_event_run(inner, USEC_INFINITY);
if (r < 0)
return r;
if (counter != 1)
return -EIO;
return 0;
}
TEST(sd_event_run_timer) {
_cleanup_(sd_event_unrefp) sd_event *outer = NULL;
ASSERT_OK(sd_event_new(&outer));
ASSERT_OK(sd_event_set_exit_on_idle(outer, true));
_cleanup_(sd_future_unrefp) sd_future *f = NULL;
ASSERT_OK(sd_fiber_new(outer, "run-suspend-timer", sd_event_run_timer_fiber, NULL, /* destroy= */ NULL, &f));
ASSERT_OK(sd_event_loop(outer));
ASSERT_OK_ZERO(sd_future_result(f));
}
DEFINE_TEST_MAIN(LOG_DEBUG);
+471
View File
@@ -0,0 +1,471 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <poll.h>
#include <sys/epoll.h> /* IWYU pragma: keep */
#include <sys/socket.h>
#include <sys/uio.h>
#include <time.h>
#include <unistd.h>
#include "sd-event.h"
#include "sd-future.h"
#include "alloc-util.h"
#include "errno-util.h"
#include "event-future.h"
#include "fd-util.h"
#include "io-util.h"
#include "time-util.h"
typedef ssize_t (*FiberIOFunc)(int fd, void *args);
static ssize_t fiber_io_operation(
int fd,
uint32_t events,
FiberIOFunc func,
void *args) {
_cleanup_(nonblock_resetp) int reset_fd = -EBADF;
int r;
assert(fd >= 0);
assert(func);
if (!sd_fiber_is_running())
return func(fd, args);
sd_event *e = sd_fiber_get_event();
assert(e);
r = fd_nonblock(fd, true);
if (r < 0)
return r;
if (r > 0)
reset_fd = fd;
ssize_t n = func(fd, args);
if (n >= 0 || !ERRNO_IS_NEG_TRANSIENT(n))
return n;
_cleanup_(sd_future_cancel_wait_unrefp) sd_future *io = NULL;
r = future_new_io(e, fd, events, &io);
if (r < 0)
return r;
r = sd_fiber_suspend();
if (r < 0)
return r;
return func(fd, args);
}
typedef struct ReadArgs {
void *buf;
size_t count;
} ReadArgs;
static ssize_t read_callback(int fd, void *args) {
ReadArgs *a = ASSERT_PTR(args);
ssize_t n;
n = read(fd, a->buf, a->count);
return n >= 0 ? n : -errno;
}
ssize_t sd_fiber_read(int fd, void *buf, size_t count) {
assert_return(fd >= 0, -EBADF);
assert_return(buf || count == 0, -EINVAL);
return fiber_io_operation(fd, EPOLLIN, read_callback, &(ReadArgs) {
.buf = buf,
.count = count,
});
}
typedef struct WriteArgs {
const void *buf;
size_t count;
} WriteArgs;
static ssize_t write_callback(int fd, void *args) {
WriteArgs *a = ASSERT_PTR(args);
ssize_t n;
n = write(fd, a->buf, a->count);
return n >= 0 ? n : -errno;
}
ssize_t sd_fiber_write(int fd, const void *buf, size_t count) {
assert_return(fd >= 0, -EBADF);
assert_return(buf || count == 0, -EINVAL);
return fiber_io_operation(fd, EPOLLOUT, write_callback, &(WriteArgs) {
.buf = buf,
.count = count,
});
}
typedef struct ReadvArgs {
const struct iovec *iov;
int iovcnt;
} ReadvArgs;
static ssize_t readv_callback(int fd, void *args) {
ReadvArgs *a = ASSERT_PTR(args);
ssize_t n;
n = readv(fd, a->iov, a->iovcnt);
return n >= 0 ? n : -errno;
}
ssize_t sd_fiber_readv(int fd, const struct iovec *iov, int iovcnt) {
assert_return(fd >= 0, -EBADF);
assert_return(iov || iovcnt == 0, -EINVAL);
return fiber_io_operation(fd, EPOLLIN, readv_callback, &(ReadvArgs) {
.iov = iov,
.iovcnt = iovcnt,
});
}
typedef struct WritevArgs {
const struct iovec *iov;
int iovcnt;
} WritevArgs;
static ssize_t writev_callback(int fd, void *args) {
WritevArgs *a = ASSERT_PTR(args);
ssize_t n;
n = writev(fd, a->iov, a->iovcnt);
return n >= 0 ? n : -errno;
}
ssize_t sd_fiber_writev(int fd, const struct iovec *iov, int iovcnt) {
assert_return(fd >= 0, -EBADF);
assert_return(iov || iovcnt == 0, -EINVAL);
return fiber_io_operation(fd, EPOLLOUT, writev_callback, &(WritevArgs) {
.iov = iov,
.iovcnt = iovcnt,
});
}
typedef struct RecvArgs {
void *buf;
size_t len;
int flags;
} RecvArgs;
static ssize_t recv_callback(int fd, void *args) {
RecvArgs *a = ASSERT_PTR(args);
ssize_t n;
n = recv(fd, a->buf, a->len, a->flags);
return n >= 0 ? n : -errno;
}
ssize_t sd_fiber_recv(int sockfd, void *buf, size_t len, int flags) {
assert_return(sockfd >= 0, -EBADF);
assert_return(buf || len == 0, -EINVAL);
return fiber_io_operation(sockfd, EPOLLIN, recv_callback, &(RecvArgs) {
.buf = buf,
.len = len,
.flags = flags,
});
}
typedef struct SendArgs {
const void *buf;
size_t len;
int flags;
} SendArgs;
static ssize_t send_callback(int fd, void *args) {
SendArgs *a = ASSERT_PTR(args);
ssize_t n;
n = send(fd, a->buf, a->len, a->flags);
return n >= 0 ? n : -errno;
}
ssize_t sd_fiber_send(int sockfd, const void *buf, size_t len, int flags) {
assert_return(sockfd >= 0, -EBADF);
assert_return(buf || len == 0, -EINVAL);
return fiber_io_operation(sockfd, EPOLLOUT, send_callback, &(SendArgs) {
.buf = buf,
.len = len,
.flags = flags,
});
}
int sd_fiber_connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen) {
_cleanup_(nonblock_resetp) int reset_fd = -EBADF;
int r;
assert_return(sockfd >= 0, -EBADF);
assert_return(addr, -EINVAL);
if (!sd_fiber_is_running())
return RET_NERRNO(connect(sockfd, addr, addrlen));
sd_event *e = sd_fiber_get_event();
assert(e);
r = fd_nonblock(sockfd, true);
if (r < 0)
return r;
if (r > 0)
reset_fd = sockfd;
r = RET_NERRNO(connect(sockfd, addr, addrlen));
if (r != -EINPROGRESS)
return r;
_cleanup_(sd_future_cancel_wait_unrefp) sd_future *io = NULL;
r = future_new_io(e, sockfd, EPOLLOUT, &io);
if (r < 0)
return r;
/* future_new_io resolves with the revents mask on success; translate any positive value
* (e.g. POLLOUT) back to the connect(2) success status. */
r = sd_fiber_suspend();
return r > 0 ? 0 : r;
}
typedef struct RecvmsgArgs {
struct msghdr *msg;
int flags;
} RecvmsgArgs;
static ssize_t recvmsg_callback(int fd, void *args) {
RecvmsgArgs *a = ASSERT_PTR(args);
ssize_t n;
n = recvmsg(fd, a->msg, a->flags);
return n >= 0 ? n : -errno;
}
ssize_t sd_fiber_recvmsg(int sockfd, struct msghdr *msg, int flags) {
assert_return(sockfd >= 0, -EBADF);
assert_return(msg, -EINVAL);
return fiber_io_operation(sockfd, EPOLLIN, recvmsg_callback, &(RecvmsgArgs) {
.msg = msg,
.flags = flags,
});
}
typedef struct SendmsgArgs {
const struct msghdr *msg;
int flags;
} SendmsgArgs;
static ssize_t sendmsg_callback(int fd, void *args) {
SendmsgArgs *a = ASSERT_PTR(args);
ssize_t n;
n = sendmsg(fd, a->msg, a->flags);
return n >= 0 ? n : -errno;
}
ssize_t sd_fiber_sendmsg(int sockfd, const struct msghdr *msg, int flags) {
assert_return(sockfd >= 0, -EBADF);
assert_return(msg, -EINVAL);
return fiber_io_operation(sockfd, EPOLLOUT, sendmsg_callback, &(SendmsgArgs) {
.msg = msg,
.flags = flags,
});
}
static ssize_t recvfrom_callback(int fd, void *args) {
RecvmsgArgs *a = ASSERT_PTR(args);
ssize_t n;
n = recvmsg(fd, a->msg, a->flags);
return n >= 0 ? n : -errno;
}
ssize_t sd_fiber_recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen) {
ssize_t n;
assert_return(sockfd >= 0, -EBADF);
assert_return(buf || len == 0, -EINVAL);
assert_return(!src_addr || addrlen, -EINVAL);
/* io_uring has no direct recvfrom prep helper, so emulate via recvmsg with a single-iovec
* msghdr. The kernel updates msg_namelen in place; we copy it back to *addrlen below. */
struct iovec iov = { .iov_base = buf, .iov_len = len };
struct msghdr msg = {
.msg_name = src_addr,
.msg_namelen = src_addr ? *addrlen : 0,
.msg_iov = &iov,
.msg_iovlen = 1,
};
n = fiber_io_operation(sockfd, EPOLLIN, recvfrom_callback, &(RecvmsgArgs) {
.msg = &msg,
.flags = flags,
});
if (n < 0)
return n;
if (addrlen)
*addrlen = msg.msg_namelen;
return n;
}
static ssize_t sendto_callback(int fd, void *args) {
SendmsgArgs *a = ASSERT_PTR(args);
ssize_t n;
n = sendmsg(fd, a->msg, a->flags);
return n >= 0 ? n : -errno;
}
ssize_t sd_fiber_sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen) {
assert_return(sockfd >= 0, -EBADF);
assert_return(buf || len == 0, -EINVAL);
struct iovec iov = { .iov_base = (void *) buf, .iov_len = len };
struct msghdr msg = {
.msg_name = (void *) dest_addr,
.msg_namelen = dest_addr ? addrlen : 0,
.msg_iov = &iov,
.msg_iovlen = 1,
};
return fiber_io_operation(sockfd, EPOLLOUT, sendto_callback, &(SendmsgArgs) {
.msg = &msg,
.flags = flags,
});
}
typedef struct AcceptArgs {
struct sockaddr *addr;
socklen_t *addrlen;
int flags;
} AcceptArgs;
static ssize_t accept_callback(int fd, void *args) {
AcceptArgs *a = ASSERT_PTR(args);
return RET_NERRNO(accept4(fd, a->addr, a->addrlen, a->flags));
}
int sd_fiber_accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags) {
assert_return(sockfd >= 0, -EBADF);
return fiber_io_operation(sockfd, EPOLLIN, accept_callback, &(AcceptArgs) {
.addr = addr,
.addrlen = addrlen,
.flags = flags,
});
}
int sd_fiber_ppoll(struct pollfd *fds, size_t n_fds, const struct timespec *timeout, const sigset_t *sigmask) {
int r;
assert_return(fds || n_fds == 0, -EINVAL);
if (!sd_fiber_is_running())
return RET_NERRNO(ppoll(fds, n_fds, timeout, sigmask));
/* When on a fiber signals are handled via sd-event hence we should never mess around with the
* signal mask when running on a fiber. */
assert_return(!sigmask, -EOPNOTSUPP);
sd_event *e = sd_fiber_get_event();
assert(e);
/* No fds to wait on and no timeout means there's nothing that could ever wake the fiber up,
* since unlike raw ppoll() we cannot use signal delivery as a wakeup. Signals received while
* the fiber is suspended are handled by sd-event via signalfd, in which case the signal handler
* is expected to cancel the fiber via sd_future_cancel() if a wakeup is desired. */
if (n_fds == 0 && !timeout)
return -EINVAL;
bool zero_timeout = timeout && timeout->tv_sec == 0 && timeout->tv_nsec == 0;
/* Try polling with zero timeout first to see if any are immediately ready. */
r = RET_NERRNO(ppoll(fds, n_fds, &(const struct timespec) {}, /* sigmask= */ NULL));
if (zero_timeout || r != 0) /* Either error or some fds are ready */
return r;
sd_future **futures = NULL;
CLEANUP_ARRAY(futures, n_fds, sd_future_cancel_wait_unref_array);
futures = new0(sd_future*, n_fds);
if (!futures)
return -ENOMEM;
/* Set up I/O event sources for all valid fds. POLL* and EPOLL* share their bit values (see
* EPOLL_POLL_COMMON_MASK in io-util.h), so we can pass the user-supplied event mask through
* to either backend without translation. */
size_t n_io_futures = 0;
for (size_t i = 0; i < n_fds; i++) {
if (fds[i].fd < 0)
continue;
uint32_t events = fds[i].events & EPOLL_POLL_COMMON_MASK;
if (events == 0)
continue;
r = future_new_io(e, fds[i].fd, events, &futures[i]);
if (r < 0)
return r;
n_io_futures++;
}
/* A timeout that overflows usec_t saturates to USEC_INFINITY in timespec_load(); treat that
* like "no timeout" (matches sd_fiber_sleep(USEC_INFINITY)) rather than letting
* sd_event_add_time_relative() reject it with -EOVERFLOW — standard ppoll() would just
* wait a very long time. */
usec_t usec = timeout ? timespec_load(timeout) : USEC_INFINITY;
/* If every fd was skipped (negative or empty event mask) and we'd have no timer, there's
* nothing that could ever wake the fiber up — same situation as n_fds == 0 && !timeout,
* just not detectable upfront. Refuse rather than suspend forever. */
if (n_io_futures == 0 && usec == USEC_INFINITY)
return -EINVAL;
_cleanup_(sd_future_cancel_wait_unrefp) sd_future *timer = NULL;
if (usec != USEC_INFINITY) {
r = future_new_time_relative(
e,
CLOCK_MONOTONIC,
usec,
/* accuracy= */ 1,
/* result= */ 0,
&timer);
if (r < 0)
return r;
}
r = sd_fiber_suspend();
if (r < 0 && r != -ETIME)
return r;
/* Always sweep fds with a non-blocking ppoll(): the timer and an fd readiness can resolve in
* the same event-loop tick (or the fd can become ready between the timer firing and us being
* scheduled), and ppoll() semantics give events precedence over the timeout in that case. */
int n = RET_NERRNO(ppoll(fds, n_fds, &(const struct timespec) {}, /* sigmask= */ NULL));
if (n != 0)
return n;
/* No fds ready: distinguish our own timer from an external -ETIME. */
if (timer && sd_future_state(timer) == SD_FUTURE_RESOLVED)
return 0;
/* An IO future resolved with a revents mask (r > 0) but the readiness was already consumed
* by the time we swept — report 0 rather than leaking the bitmask as a (bogus) ppoll fd
* count to the caller. */
if (r > 0)
return 0;
return r;
}
+827
View File
@@ -0,0 +1,827 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <pthread.h>
#include <setjmp.h>
#include <sys/mman.h>
#include <sys/resource.h>
#include <sys/uio.h>
#include <threads.h>
#include <ucontext.h>
#include <unistd.h>
#if HAVE_VALGRIND_VALGRIND_H
#include <valgrind/valgrind.h>
#endif
#include "sd-event.h"
#include "sd-future.h"
#include "alloc-util.h"
#include "architecture.h"
#include "errno-util.h"
#include "event-future.h"
#include "fiber-ops.h"
#include "log-context.h"
#include "log.h"
#include "memory-util.h"
#include "pthread-util.h"
#include "time-util.h"
#if HAS_FEATURE_ADDRESS_SANITIZER
#include <sanitizer/common_interface_defs.h>
#endif
/* glibc's _FORTIFY_SOURCE wraps siglongjmp() with __longjmp_chk, which asserts that the target SP is below
* the current SP. That assumption is incompatible with fiber switching, where the target SP lives on a
* separately-mmap'd stack and can be at any address relative to the caller. The fortify redirect happens
* in <setjmp.h>'s declaration of siglongjmp; we sidestep it by declaring our own alias that links
* directly to the unchecked "siglongjmp" symbol. musl doesn't fortify setjmp.h, so the alias is a plain
* synonym there. */
_noreturn_ extern void siglongjmp_unchecked(sigjmp_buf env, int val) __asm__("siglongjmp");
static thread_local Fiber *current_fiber = NULL;
typedef enum FiberState {
FIBER_STATE_INITIAL,
FIBER_STATE_READY,
FIBER_STATE_SUSPENDED,
FIBER_STATE_CANCELLED,
FIBER_STATE_COMPLETED,
_FIBER_STATE_MAX,
_FIBER_STATE_INVALID = -EINVAL,
} FiberState;
typedef struct Fiber {
struct iovec stack;
sigjmp_buf context; /* Where to jump to when entering or resuming the fiber. */
sigjmp_buf resume_context; /* Where to jump back to when the fiber yields or completes. */
/* Caller's stack range, recorded by fiber_run() on each entry so the fiber's siglongjmp back
* out (in fiber_swap() or the trampoline's terminate path) can hand AddressSanitizer the
* destination stack info. With ucontext this comes for free via uc_link/uc_stack; sigjmp_buf
* is opaque and doesn't carry it. */
struct iovec resume_stack;
FiberState state;
int result; /* Either resume error code or final return value */
sd_future *floating; /* Self-ref held while the fiber is floating; dropped on resolve. */
sd_event *event;
sd_event_source *defer_event_source;
sd_event_source *exit_event_source;
char *name;
int64_t priority;
sd_fiber_func_t func;
void *userdata;
sd_fiber_destroy_t destroy;
/* Storage for the swap performed in fiber_run(): while the fiber is suspended these hold the
* fiber's own log state; while it is running they hold the caller's log state. The active state
* always lives in the thread-locals in log.c / log-context.c. */
LIST_HEAD(LogContext, log_context);
size_t log_context_num_fields;
const char *log_prefix;
#if HAVE_VALGRIND_VALGRIND_H
unsigned stack_id;
#endif
} Fiber;
static Fiber* fiber_get_current(void) {
return current_fiber;
}
static void fiber_set_current(Fiber *f) {
current_fiber = f;
}
static int fiber_allocate_stack(size_t size, void **ret) {
void *stack = NULL;
int r;
/* The effective stack size is one page less than the given size, because we have to use
* one page as the guard page for the stack. */
assert(size > 0 && size % page_size() == 0);
assert(ret);
stack = mmap(/* addr= */ NULL, size,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK,
/* fd= */ -EBADF, /* offset= */ 0);
if (stack == MAP_FAILED)
return -errno;
/* Place the guard page where stack overflow will hit it: the high end on architectures
* where the stack grows up (PA-RISC), the low end everywhere else. fiber_stack_usable()
* mirrors this with the inverse offset. */
void *guard = STACK_GROWS_UP ? (uint8_t*) stack + size - page_size() : stack;
/* Prefer MADV_GUARD_INSTALL (Linux 6.13+): unlike mprotect(PROT_NONE) it doesn't split
* the VMA, so guard installation skips the mmap-lock contention and per-guard VMA cost.
* Fall back to mprotect on older kernels, which return EINVAL for unknown advice.
* FIXME: delete when baseline above 6.13. */
r = RET_NERRNO(madvise(guard, page_size(), MADV_GUARD_INSTALL));
if (r == -EINVAL)
r = RET_NERRNO(mprotect(guard, page_size(), PROT_NONE));
if (r < 0) {
(void) munmap(stack, size);
return r;
}
*ret = TAKE_PTR(stack);
return 0;
}
/* Usable stack range of a fiber: the full mmap region minus the guard page. Single source of
* truth for the layout assumed by fiber_allocate_stack(); every consumer (ucontext ss_sp,
* ASAN handoff iovecs, Valgrind stack registration) goes through here.
*
* iov_base is the lowest usable byte regardless of growth direction — that matches POSIX's
* definition of stack_t.ss_sp, so libc's makecontext() handles the direction for us. Only the
* guard page placement (and hence iov_base's offset within the mapping) varies. */
static struct iovec fiber_stack_usable(const struct iovec *stack) {
assert(stack);
assert(stack->iov_len > page_size());
return (struct iovec) {
.iov_base = STACK_GROWS_UP ? stack->iov_base : (uint8_t*) stack->iov_base + page_size(),
.iov_len = stack->iov_len - page_size(),
};
}
static inline void start_switch_stack(void **fake_stack_save, const struct iovec *dest) {
#if HAS_FEATURE_ADDRESS_SANITIZER
__sanitizer_start_switch_fiber(fake_stack_save,
dest ? dest->iov_base : NULL,
dest ? dest->iov_len : 0);
#endif
}
static inline void finish_switch_stack(void *fake_stack_save) {
#if HAS_FEATURE_ADDRESS_SANITIZER
__sanitizer_finish_switch_fiber(fake_stack_save, NULL, NULL);
#endif
}
/* Refresh f->resume_stack from whoever is currently the running fiber, so the next siglongjmp() out
* of f (in the trampoline or fiber_swap()) can hand the right destination stack to ASAN. Must be
* called before fiber_set_current(f) — relies on fiber_get_current() returning the caller. */
static void fiber_set_resume_stack(Fiber *f, Fiber *resume) {
assert(f);
if (resume)
f->resume_stack = fiber_stack_usable(&resume->stack);
else
f->resume_stack = (struct iovec) {};
}
_noreturn_ static void fiber_entry_point(void) {
Fiber *f = ASSERT_PTR(fiber_get_current());
void *fake_stack_save = NULL;
assert(f->func);
assert(IN_SET(f->state, FIBER_STATE_INITIAL, FIBER_STATE_READY, FIBER_STATE_CANCELLED));
finish_switch_stack(NULL);
/* Capture our resumable point on the fiber's stack, then bounce back to whoever last set
* f->resume_context. On bootstrap that's fiber_bootstrap(); on every subsequent yield it's
* the most recent fiber_run(). sigsetjmp(buf, 0) skips the signal-mask save: switching is
* thread-shared with respect to signal masks. */
if (sigsetjmp(f->context, /* savemask= */ 0) == 0) {
start_switch_stack(&fake_stack_save, &f->resume_stack);
siglongjmp_unchecked(f->resume_context, /* val= */ 1);
}
/* Re-entered for real via fiber_run()'s siglongjmp(f->context). */
finish_switch_stack(fake_stack_save);
/* Block scope so the cleanups attached to LOG_SET_PREFIX / LOG_CONTEXT_PUSH_KEY_VALUE fire
* before the siglongjmp below — siglongjmp skips _cleanup_ attributes, so we have to make
* sure the scope ends via a normal control-flow path first. */
{
LOG_SET_PREFIX(f->name);
LOG_CONTEXT_PUSH_KEY_VALUE("FIBER=", f->name);
f->result = f->state == FIBER_STATE_CANCELLED ? -ECANCELED : f->func(f->userdata);
f->state = FIBER_STATE_COMPLETED;
}
/* Pass NULL fake_stack_save to discard the fiber's fake stack since the fiber is done. */
start_switch_stack(NULL, &f->resume_stack);
/* Bounce back to whichever fiber_run() call most recently entered us. resume_context is
* per-fiber so nested fiber_run() — e.g. a bus method dispatched as a fiber handler while
* sd_event_loop() itself runs in a fiber — is safe. */
siglongjmp_unchecked(f->resume_context, 1);
assert_not_reached();
}
static int fiber_init(Fiber *f) {
ucontext_t old_uc, uc;
void *fake_stack_save = NULL;
assert(f);
if (getcontext(&uc) < 0)
return -errno;
struct iovec fiber_stack = fiber_stack_usable(&f->stack);
uc.uc_link = NULL; /* Unused: trampoline siglongjmps out instead of returning. */
uc.uc_stack.ss_sp = fiber_stack.iov_base;
uc.uc_stack.ss_size = fiber_stack.iov_len;
uc.uc_stack.ss_flags = 0;
Fiber *prev = fiber_get_current();
fiber_set_current(f);
makecontext(&uc, fiber_entry_point, /* argc= */ 0);
fiber_set_resume_stack(f, prev);
if (sigsetjmp(f->resume_context, /* savemask= */ 0) == 0) {
start_switch_stack(&fake_stack_save, &fiber_stack);
if (swapcontext(&old_uc, &uc) < 0) {
finish_switch_stack(fake_stack_save);
fiber_set_current(prev);
return -errno;
}
assert_not_reached(); /* Trampoline siglongjmps back; swapcontext doesn't return. */
}
finish_switch_stack(fake_stack_save);
fiber_set_current(prev);
return 0;
}
/* Swap the thread-local log prefix and log context with the values stashed in f. While the fiber is
* suspended, f holds the fiber's own log state; while it's running, f holds the caller's log state. The
* swap is its own inverse, so the same call drives both directions. */
static void fiber_swap_log_state(Fiber *f) {
assert(f);
log_prefix_swap(&f->log_prefix);
log_context_swap(&f->log_context, &f->log_context_num_fields);
}
static void reset_current_fiber(void) {
/* Restore the caller's log state stashed in the running fiber (if any) before clearing
* current_fiber. Without this, the child of a fork() that happened mid-fiber would inherit the
* fiber's log prefix / context list in its thread-locals even though no fiber is running. */
Fiber *f = fiber_get_current();
if (f) {
fiber_swap_log_state(f);
fiber_ops_set(NULL);
}
fiber_set_current(NULL);
}
static sd_event_source* fiber_current_event_source(Fiber *f) {
assert(f);
assert(f->state != FIBER_STATE_COMPLETED);
assert(f->event);
return sd_event_get_state(f->event) == SD_EVENT_EXITING ? f->exit_event_source : f->defer_event_source;
}
static int atfork_ret;
static void install_atfork(void) {
/* __register_atfork() either returns 0 or -ENOMEM, in its glibc implementation. Since it's
* only half-documented (glibc doesn't document it but LSB does — though only superficially)
* we'll check for errors only in the most generic fashion possible. */
atfork_ret = pthread_atfork(/* prepare= */ NULL, /* parent= */ NULL, reset_current_fiber);
if (atfork_ret != 0)
log_debug_errno(atfork_ret, "pthread_atfork() failed: %m");
}
static void fiber_resolve(sd_future *f) {
Fiber *fiber = ASSERT_PTR(sd_future_get_private(ASSERT_PTR(f)));
fiber->defer_event_source = sd_event_source_disable_unref(fiber->defer_event_source);
fiber->exit_event_source = sd_event_source_disable_unref(fiber->exit_event_source);
/* The floating self-ref (if any) is potentially the last ref keeping the fiber alive — moving it
* into a local _cleanup_ slot ensures sd_future_resolve() runs callbacks and waiters while f is
* still valid; the local's cleanup drops the ref afterwards, at which point no further f->...
* access can happen. */
_unused_ _cleanup_(sd_future_unrefp) sd_future *floating = TAKE_PTR(fiber->floating);
sd_future_resolve(f, fiber->result);
}
static const FiberOps fiber_ops = {
.ppoll = sd_fiber_ppoll,
.read = sd_fiber_read,
.write = sd_fiber_write,
.timeout = sd_fiber_timeout,
.cancel_wait_unref = sd_future_cancel_wait_unref,
};
static void fiber_enter(Fiber *fiber, Fiber *prev, void **fake_stack_save) {
fiber_set_current(fiber);
fiber_swap_log_state(fiber);
if (!prev)
fiber_ops_set(&fiber_ops);
struct iovec fiber_stack = fiber_stack_usable(&fiber->stack);
start_switch_stack(fake_stack_save, &fiber_stack);
fiber_set_resume_stack(fiber, prev);
}
static void fiber_leave(Fiber *fiber, Fiber *prev, void *fake_stack_save) {
finish_switch_stack(fake_stack_save);
if (!prev)
fiber_ops_set(NULL);
fiber_swap_log_state(fiber);
fiber_set_current(prev);
}
static int fiber_run(sd_future *f) {
Fiber *fiber = ASSERT_PTR(sd_future_get_private(ASSERT_PTR(f)));
int r;
if (fiber->state == FIBER_STATE_COMPLETED)
return -ESTALE;
assert(IN_SET(fiber->state, FIBER_STATE_INITIAL, FIBER_STATE_READY, FIBER_STATE_CANCELLED));
static pthread_once_t atfork_once = PTHREAD_ONCE_INIT;
r = pthread_once(&atfork_once, install_atfork);
if (r != 0)
return -r;
if (atfork_ret != 0)
return -atfork_ret;
LOG_SET_PREFIX(fiber->name);
LOG_CONTEXT_PUSH_KEY_VALUE("FIBER=", fiber->name);
log_debug("Scheduling fiber");
/* Save the previously-current fiber (if any) so we can restore it when this fiber yields or
* completes. This matters when fiber_run() is invoked from within another fiber (e.g. an
* sd-event dispatch that happens to be running inside a fiber context itself): the
* LOG_SET_PREFIX/LOG_CONTEXT_PUSH above attached to whichever fiber was current at that moment,
* and their scope-level cleanup must see the same fiber_get_current() when it runs to detach
* them from the correct list. */
Fiber *prev = fiber_get_current();
void *fake_stack_save = NULL;
fiber_enter(fiber, prev, &fake_stack_save);
/* This is where we start executing the fiber. Once it yields, we continue here as if nothing
* happened. resume_context captures this point; the fiber siglongjmps back to it. */
if (sigsetjmp(fiber->resume_context, 0) == 0)
siglongjmp_unchecked(fiber->context, 1);
fiber_leave(fiber, prev, fake_stack_save);
switch (fiber->state) {
case FIBER_STATE_COMPLETED:
if (fiber->result < 0 && fiber->result != -ECANCELED)
log_debug_errno(fiber->result, "Fiber failed with error: %m");
else
log_debug("Fiber finished executing");
fiber_resolve(f);
break;
case FIBER_STATE_CANCELLED:
case FIBER_STATE_READY:
log_debug("Fiber yielded execution");
r = sd_event_source_set_enabled(fiber_current_event_source(fiber), SD_EVENT_ONESHOT);
if (r < 0)
return r;
break;
case FIBER_STATE_SUSPENDED:
log_debug("Fiber suspended execution");
/* Fiber is waiting for something - don't re-queue it */
break;
default:
assert_not_reached();
}
return 0;
}
static int fiber_cancel(sd_future *f) {
Fiber *fiber = ASSERT_PTR(sd_future_get_private(ASSERT_PTR(f)));
int r;
assert(fiber != fiber_get_current());
if (IN_SET(fiber->state, FIBER_STATE_COMPLETED, FIBER_STATE_CANCELLED))
return 0;
if (fiber->state == FIBER_STATE_INITIAL) {
/* The fiber's stack was allocated but never entered, so there are no scope-level cleanups
* waiting to run. Skip the dispatch round-trip that would just have fiber_entry_point()
* fall straight through with -ECANCELED, and settle the future right here — mirroring the
* FIBER_STATE_COMPLETED branch of fiber_run(). */
fiber->result = -ECANCELED;
fiber->state = FIBER_STATE_COMPLETED;
fiber_resolve(f);
return 1;
}
/* Once we cancel a fiber, we want to immediately resume it with -ECANCELED. */
r = sd_event_source_set_enabled(fiber_current_event_source(fiber), SD_EVENT_ONESHOT);
if (r < 0)
return r;
fiber->state = FIBER_STATE_CANCELLED;
return 1;
}
static int fiber_on_defer(sd_event_source *s, void *userdata) {
sd_future *f = ASSERT_PTR(userdata);
return fiber_run(f);
}
static int fiber_on_exit(sd_event_source *s, void *userdata) {
sd_future *f = ASSERT_PTR(userdata);
Fiber *fiber = ASSERT_PTR(sd_future_get_private(f));
int r;
/* The fiber may already have completed via the regular defer path before sd_event_exit()
* fires the exit source; in that case there's nothing left to drive and we'd otherwise
* trip fiber_run()'s -ESTALE return, which sd_event would log spuriously and disable the
* source for. */
if (fiber->state == FIBER_STATE_COMPLETED)
return 0;
/* If fiber_cancel() returned 1 the fiber was just marked cancelled and its deferred/exit event
* source was re-armed; we let the event loop dispatch that source on the next iteration so it goes
* through the normal fiber_on_defer/fiber_on_exit path rather than running it recursively here. */
r = fiber_cancel(f);
if (r != 0)
return r;
return fiber_run(f);
}
static void* fiber_alloc(void) {
return new0(Fiber, 1);
}
static void fiber_free(sd_future *f) {
Fiber *fiber = ASSERT_PTR(sd_future_get_private(f));
/* To make sure all memory is deallocated, the fiber has to have completed by the time we free it to
* make sure its stack has finished unwinding (which will invoke the registered cleanup functions).
* As this function may get called when not running on a fiber ourselves, we can't guarantee here
* that we can run the fiber to completion ourselves, so we insist that this happens before we get
* here. To ensure fibers are cleaned up before exiting the event loop, exit handlers are added for
* fibers created outside of existing fibers. For fibers created within running fibers, unwinding the
* outer fiber should take care of cleaning up any created child fibers (for example using
* sd_future_cancel_wait_unref()).
*
* FIBER_STATE_INITIAL is also accepted: the stack was allocated but never entered, so there are no
* registered cleanups to run. This covers the partial-construction failure path in sd_fiber_new()
* as well as fibers that are unrefed before the event loop ever dispatches them. */
assert(IN_SET(fiber->state, FIBER_STATE_INITIAL, FIBER_STATE_COMPLETED));
if (fiber->destroy)
fiber->destroy(fiber->userdata);
#if HAVE_VALGRIND_VALGRIND_H
if (fiber->stack.iov_base)
VALGRIND_STACK_DEREGISTER(fiber->stack_id);
#endif
if (fiber->stack.iov_base)
(void) munmap(fiber->stack.iov_base, fiber->stack.iov_len);
sd_event_source_disable_unref(fiber->defer_event_source);
sd_event_source_disable_unref(fiber->exit_event_source);
sd_event_unref(fiber->event);
free(fiber->name);
free(fiber);
}
sd_future* sd_fiber_get_current(void) {
Fiber *f = fiber_get_current();
if (!f)
return NULL;
return sd_event_source_get_userdata(fiber_current_event_source(f));
}
int sd_fiber_is_running(void) {
return !!fiber_get_current();
}
sd_event* sd_fiber_get_event(void) {
Fiber *f = fiber_get_current();
assert_return(f, NULL);
return f->event;
}
int sd_fiber_get_priority(int64_t *ret) {
Fiber *f = fiber_get_current();
assert_return(ret, -EINVAL);
assert_return(f, -ESRCH);
*ret = f->priority;
return 0;
}
static int fiber_swap(FiberState state) {
Fiber *f = ASSERT_PTR(fiber_get_current());
f->state = state;
void *fake_stack_save = NULL;
if (sigsetjmp(f->context, 0) == 0) {
start_switch_stack(&fake_stack_save, &f->resume_stack);
siglongjmp_unchecked(f->resume_context, 1);
}
finish_switch_stack(fake_stack_save);
/* When we get here, we've been resumed. */
if (f->state == FIBER_STATE_CANCELLED)
return -ECANCELED;
/* sd_fiber_resume() stashes the resumer's value (an async wakeup error from a deadline
* timer, an io_uring CQE result, etc.) into f->result for us to surface here. Consume it
* unconditionally so it doesn't pollute subsequent suspends or the fiber's eventual return
* value — both negative errors and positive payloads (byte counts, accepted fds, revents
* masks) are valid resume values. */
return TAKE_GENERIC(f->result, int, 0);
}
int sd_fiber_yield(void) {
assert_return(fiber_get_current(), -ESRCH);
return fiber_swap(FIBER_STATE_READY);
}
int sd_fiber_suspend(void) {
assert_return(fiber_get_current(), -ESRCH);
return fiber_swap(FIBER_STATE_SUSPENDED);
}
static int fiber_set_priority(sd_future *f, int64_t priority) {
Fiber *fiber = ASSERT_PTR(sd_future_get_private(ASSERT_PTR(f)));
int r = 0;
if (fiber->defer_event_source)
RET_GATHER(r, sd_event_source_set_priority(fiber->defer_event_source, priority));
if (fiber->exit_event_source)
RET_GATHER(r, sd_event_source_set_priority(fiber->exit_event_source, priority));
if (r >= 0)
fiber->priority = priority;
return r;
}
static const sd_future_ops fiber_future_ops;
int sd_fiber_resume(sd_future *f, int result) {
assert_return(f, -EINVAL);
assert_return(sd_future_get_ops(f) == &fiber_future_ops, -EINVAL);
Fiber *fiber = ASSERT_PTR(sd_future_get_private(f));
if (fiber->state != FIBER_STATE_SUSPENDED)
return 0;
/* Stash the result so fiber_swap() returns it from sd_fiber_suspend(). */
fiber->result = result;
fiber->state = FIBER_STATE_READY;
return sd_event_source_set_enabled(fiber_current_event_source(fiber), SD_EVENT_ONESHOT);
}
/* The fiber_future ops pass the Fiber pointer through as the future's private state. The fiber resolves
* its own future once it finishes running, so fiber_cancel() intentionally does not resolve. */
static const sd_future_ops fiber_future_ops = {
.size = sizeof(sd_future_ops),
.alloc = fiber_alloc,
.free = fiber_free,
.cancel = fiber_cancel,
.set_priority = fiber_set_priority,
};
int sd_fiber_new(sd_event *e, const char *name, sd_fiber_func_t func, void *userdata, sd_fiber_destroy_t destroy, sd_future **ret) {
int r;
assert_return(e, -EINVAL);
assert_return(name, -EINVAL);
assert_return(func, -EINVAL);
if (IN_SET(sd_event_get_state(e), SD_EVENT_EXITING, SD_EVENT_FINISHED))
return -ECANCELED;
_cleanup_(sd_future_unrefp) sd_future *f = NULL;
r = sd_future_new(&fiber_future_ops, &f);
if (r < 0)
return r;
Fiber *fiber = ASSERT_PTR(sd_future_get_private(f));
struct rlimit rl = { RLIM_INFINITY, RLIM_INFINITY };
if (getrlimit(RLIMIT_STACK, &rl) < 0)
log_debug_errno(errno, "Reading RLIMIT_STACK failed, ignoring: %m");
if (rl.rlim_cur == RLIM_INFINITY)
rl.rlim_cur = 8U * U64_MB; /* Same as the default thread stack size */
/* Reserve room for the guard page so the usable region stays above PTHREAD_STACK_MIN, which
* is what libc/pthread routines (e.g. TLS setup on musl) assume. */
size_t stack_len = ROUND_UP(rl.rlim_cur, page_size());
if (stack_len < (size_t) PTHREAD_STACK_MIN + page_size())
stack_len = ROUND_UP((size_t) PTHREAD_STACK_MIN + page_size(), page_size());
*fiber = (Fiber) {
.stack.iov_len = stack_len,
.state = FIBER_STATE_INITIAL,
.name = strdup(name),
.func = func,
.userdata = userdata,
.event = sd_event_ref(e),
};
if (!fiber->name)
return -ENOMEM;
r = fiber_allocate_stack(fiber->stack.iov_len, &fiber->stack.iov_base);
if (r < 0)
return r;
#if HAVE_VALGRIND_VALGRIND_H
/* Register the usable stack range (above the guard page) before fiber_bootstrap() so the
* trampoline's first sigsetjmp doesn't trip Valgrind's stack-tracking heuristics. */
struct iovec usable = fiber_stack_usable(&fiber->stack);
fiber->stack_id = VALGRIND_STACK_REGISTER(
usable.iov_base,
(uint8_t*) usable.iov_base + usable.iov_len);
#endif
r = fiber_init(fiber);
if (r < 0)
return r;
/* Execution of the fiber is driven by two event sources, one deferred, one exit. The exit event
* source kicks in when sd_event_exit() is called, as from that point onwards only exit event
* sources will be dispatched. */
r = sd_event_add_defer(e, &fiber->defer_event_source, fiber_on_defer, f);
if (r < 0)
return r;
r = sd_event_source_set_description(fiber->defer_event_source, fiber->name);
if (r < 0)
return r;
r = sd_event_add_exit(e, &fiber->exit_event_source, fiber_on_exit, f);
if (r < 0)
return r;
r = sd_event_source_set_description(fiber->exit_event_source, fiber->name);
if (r < 0)
return r;
/* If we're on a fiber, we'll rely on the parent fiber to cancel this fiber if the event loop is
* exiting. Otherwise, we'll trigger cancellation of this fiber via the exit event source. Why cancel
* via the exit event source? We can only run the fiber while the event loop is active, so we need to
* make sure all fibers finish running before the event loop is finished, which an exit event source
* allows us to do. */
r = sd_event_source_set_enabled(fiber->exit_event_source, sd_fiber_is_running() ? SD_EVENT_OFF : SD_EVENT_ONESHOT);
if (r < 0)
return r;
/* Stays in FIBER_STATE_INITIAL until the event loop first dispatches it via fiber_run(). */
if (ret)
*ret = TAKE_PTR(f);
else {
/* Fire-and-forget: the fiber is guaranteed to resolve (via completion, cancellation, or
* the event loop exit handler), so making the future floating cleans it up. */
r = sd_fiber_set_floating(f, true);
if (r < 0)
return r;
}
/* We only take ownership of the given userdata pointer on success so assign the destroy callback
* at the very end so we don't clean up the userdata pointer on failure. */
fiber->destroy = destroy;
return 0;
}
int sd_fiber_set_floating(sd_future *f, int b) {
assert_return(f, -EINVAL);
assert_return(sd_future_get_ops(f) == &fiber_future_ops, -EINVAL);
Fiber *fiber = ASSERT_PTR(sd_future_get_private(f));
if (!!fiber->floating == !!b)
return 0;
/* The floating self-ref keeps the future alive until the fiber resolves; fiber_run() drops it
* in the COMPLETED branch. Only valid for fiber futures because fibers uniquely guarantee
* resolution (via completion, cancellation, or the event loop exit handler). */
if (b)
fiber->floating = sd_future_ref(f);
else
fiber->floating = sd_future_unref(fiber->floating);
return 0;
}
int sd_fiber_get_floating(sd_future *f) {
assert_return(f, -EINVAL);
assert_return(sd_future_get_ops(f) == &fiber_future_ops, -EINVAL);
Fiber *fiber = ASSERT_PTR(sd_future_get_private(f));
return !!fiber->floating;
}
int sd_fiber_sleep(uint64_t usec) {
Fiber *f = fiber_get_current();
int r;
if (!f)
return usleep_safe(usec);
if (usec == 0)
return sd_fiber_yield();
/* Match usleep_safe(USEC_INFINITY): suspend indefinitely. Passing USEC_INFINITY to
* sd_event_add_time_relative() would overflow into -EOVERFLOW. */
if (usec == USEC_INFINITY)
return sd_fiber_suspend();
assert(f->event);
_cleanup_(sd_future_cancel_wait_unrefp) sd_future *timer = NULL;
r = future_new_time_relative(
f->event,
CLOCK_MONOTONIC,
usec,
/* accuracy= */ 1,
/* result= */ 0,
&timer);
if (r < 0)
return r;
return sd_fiber_suspend();
}
int sd_fiber_await(sd_future *target) {
sd_future *f = sd_fiber_get_current();
int r;
assert_return(f, -ESRCH);
assert_return(target, -EINVAL);
assert_return(target != f, -EDEADLK);
Fiber *fiber = ASSERT_PTR(sd_future_get_private(f));
if (sd_future_state(target) == SD_FUTURE_RESOLVED)
return sd_future_result(target);
/* Note that we do allow waiting for other fibers when the event loop is exiting, since waiting for
* other fibers does not require adding new event sources to the event loop. */
if (sd_event_get_state(fiber->event) == SD_EVENT_FINISHED)
return -ECANCELED;
_cleanup_(sd_future_cancel_wait_unrefp) sd_future *wait = NULL;
r = sd_future_new_wait(target, &wait);
if (r < 0)
return r;
return sd_fiber_suspend();
}
sd_future* sd_fiber_timeout(uint64_t timeout) {
Fiber *fiber = fiber_get_current();
int r;
assert_return(fiber, NULL);
if (timeout == USEC_INFINITY)
return NULL;
sd_future *timer;
r = future_new_time_relative(
fiber->event,
CLOCK_MONOTONIC,
timeout,
/* accuracy= */ 1,
/* result= */ -ETIME,
&timer);
if (r < 0)
return NULL; /* On allocation failure no timer is armed and the scope becomes a no-op.
* Errors here are rare; if the caller cares they can compare to NULL. */
return timer;
}
+263
View File
@@ -0,0 +1,263 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "sd-future.h"
#include "alloc-util.h"
#include "errno-util.h"
#include "log.h"
#include "macro.h"
#include "set.h"
struct sd_future {
unsigned n_ref;
int state;
int result;
Set *waiters;
sd_future_func_t callback;
void *userdata;
const sd_future_ops *ops;
/* Opaque per-future state owned by the future implementation (the code that called
* sd_future_new()). The ops vtable above receives this pointer in its callbacks, and
* external code can fetch it via sd_future_get_private(). */
void *private;
};
static int fiber_resume_trampoline(sd_future *f) {
/* The future's result is what the fiber should resume with. Impls choose the value at
* resolution time — e.g. a deadline timer resolves with -ETIME, a wait future resolves
* with the target's result, a normal IO/sleep future resolves with 0 on success. */
return sd_fiber_resume(sd_future_get_userdata(f), sd_future_result(f));
}
int sd_future_resolve(sd_future *f, int result) {
int r = 0;
assert_return(f, -EINVAL);
if (f->state != SD_FUTURE_PENDING)
return 0;
/* Hold a self-ref across callback/waiter dispatch: callbacks (e.g. bus_fiber_resolved()
* dropping the tracking-set's ref) may legitimately release what would otherwise be the
* last reference, and we still access f->waiters below. The cleanup unrefs at scope exit,
* which is when freeing is safe again. */
_unused_ _cleanup_(sd_future_unrefp) sd_future *self = sd_future_ref(f);
f->state = SD_FUTURE_RESOLVED;
f->result = result;
if (f->callback)
RET_GATHER(r, f->callback(f));
/* We'd like the set to not be modified while iterating over it, hence take ownership over it in
* a local variable. Otherwise code invoked via sd_future_resolve() could try to modify the set while
* we're iterating over it (for example wait_future_free()). */
Set *waiters = TAKE_PTR(f->waiters);
sd_future *w;
SET_FOREACH(w, waiters)
RET_GATHER(r, sd_future_resolve(w, result));
set_free(waiters);
return r;
}
static sd_future* sd_future_free(sd_future *f) {
if (!f)
return NULL;
if (f->state == SD_FUTURE_PENDING)
sd_future_resolve(f, -ECANCELED);
set_free(f->waiters);
if (f->ops->free)
f->ops->free(f);
return mfree(f);
}
DEFINE_TRIVIAL_REF_UNREF_FUNC(sd_future, sd_future, sd_future_free);
DEFINE_POINTER_ARRAY_CLEAR_FUNC(sd_future*, sd_future_unref);
DEFINE_POINTER_ARRAY_FREE_FUNC(sd_future*, sd_future_unref);
sd_future* sd_future_cancel_wait_unref(sd_future *f) {
int r;
if (!f)
return NULL;
/* We have to be able to suspend until the fiber we're waiting for finishes, and that's only
* possible if we're running on a fiber ourselves. */
if (!sd_fiber_is_running())
return sd_future_unref(f);
r = sd_future_cancel(f);
if (r < 0)
log_debug_errno(r, "Failed to cancel future, ignoring: %m");
if (f->state == SD_FUTURE_PENDING) {
/* Fast path: when f's resolve callback already targets the current fiber (the default for
* futures created on this fiber), we can suspend directly and let the existing trampoline
* wake us up — no need to allocate a wait future just to learn about the resolution.
* Otherwise fall back to sd_fiber_await() which sets up an explicit waiter. */
if (f->callback == fiber_resume_trampoline && f->userdata == sd_fiber_get_current())
r = sd_fiber_suspend();
else
r = sd_fiber_await(f);
if (r < 0 && r != -ECANCELED)
log_debug_errno(r, "Failed to wait for future to finish, ignoring: %m");
}
return sd_future_unref(f);
}
DEFINE_POINTER_ARRAY_CLEAR_FUNC(sd_future*, sd_future_cancel_wait_unref);
DEFINE_POINTER_ARRAY_FREE_FUNC(sd_future*, sd_future_cancel_wait_unref);
int sd_future_new(const sd_future_ops *ops, sd_future **ret) {
assert_return(ops, -EINVAL);
assert_return(ops->size >= endoffsetof_field(sd_future_ops, set_priority), -EINVAL);
assert_return(ops->alloc, -EINVAL);
assert_return(ops->free, -EINVAL);
assert_return(ret, -EINVAL);
sd_future *f = new(sd_future, 1);
if (!f)
return -ENOMEM;
*f = (sd_future) {
.n_ref = 1,
.state = SD_FUTURE_PENDING,
.ops = ops,
};
f->private = ops->alloc();
if (!f->private) {
free(f);
return -ENOMEM;
}
/* If we're being created on a fiber, default the callback to resuming that fiber on resolve —
* this is almost always what you want, and it saves the usual set_callback boilerplate before
* sd_fiber_suspend(). Callers that want different behavior can override with
* sd_future_set_callback(). */
sd_future *fiber = sd_fiber_get_current();
if (fiber)
(void) sd_future_set_callback(f, fiber_resume_trampoline, fiber);
*ret = f;
return 0;
}
int sd_future_state(sd_future *f) {
assert_return(f, -EINVAL);
return f->state;
}
int sd_future_result(sd_future *f) {
assert_return(f, -EINVAL);
assert_return(f->state == SD_FUTURE_RESOLVED, -EBUSY);
return f->result;
}
void* sd_future_get_userdata(sd_future *f) {
assert_return(f, NULL);
return f->userdata;
}
void* sd_future_get_private(sd_future *f) {
assert_return(f, NULL);
return f->private;
}
const sd_future_ops* sd_future_get_ops(sd_future *f) {
assert_return(f, NULL);
return f->ops;
}
int sd_future_set_callback(sd_future *f, sd_future_func_t callback, void *userdata) {
assert_return(f, -EINVAL);
f->callback = callback;
f->userdata = userdata;
return 0;
}
int sd_future_set_priority(sd_future *f, int64_t priority) {
assert_return(f, -EINVAL);
assert_return(f->state == SD_FUTURE_PENDING, -ESTALE);
assert_return(f->ops->set_priority, -EOPNOTSUPP);
return f->ops->set_priority(f, priority);
}
int sd_future_cancel(sd_future *f) {
assert_return(f, -EINVAL);
assert_return(f->ops->cancel, -EOPNOTSUPP);
if (f->state == SD_FUTURE_RESOLVED)
return 0;
return f->ops->cancel(f);
}
typedef struct WaitFuture {
sd_future *target;
} WaitFuture;
static void* wait_future_alloc(void) {
return new0(WaitFuture, 1);
}
static void wait_future_free(sd_future *f) {
WaitFuture *wf = ASSERT_PTR(sd_future_get_private(ASSERT_PTR(f)));
set_remove(wf->target->waiters, f);
sd_future_unref(wf->target);
free(wf);
}
static int wait_future_cancel(sd_future *f) {
WaitFuture *wf = ASSERT_PTR(sd_future_get_private(ASSERT_PTR(f)));
set_remove(wf->target->waiters, f);
return sd_future_resolve(f, -ECANCELED);
}
static const sd_future_ops wait_future_ops = {
.size = sizeof(sd_future_ops),
.alloc = wait_future_alloc,
.free = wait_future_free,
.cancel = wait_future_cancel,
};
int sd_future_new_wait(sd_future *target, sd_future **ret) {
int r;
assert_return(target, -EINVAL);
assert_return(ret, -EINVAL);
_cleanup_(sd_future_unrefp) sd_future *f = NULL;
r = sd_future_new(&wait_future_ops, &f);
if (r < 0)
return r;
WaitFuture *wf = sd_future_get_private(f);
wf->target = sd_future_ref(target);
if (target->state == SD_FUTURE_RESOLVED)
r = sd_future_resolve(f, target->result);
else
r = set_ensure_put(&target->waiters, &trivial_hash_ops, f);
if (r < 0)
return r;
*ret = TAKE_PTR(f);
return 0;
}
File diff suppressed because it is too large Load Diff
+574
View File
@@ -0,0 +1,574 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <fcntl.h>
#include <poll.h>
#include <unistd.h>
#include "sd-event.h"
#include "sd-future.h"
#include "alloc-util.h"
#include "cleanup-util.h"
#include "fd-util.h"
#include "io-util.h"
#include "pidref.h"
#include "process-util.h"
#include "tests.h"
#include "time-util.h"
/* Test: wait_for_terminate basic functionality */
static int wait_simple_fiber(void *userdata) {
_cleanup_(pidref_done_sigkill_wait) PidRef pidref = PIDREF_NULL;
siginfo_t si;
int r;
/* Fork a child that exits immediately */
r = pidref_safe_fork("(test-child)", FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_LOG, &pidref);
if (r < 0)
return r;
if (r == 0)
_exit(42);
/* Parent - wait for child */
r = pidref_wait_for_terminate(&pidref, &si);
if (r < 0)
return r;
pidref_done(&pidref);
/* Verify child exited with status 42 */
if (si.si_code != CLD_EXITED || si.si_status != 42)
return -EIO;
return 0;
}
TEST(wait_for_terminate_fiber_basic) {
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
ASSERT_OK(sd_event_new(&e));
ASSERT_OK(sd_event_set_exit_on_idle(e, true));
_cleanup_(sd_future_unrefp) sd_future *f = NULL;
ASSERT_OK(sd_fiber_new(e, "wait-simple", wait_simple_fiber, NULL, /* destroy= */ NULL, &f));
ASSERT_OK(sd_event_loop(e));
ASSERT_OK(sd_future_result(f));
}
/* Test: wait_for_terminate with multiple children */
static int wait_multiple_fiber(void *userdata) {
PidRef pidrefs[3] = { PIDREF_NULL, PIDREF_NULL, PIDREF_NULL };
siginfo_t si;
int r;
/* Fork three children with different exit codes */
for (size_t i = 0; i < 3; i++) {
r = pidref_safe_fork("(test-child)", FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_LOG, &pidrefs[i]);
if (r < 0)
goto cleanup;
if (r == 0)
/* Child process */
_exit(10 + i);
}
/* Wait for all three in order */
for (size_t i = 0; i < 3; i++) {
r = pidref_wait_for_terminate(&pidrefs[i], &si);
if (r < 0)
goto cleanup;
pidref_done(&pidrefs[i]);
if (si.si_code != CLD_EXITED || si.si_status != (int) (10 + i)) {
r = -EIO;
goto cleanup;
}
}
return 0;
cleanup:
for (size_t i = 0; i < 3; i++)
pidref_done(&pidrefs[i]);
return r;
}
TEST(wait_for_terminate_fiber_multiple) {
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
ASSERT_OK(sd_event_new(&e));
ASSERT_OK(sd_event_set_exit_on_idle(e, true));
_cleanup_(sd_future_unrefp) sd_future *f = NULL;
ASSERT_OK(sd_fiber_new(e, "wait-multiple", wait_multiple_fiber, NULL, /* destroy= */ NULL, &f));
ASSERT_OK(sd_event_loop(e));
ASSERT_OK(sd_future_result(f));
}
static int concurrent_wait_fiber(void *userdata) {
_cleanup_(pidref_done_sigkill_wait) PidRef pidref = PIDREF_NULL;
siginfo_t si;
int r;
r = pidref_safe_fork("(test-child)", FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_LOG, &pidref);
if (r < 0)
return r;
if (r == 0)
/* Child exits with specified status */
_exit(PTR_TO_INT(userdata));
r = pidref_wait_for_terminate(&pidref, &si);
if (r < 0)
return r;
pidref_done(&pidref);
if (si.si_code != CLD_EXITED || si.si_status != PTR_TO_INT(userdata))
return -EIO;
return 0;
}
TEST(wait_for_terminate_fiber_concurrent) {
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
ASSERT_OK(sd_event_new(&e));
ASSERT_OK(sd_event_set_exit_on_idle(e, true));
sd_future *fibers[3] = {};
CLEANUP_ELEMENTS(fibers, sd_future_unref_array_clear);
/* Create 3 fibers, each waiting for a different child */
for (size_t i = 0; i < ELEMENTSOF(fibers); i++)
ASSERT_OK(sd_fiber_new(e, "concurrent-wait", concurrent_wait_fiber, INT_TO_PTR(20 + i), /* destroy= */ NULL, &fibers[i]));
ASSERT_OK(sd_event_loop(e));
/* All fibers should complete successfully */
for (size_t i = 0; i < ELEMENTSOF(fibers); i++)
ASSERT_OK(sd_future_result(fibers[i]));
}
typedef struct LoopIOContext {
int *pipefd;
const char *data;
size_t len;
int order;
} LoopIOContext;
static int loop_read_suspend_fiber(void *userdata) {
LoopIOContext *ctx = ASSERT_PTR(userdata);
char buf[64];
ASSERT_EQ(ctx->order, 0);
ctx->order = 1;
ssize_t n = loop_read(ctx->pipefd[0], buf, sizeof(buf), /* do_poll= */ true);
/* While we were suspended, the writer fiber should have run. */
ASSERT_EQ(ctx->order, 2);
if (n < 0)
return (int) n;
if ((size_t) n != ctx->len || memcmp(buf, ctx->data, ctx->len) != 0)
return -EIO;
return (int) n;
}
static int loop_write_suspend_fiber(void *userdata) {
LoopIOContext *ctx = ASSERT_PTR(userdata);
ASSERT_EQ(ctx->order, 1);
ctx->order = 2;
int r = loop_write(ctx->pipefd[1], ctx->data, ctx->len);
if (r < 0)
return r;
/* Close the write end so the reader sees EOF after reading the data. */
ctx->pipefd[1] = safe_close(ctx->pipefd[1]);
return 0;
}
/* Test: two fibers cooperatively pass a small payload through a blocking pipe using the suspending
* loop helpers. Exercises the non-blocking flip, event-loop yielding, and the blocking-mode restore. */
TEST(loop_read_write_suspend) {
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
ASSERT_OK(sd_event_new(&e));
ASSERT_OK(sd_event_set_exit_on_idle(e, true));
_cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC));
static const char payload[] = "loop-suspend";
LoopIOContext ctx = {
.pipefd = pipefd,
.data = payload,
.len = sizeof(payload) - 1,
};
_cleanup_(sd_future_unrefp) sd_future *fr = NULL, *fw = NULL;
ASSERT_OK(sd_fiber_new(e, "loop-read", loop_read_suspend_fiber, &ctx, /* destroy= */ NULL, &fr));
ASSERT_OK(sd_future_set_priority(fr, 0));
ASSERT_OK(sd_fiber_new(e, "loop-write", loop_write_suspend_fiber, &ctx, /* destroy= */ NULL, &fw));
ASSERT_OK(sd_future_set_priority(fw, 1));
ASSERT_OK(sd_event_loop(e));
ASSERT_OK_EQ(sd_future_result(fr), (int) ctx.len);
ASSERT_OK_ZERO(sd_future_result(fw));
/* The read fd started out blocking and loop_read() must have restored it before returning. */
ASSERT_OK_ZERO(fcntl(pipefd[0], F_GETFL) & O_NONBLOCK);
}
static int loop_read_exact_short_fiber(void *userdata) {
int fd = PTR_TO_INT(userdata);
char buf[16];
/* Requesting more bytes than the peer writes should return -EIO once EOF is hit. */
return loop_read_exact(fd, buf, sizeof(buf), /* do_poll= */ true);
}
/* Test: loop_read_exact() returns -EIO when the peer closes early. */
TEST(loop_read_exact_short) {
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
ASSERT_OK(sd_event_new(&e));
ASSERT_OK(sd_event_set_exit_on_idle(e, true));
_cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC));
_cleanup_(sd_future_unrefp) sd_future *f = NULL;
ASSERT_OK(sd_fiber_new(e, "loop-read-exact", loop_read_exact_short_fiber,
INT_TO_PTR(pipefd[0]), /* destroy= */ NULL, &f));
/* Write a few bytes and close the write end — less than the fiber asked for. */
ASSERT_OK_EQ_ERRNO(write(pipefd[1], "abc", 3), (ssize_t) 3);
pipefd[1] = safe_close(pipefd[1]);
ASSERT_OK(sd_event_loop(e));
ASSERT_ERROR(sd_future_result(f), EIO);
}
typedef struct LoopWriteTimeoutContext {
int fd;
int result;
} LoopWriteTimeoutContext;
static int loop_write_timeout_fiber(void *userdata) {
LoopWriteTimeoutContext *ctx = ASSERT_PTR(userdata);
/* Try to write much more than the pipe buffer can hold with a short timeout. The write will
* succeed partially and then hit -ETIME after exhausting the timeout while blocked. */
static const char big_buf[128 * 1024] = { 0 };
ctx->result = loop_write_full(ctx->fd, big_buf, sizeof(big_buf), 100 * USEC_PER_MSEC);
return 0;
}
/* Test: loop_write_full() returns -ETIME when the peer never drains. */
TEST(loop_write_full_timeout) {
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
ASSERT_OK(sd_event_new(&e));
ASSERT_OK(sd_event_set_exit_on_idle(e, true));
_cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC));
/* Shrink the pipe buffer to its minimum (one page) so the 128K write below is guaranteed to block
* regardless of the architecture's page size. The default pipe buffer is 16 pages, which on
* 64K-page architectures (e.g. ppc64le) is 1 MiB — enough to absorb the entire write without ever
* blocking, defeating the purpose of the timeout. */
ASSERT_OK_ERRNO(fcntl(pipefd[1], F_SETPIPE_SZ, 1));
LoopWriteTimeoutContext ctx = { .fd = pipefd[1], .result = 0 };
_cleanup_(sd_future_unrefp) sd_future *f = NULL;
ASSERT_OK(sd_fiber_new(e, "loop-write-timeout", loop_write_timeout_fiber, &ctx, /* destroy= */ NULL, &f));
ASSERT_OK(sd_event_loop(e));
ASSERT_OK_ZERO(sd_future_result(f));
ASSERT_ERROR(ctx.result, ETIME);
}
typedef struct PpollDispatchContext {
int *pipefd;
int order;
} PpollDispatchContext;
static int ppoll_dispatch_read_fiber(void *userdata) {
PpollDispatchContext *ctx = ASSERT_PTR(userdata);
struct pollfd pfd = {
.fd = ctx->pipefd[0],
.events = POLLIN,
};
ASSERT_EQ(ctx->order, 0);
ctx->order = 1;
/* Direct ppoll_usec() call from a fiber must dispatch through sd_fiber_poll(), suspending the
* fiber instead of blocking the entire thread. If dispatch fails, the writer fiber never gets a
* chance to run and the test deadlocks. */
int r = ppoll_usec(&pfd, 1, USEC_INFINITY);
if (r < 0)
return r;
ASSERT_EQ(ctx->order, 2);
if (r != 1 || !FLAGS_SET(pfd.revents, POLLIN))
return -EIO;
return 0;
}
static int ppoll_dispatch_write_fiber(void *userdata) {
PpollDispatchContext *ctx = ASSERT_PTR(userdata);
ASSERT_EQ(ctx->order, 1);
ctx->order = 2;
if (write(ctx->pipefd[1], "x", 1) < 0)
return -errno;
return 0;
}
/* Test: ppoll_usec() called from a fiber dispatches through the FiberOps hook to sd_fiber_poll(),
* yielding to the event loop instead of blocking. */
TEST(ppoll_usec_dispatch) {
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
ASSERT_OK(sd_event_new(&e));
ASSERT_OK(sd_event_set_exit_on_idle(e, true));
_cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC | O_NONBLOCK));
PpollDispatchContext ctx = { .pipefd = pipefd };
_cleanup_(sd_future_unrefp) sd_future *fr = NULL, *fw = NULL;
ASSERT_OK(sd_fiber_new(e, "ppoll-read", ppoll_dispatch_read_fiber, &ctx, /* destroy= */ NULL, &fr));
ASSERT_OK(sd_future_set_priority(fr, 0));
ASSERT_OK(sd_fiber_new(e, "ppoll-write", ppoll_dispatch_write_fiber, &ctx, /* destroy= */ NULL, &fw));
ASSERT_OK(sd_future_set_priority(fw, 1));
ASSERT_OK(sd_event_loop(e));
ASSERT_OK(sd_future_result(fr));
ASSERT_OK(sd_future_result(fw));
}
static int loop_write_zero_timeout_nonblock_fiber(void *userdata) {
int fd = PTR_TO_INT(userdata);
/* Fill the pipe so the next write would block. The fd is non-blocking, so on a fiber
* loop_write_full(timeout=0) must take the non-fiber path and return -EAGAIN immediately
* rather than suspending. */
static const char big_buf[128 * 1024] = { 0 };
return loop_write_full(fd, big_buf, sizeof(big_buf), /* timeout= */ 0);
}
/* Test: timeout == 0 on a non-blocking fd from a fiber preserves the "don't wait" semantic and
* returns -EAGAIN when the pipe buffer is full, instead of suspending the fiber. */
TEST(loop_write_zero_timeout_nonblock) {
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
ASSERT_OK(sd_event_new(&e));
ASSERT_OK(sd_event_set_exit_on_idle(e, true));
_cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC | O_NONBLOCK));
ASSERT_OK_ERRNO(fcntl(pipefd[1], F_SETPIPE_SZ, 1));
_cleanup_(sd_future_unrefp) sd_future *f = NULL;
ASSERT_OK(sd_fiber_new(e, "loop-write-zt-nb", loop_write_zero_timeout_nonblock_fiber,
INT_TO_PTR(pipefd[1]), /* destroy= */ NULL, &f));
ASSERT_OK(sd_event_loop(e));
ASSERT_ERROR(sd_future_result(f), EAGAIN);
}
typedef struct LoopWriteZeroBlockingContext {
int *pipefd;
size_t total;
int order;
} LoopWriteZeroBlockingContext;
static int loop_write_zero_blocking_writer_fiber(void *userdata) {
LoopWriteZeroBlockingContext *ctx = ASSERT_PTR(userdata);
ASSERT_EQ(ctx->order, 0);
ctx->order = 1;
/* timeout == 0 on a *blocking* fd from a fiber: the fast EAGAIN return isn't possible, so
* loop_write_full() takes the fiber path. The reader fiber drains the pipe, letting our
* write complete via fiber suspension/resume. */
_cleanup_free_ char *big_buf = malloc0(ctx->total);
ASSERT_NOT_NULL(big_buf);
int r = loop_write_full(ctx->pipefd[1], big_buf, ctx->total, /* timeout= */ 0);
ASSERT_EQ(ctx->order, 2);
return r;
}
static int loop_write_zero_blocking_reader_fiber(void *userdata) {
LoopWriteZeroBlockingContext *ctx = ASSERT_PTR(userdata);
ASSERT_EQ(ctx->order, 1);
ctx->order = 2;
_cleanup_free_ char *buf = malloc(ctx->total);
ASSERT_NOT_NULL(buf);
ssize_t n = loop_read(ctx->pipefd[0], buf, ctx->total, /* do_poll= */ true);
if (n < 0)
return (int) n;
return (int) n;
}
/* Test: timeout == 0 on a blocking fd from a fiber takes the fiber path (suspends until the peer
* drains) instead of blocking the entire thread. */
TEST(loop_write_zero_timeout_blocking) {
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
ASSERT_OK(sd_event_new(&e));
ASSERT_OK(sd_event_set_exit_on_idle(e, true));
_cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC));
ASSERT_OK_ERRNO(fcntl(pipefd[1], F_SETPIPE_SZ, 1));
/* F_SETPIPE_SZ rounds up to the kernel's pipe minimum (typically a page); query the actual
* size and write more than that, so the write must wait on the reader regardless of page size. */
int pipe_sz = fcntl(pipefd[1], F_GETPIPE_SZ);
ASSERT_OK_ERRNO(pipe_sz);
LoopWriteZeroBlockingContext ctx = { .pipefd = pipefd, .total = (size_t) pipe_sz * 2 };
_cleanup_(sd_future_unrefp) sd_future *fw = NULL, *fr = NULL;
ASSERT_OK(sd_fiber_new(e, "loop-write-zt-blk", loop_write_zero_blocking_writer_fiber,
&ctx, /* destroy= */ NULL, &fw));
ASSERT_OK(sd_future_set_priority(fw, 0));
ASSERT_OK(sd_fiber_new(e, "loop-read-zt-blk", loop_write_zero_blocking_reader_fiber,
&ctx, /* destroy= */ NULL, &fr));
ASSERT_OK(sd_future_set_priority(fr, 1));
ASSERT_OK(sd_event_loop(e));
ASSERT_OK(sd_future_result(fw));
ASSERT_OK_EQ(sd_future_result(fr), (int) ctx.total);
}
static int loop_read_no_poll_nonblock_fiber(void *userdata) {
int fd = PTR_TO_INT(userdata);
char buf[64];
/* Empty non-blocking pipe + do_poll=false: on a fiber loop_read() must take the non-fiber
* path and return -EAGAIN immediately rather than suspending. */
return (int) loop_read(fd, buf, sizeof(buf), /* do_poll= */ false);
}
/* Test: do_poll == false on a non-blocking fd from a fiber preserves the "don't wait" semantic
* and returns -EAGAIN when no data is available, instead of suspending the fiber. */
TEST(loop_read_no_poll_nonblock) {
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
ASSERT_OK(sd_event_new(&e));
ASSERT_OK(sd_event_set_exit_on_idle(e, true));
_cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC | O_NONBLOCK));
_cleanup_(sd_future_unrefp) sd_future *f = NULL;
ASSERT_OK(sd_fiber_new(e, "loop-read-np-nb", loop_read_no_poll_nonblock_fiber,
INT_TO_PTR(pipefd[0]), /* destroy= */ NULL, &f));
ASSERT_OK(sd_event_loop(e));
ASSERT_ERROR(sd_future_result(f), EAGAIN);
}
typedef struct LoopReadNoPollBlockingContext {
int *pipefd;
const char *data;
size_t len;
int order;
} LoopReadNoPollBlockingContext;
static int loop_read_no_poll_blocking_reader_fiber(void *userdata) {
LoopReadNoPollBlockingContext *ctx = ASSERT_PTR(userdata);
char buf[64];
ASSERT_EQ(ctx->order, 0);
ctx->order = 1;
/* do_poll == false on a *blocking* fd from a fiber: the fast EAGAIN return isn't possible,
* so loop_read() takes the fiber path and suspends until the writer fiber feeds data. */
ssize_t n = loop_read(ctx->pipefd[0], buf, sizeof(buf), /* do_poll= */ false);
ASSERT_EQ(ctx->order, 2);
if (n < 0)
return (int) n;
if ((size_t) n != ctx->len || memcmp(buf, ctx->data, ctx->len) != 0)
return -EIO;
return (int) n;
}
static int loop_read_no_poll_blocking_writer_fiber(void *userdata) {
LoopReadNoPollBlockingContext *ctx = ASSERT_PTR(userdata);
ASSERT_EQ(ctx->order, 1);
ctx->order = 2;
int r = loop_write(ctx->pipefd[1], ctx->data, ctx->len);
if (r < 0)
return r;
ctx->pipefd[1] = safe_close(ctx->pipefd[1]);
return 0;
}
/* Test: do_poll == false on a blocking fd from a fiber takes the fiber path (suspends until the
* peer feeds data) instead of blocking the entire thread. */
TEST(loop_read_no_poll_blocking) {
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
ASSERT_OK(sd_event_new(&e));
ASSERT_OK(sd_event_set_exit_on_idle(e, true));
_cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC));
static const char payload[] = "no-poll";
LoopReadNoPollBlockingContext ctx = {
.pipefd = pipefd,
.data = payload,
.len = sizeof(payload) - 1,
};
_cleanup_(sd_future_unrefp) sd_future *fr = NULL, *fw = NULL;
ASSERT_OK(sd_fiber_new(e, "loop-read-np-blk", loop_read_no_poll_blocking_reader_fiber,
&ctx, /* destroy= */ NULL, &fr));
ASSERT_OK(sd_future_set_priority(fr, 0));
ASSERT_OK(sd_fiber_new(e, "loop-write-np-blk", loop_read_no_poll_blocking_writer_fiber,
&ctx, /* destroy= */ NULL, &fw));
ASSERT_OK(sd_future_set_priority(fw, 1));
ASSERT_OK(sd_event_loop(e));
ASSERT_OK_EQ(sd_future_result(fr), (int) ctx.len);
ASSERT_OK_ZERO(sd_future_result(fw));
}
/* Test: loop_*() helpers transparently fall back to blocking I/O when called outside any
* fiber context. */
TEST(loop_read_write_fallback) {
_cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC));
ASSERT_OK(loop_write(pipefd[1], "fallback", STRLEN("fallback")));
char buf[16];
ssize_t n = loop_read(pipefd[0], buf, STRLEN("fallback"), /* do_poll= */ true);
ASSERT_OK_EQ(n, (ssize_t) STRLEN("fallback"));
ASSERT_EQ(memcmp(buf, "fallback", STRLEN("fallback")), 0);
}
DEFINE_TEST_MAIN(LOG_DEBUG);
File diff suppressed because it is too large Load Diff
+257 -44
View File
@@ -6,6 +6,7 @@
#include "sd-daemon.h"
#include "sd-event.h"
#include "sd-future.h"
#include "sd-varlink.h"
#include "alloc-util.h"
@@ -37,6 +38,7 @@
#include "varlink-internal.h"
#include "varlink-io.systemd.h"
#include "varlink-org.varlink.service.h"
#include "varlink-util.h"
#define VARLINK_DEFAULT_CONNECTIONS_MAX 4096U
#define VARLINK_DEFAULT_CONNECTIONS_PER_UID_MAX 128U
@@ -956,6 +958,178 @@ static int generic_method_get_interface_description(
SD_JSON_BUILD_PAIR_STRING("description", text));
}
static int varlink_dispatch_sentinel(sd_varlink *v) {
int r;
assert(v);
assert(v->sentinel);
if (v->previous) {
r = json_stream_enqueue_full(&v->stream, v->previous, v->previous_fds, v->n_previous_fds);
if (r >= 0) {
v->previous = sd_json_variant_unref(v->previous);
v->previous_fds = mfree(v->previous_fds);
v->n_previous_fds = 0;
/* Mirror sd_varlink_reply()'s post-enqueue state machine: PENDING_* means we're
* outside the dispatch stack frame (e.g. called from varlink_fiber_entry after
* the fiber returned), so we go straight to IDLE_SERVER ourselves. PROCESSING_*
* means we're inside varlink_dispatch_method(), which will transition us. */
if (IN_SET(v->state, VARLINK_PENDING_METHOD, VARLINK_PENDING_METHOD_MORE)) {
varlink_clear_current(v);
varlink_set_state(v, VARLINK_IDLE_SERVER);
} else
varlink_set_state(v, VARLINK_PROCESSED_METHOD);
}
return r;
}
char *sentinel = TAKE_PTR(v->sentinel);
/* Propagate the sentinel to the client if one was configured and no replies were enqueued by
* the callback. */
if (sentinel == POINTER_MAX)
r = sd_varlink_reply(v, NULL);
else {
r = sd_varlink_error(v, sentinel, NULL);
/* sd_varlink_error() deliberately returns a negative
* errno mapped from the error id on success (so method
* callbacks can `return sd_varlink_error(...);` to
* enqueue a reply and propagate a matching errno in one
* go). For sentinel dispatch we don't care about that
* mapping — the reply is either enqueued or not, which
* we detect via the state transition instead. */
if (IN_SET(v->state, VARLINK_PROCESSED_METHOD, VARLINK_IDLE_SERVER))
r = 0;
}
if (sentinel != POINTER_MAX)
free(sentinel);
return r;
}
typedef struct VarlinkFiberData {
sd_varlink *link;
sd_json_variant *parameters;
sd_varlink_method_flags_t flags;
void *userdata;
sd_varlink_method_t callback;
} VarlinkFiberData;
static VarlinkFiberData* varlink_fiber_data_free(VarlinkFiberData *d) {
if (!d)
return NULL;
sd_json_variant_unref(d->parameters);
sd_varlink_unref(d->link);
return mfree(d);
}
DEFINE_TRIVIAL_CLEANUP_FUNC(VarlinkFiberData*, varlink_fiber_data_free);
static void varlink_fiber_data_destroy(void *userdata) {
varlink_fiber_data_free(userdata);
}
static int varlink_fiber_entry(void *userdata) {
VarlinkFiberData *d = ASSERT_PTR(userdata);
sd_varlink *v = d->link;
int r;
r = d->callback(v, d->parameters, d->flags, d->userdata);
/* The fiber runs after varlink_dispatch_method() has already transitioned the state from
* VARLINK_PROCESSING_METHOD{,_MORE} to VARLINK_PENDING_METHOD{,_MORE}, so that's what we match
* here to decide whether the call still needs a reply. Any other state (e.g. IDLE_SERVER after
* the callback replied, or DISCONNECTED after sd_varlink_close()) means no fixup is needed. */
if (!IN_SET(v->state, VARLINK_PENDING_METHOD, VARLINK_PENDING_METHOD_MORE))
return r;
if (r < 0) {
varlink_log_errno(v, r, "Fiber returned error: %m");
/* Propagate error to the client if the method call remains unanswered. */
r = sd_varlink_error_errno(v, r);
} else if (v->sentinel) {
r = varlink_dispatch_sentinel(v);
if (r < 0)
varlink_log_errno(v, r, "Failed to process sentinel: %m");
} else if (v->n_ref <= 2) {
/* Bare minimum refs (server + fiber data) means the connection wasn't stashed
* to reply later, so the fiber was supposed to reply itself but didn't. */
r = varlink_log_errno(v, SYNTHETIC_ERRNO(EPROTO),
"Fiber returned without enqueuing a reply or stashing connection, failing.");
goto fail;
} else
r = 0;
/* If we didn't manage to enqueue a response, then fail the connection completely. */
if (r < 0 && IN_SET(v->state, VARLINK_PENDING_METHOD, VARLINK_PENDING_METHOD_MORE))
goto fail;
return r;
fail:
varlink_set_state(v, VARLINK_PROCESSING_FAILURE);
varlink_dispatch_local_error(v, SD_VARLINK_ERROR_PROTOCOL);
sd_varlink_close(v);
return r;
}
static int varlink_dispatch_fiber(sd_varlink *v, const char *method, sd_varlink_method_t callback, sd_json_variant *parameters, sd_varlink_method_flags_t flags) {
int r;
assert(v);
assert(v->server);
assert(method);
assert(callback);
if (!v->server->event)
return varlink_log_errno(v, SYNTHETIC_ERRNO(EDEADLK),
"Cannot dispatch fiber method without event loop.");
_cleanup_(varlink_fiber_data_freep) VarlinkFiberData *d = new(VarlinkFiberData, 1);
if (!d)
return log_oom_debug();
*d = (VarlinkFiberData) {
.link = sd_varlink_ref(v),
.parameters = sd_json_variant_ref(parameters),
.flags = flags,
.userdata = v->userdata,
.callback = callback,
};
_cleanup_(sd_future_unrefp) sd_future *f = NULL;
r = sd_fiber_new(v->server->event, method, varlink_fiber_entry, d, varlink_fiber_data_destroy, &f);
if (r < 0)
return r;
TAKE_PTR(d); /* The fiber owns the data now. */
/* Run the fiber at a higher priority than the connection's quit event source, so that on event
* loop exit the fiber's exit source (which cancels it and drives its cleanup) fires before
* varlink's quit_callback closes the connection. This lets a fiber handler reply with an error
* or flush its sentinel on a still-open connection during graceful shutdown. */
int64_t priority;
r = sd_event_source_get_priority(v->quit_event_source, &priority);
if (r < 0)
return r;
r = sd_future_set_priority(f, priority > INT64_MIN ? priority - 1 : priority);
if (r < 0)
return r;
/* Hand the future's lifetime over to the event loop: it'll auto-unref on resolve. */
r = sd_fiber_set_floating(f, true);
if (r < 0)
return r;
return 0;
}
static int varlink_dispatch_method(sd_varlink *v) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *parameters = NULL;
sd_varlink_method_flags_t flags = 0;
@@ -1053,7 +1227,13 @@ static int varlink_dispatch_method(sd_varlink *v) {
v->protocol_upgrade || FLAGS_SET(v->server->flags, SD_VARLINK_SERVER_UPGRADABLE));
/* First consult user supplied method implementations */
bool is_fiber = false;
callback = hashmap_get(v->server->methods, method);
if (!callback) {
callback = hashmap_get(v->server->fiber_methods, method);
if (callback)
is_fiber = true;
}
if (!callback) {
if (streq(method, "org.varlink.service.GetInfo"))
callback = generic_method_get_info;
@@ -1105,7 +1285,13 @@ static int varlink_dispatch_method(sd_varlink *v) {
}
if (!invalid) {
r = callback(v, parameters, flags, v->userdata);
if (is_fiber)
/* Spawn a fiber to run the callback. The VarlinkFiberData takes a ref on the
* connection (bumping n_ref above 2), so the post-callback logic below treats
* this as a deferred reply and moves state to PENDING_METHOD. */
r = varlink_dispatch_fiber(v, method, callback, parameters, flags);
else
r = callback(v, parameters, flags, v->userdata);
if (VARLINK_STATE_WANTS_REPLY(v->state)) {
if (r < 0) {
varlink_log_errno(v, r, "Callback for '%s' returned error: %m", method);
@@ -1114,37 +1300,7 @@ static int varlink_dispatch_method(sd_varlink *v) {
* if the method call remains unanswered. */
r = sd_varlink_error_errno(v, r);
} else if (v->sentinel) {
if (v->previous) {
r = json_stream_enqueue_full(&v->stream, v->previous, v->previous_fds, v->n_previous_fds);
if (r >= 0) {
v->previous = sd_json_variant_unref(v->previous);
v->previous_fds = mfree(v->previous_fds);
v->n_previous_fds = 0;
varlink_set_state(v, VARLINK_PROCESSED_METHOD);
}
} else {
char *sentinel = TAKE_PTR(v->sentinel);
/* Propagate the sentinel to the client if one was configured
* and no replies were enqueued by the callback. */
if (sentinel == POINTER_MAX)
r = sd_varlink_reply(v, NULL);
else {
r = sd_varlink_error(v, sentinel, NULL);
/* sd_varlink_error() deliberately returns a negative
* errno mapped from the error id on success (so method
* callbacks can `return sd_varlink_error(...);` to
* enqueue a reply and propagate a matching errno in one
* go). For sentinel dispatch we don't care about that
* mapping — the reply is either enqueued or not, which
* we detect via the state transition instead. */
if (v->state == VARLINK_PROCESSED_METHOD)
r = 0;
}
if (sentinel != POINTER_MAX)
free(sentinel);
}
r = varlink_dispatch_sentinel(v);
if (r < 0)
varlink_log_errno(v, r, "Failed to process sentinel for method '%s': %m", method);
} else {
@@ -2596,8 +2752,12 @@ _public_ int sd_varlink_set_sentinel(sd_varlink *v, const char *error_id) {
if (v->state == VARLINK_PROCESSING_METHOD_ONEWAY)
return 0;
/* This has to be called during a callback, and not after it has exited. */
assert_return(IN_SET(v->state, VARLINK_PROCESSING_METHOD, VARLINK_PROCESSING_METHOD_MORE),
/* This has to be called during a callback, and not after it has exited. The PENDING states
* apply to fiber callbacks, which run after varlink_dispatch_method() has already transitioned
* the state from PROCESSING to PENDING. */
assert_return(IN_SET(v->state,
VARLINK_PROCESSING_METHOD, VARLINK_PROCESSING_METHOD_MORE,
VARLINK_PENDING_METHOD, VARLINK_PENDING_METHOD_MORE),
-EUCLEAN);
char *s = NULL;
@@ -2899,7 +3059,11 @@ static sd_varlink_server* varlink_server_destroy(sd_varlink_server *s) {
while ((m = hashmap_steal_first_key(s->methods)))
free(m);
while ((m = hashmap_steal_first_key(s->fiber_methods)))
free(m);
hashmap_free(s->methods);
hashmap_free(s->fiber_methods);
hashmap_free(s->interfaces);
hashmap_free(s->symbols);
hashmap_free(s->by_uid);
@@ -3590,23 +3754,32 @@ static bool varlink_symbol_in_interface(const char *method, const char *interfac
return !strchr(p+1, '.');
}
_public_ int sd_varlink_server_bind_method(sd_varlink_server *s, const char *method, sd_varlink_method_t callback) {
static int varlink_server_bind_internal(sd_varlink_server *s, Hashmap **methods, const char *method, sd_varlink_method_t callback) {
_cleanup_free_ char *m = NULL;
int r;
assert_return(s, -EINVAL);
assert_return(method, -EINVAL);
assert_return(callback, -EINVAL);
assert(s);
assert(methods);
assert(method);
assert(callback);
if (varlink_symbol_in_interface(method, "org.varlink.service") ||
varlink_symbol_in_interface(method, "io.systemd"))
return varlink_server_log_errno(s, SYNTHETIC_ERRNO(EEXIST), "Cannot bind server to '%s'.", method);
/* Refuse to register the same method in both the regular and fiber method maps: the dispatcher
* always consults methods first and would silently ignore a shadowed fiber_methods entry (or vice
* versa), hiding the misconfiguration. */
Hashmap *other = methods == &s->methods ? s->fiber_methods : s->methods;
if (hashmap_contains(other, method))
return varlink_server_log_errno(s, SYNTHETIC_ERRNO(EEXIST),
"Method '%s' is already bound in the other method map.", method);
m = strdup(method);
if (!m)
return log_oom_debug();
r = hashmap_ensure_put(&s->methods, &string_hash_ops, m, callback);
r = hashmap_ensure_put(methods, &string_hash_ops, m, callback);
if (r == -ENOMEM)
return log_oom_debug();
if (r < 0)
@@ -3617,13 +3790,12 @@ _public_ int sd_varlink_server_bind_method(sd_varlink_server *s, const char *met
return 0;
}
_public_ int sd_varlink_server_bind_method_many_internal(sd_varlink_server *s, ...) {
va_list ap;
static int varlink_server_bind_many_internal(sd_varlink_server *s, Hashmap **methods, va_list ap) {
int r = 0;
assert_return(s, -EINVAL);
assert(s);
assert(methods);
va_start(ap, s);
for (;;) {
sd_varlink_method_t callback;
const char *method;
@@ -3634,10 +3806,51 @@ _public_ int sd_varlink_server_bind_method_many_internal(sd_varlink_server *s, .
callback = va_arg(ap, sd_varlink_method_t);
r = sd_varlink_server_bind_method(s, method, callback);
r = varlink_server_bind_internal(s, methods, method, callback);
if (r < 0)
break;
}
return r;
}
_public_ int sd_varlink_server_bind_method(sd_varlink_server *s, const char *method, sd_varlink_method_t callback) {
assert_return(s, -EINVAL);
assert_return(method, -EINVAL);
assert_return(callback, -EINVAL);
return varlink_server_bind_internal(s, &s->methods, method, callback);
}
_public_ int sd_varlink_server_bind_method_many_internal(sd_varlink_server *s, ...) {
va_list ap;
int r;
assert_return(s, -EINVAL);
va_start(ap, s);
r = varlink_server_bind_many_internal(s, &s->methods, ap);
va_end(ap);
return r;
}
int varlink_server_bind_fiber(sd_varlink_server *s, const char *method, sd_varlink_method_t callback) {
assert_return(s, -EINVAL);
assert_return(method, -EINVAL);
assert_return(callback, -EINVAL);
return varlink_server_bind_internal(s, &s->fiber_methods, method, callback);
}
int varlink_server_bind_fiber_many_internal(sd_varlink_server *s, ...) {
va_list ap;
int r;
assert_return(s, -EINVAL);
va_start(ap, s);
r = varlink_server_bind_many_internal(s, &s->fiber_methods, ap);
va_end(ap);
return r;
+266 -96
View File
@@ -2,13 +2,13 @@
#include <fcntl.h>
#include <poll.h>
#include <pthread.h>
#include <sys/resource.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <unistd.h>
#include "sd-event.h"
#include "sd-future.h"
#include "sd-json.h"
#include "sd-varlink.h"
@@ -216,7 +216,12 @@ static void flood_test(const char *address) {
/* Block the main event loop while we flood */
ASSERT_OK_EQ_ERRNO(write(block_write_fd, &x, sizeof(x)), (ssize_t) sizeof(x));
ASSERT_OK(sd_event_default(&e));
/* Create a fresh event loop for the flood test — we can't reuse the default event because the
* main test (and the fiber we're running in) is already running it, and sd_event_loop() asserts
* the event is in the INITIAL state. Exit-on-idle so the nested loop terminates once the
* overload reply has been received and all other work is quiesced. */
ASSERT_OK(sd_event_new(&e));
ASSERT_OK(sd_event_set_exit_on_idle(e, true));
/* Flood the server with connections */
ASSERT_NOT_NULL(connections = new0(sd_varlink*, OVERLOAD_CONNECTIONS));
@@ -251,7 +256,7 @@ static void flood_test(const char *address) {
connections[k] = sd_varlink_unref(connections[k]);
}
static void *thread(void *arg) {
static int client_fiber(void *arg) {
_cleanup_(sd_varlink_flush_close_unrefp) sd_varlink *c = NULL;
_cleanup_(sd_json_variant_unrefp) sd_json_variant *i = NULL;
_cleanup_(sd_json_variant_unrefp) sd_json_variant *wrong = NULL;
@@ -263,7 +268,7 @@ static void *thread(void *arg) {
SD_JSON_BUILD_PAIR_INTEGER("b", 99))));
ASSERT_OK(sd_varlink_connect_address(&c, arg));
ASSERT_OK(sd_varlink_set_description(c, "thread-client"));
ASSERT_OK(sd_varlink_set_description(c, "fiber-client"));
ASSERT_OK(sd_varlink_set_allow_fd_passing_input(c, true));
ASSERT_OK(sd_varlink_set_allow_fd_passing_output(c, true));
@@ -321,7 +326,7 @@ static void *thread(void *arg) {
ASSERT_OK(sd_varlink_send(c, "io.test.Done", NULL));
return NULL;
return 0;
}
static int block_fd_handler(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
@@ -348,8 +353,8 @@ TEST(chat) {
_cleanup_(rm_rf_physical_and_freep) char *tmpdir = NULL;
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
_cleanup_(sd_future_unrefp) sd_future *f = NULL;
_cleanup_close_pair_ int block_fds[2] = EBADF_PAIR;
pthread_t t;
const char *sp;
ASSERT_OK(mkdtemp_malloc("/tmp/varlink-test-XXXXXX", &tmpdir));
@@ -388,11 +393,11 @@ TEST(chat) {
ASSERT_OK(sd_varlink_attach_event(c, e, 0));
ASSERT_OK(-pthread_create(&t, NULL, thread, (void*) sp));
ASSERT_OK(sd_fiber_new(e, "client", client_fiber, (void*) sp, /* destroy= */ NULL, &f));
ASSERT_OK(sd_event_loop(e));
ASSERT_OK(-pthread_join(t, NULL));
ASSERT_OK(sd_future_result(f));
}
static int method_invalid(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
@@ -618,6 +623,173 @@ TEST(sentinel_oneway) {
ASSERT_OK(sd_event_loop(e));
}
static int method_fiber_sentinel_error(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
/* Set an error sentinel from a fiber callback and return without sending a reply. The sentinel
* error should still be propagated by the fiber's post-callback logic, even though the varlink
* state has already been transitioned to VARLINK_PENDING_METHOD by the time the fiber runs. */
ASSERT_OK(sd_varlink_set_sentinel(link, "io.test.SentinelError"));
return 0;
}
TEST(fiber_sentinel_error) {
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
ASSERT_OK(sd_event_default(&e));
_cleanup_(sd_varlink_server_unrefp) sd_varlink_server *s = NULL;
ASSERT_OK(sd_varlink_server_new(&s, 0));
ASSERT_OK(sd_varlink_server_attach_event(s, e, 0));
ASSERT_OK(varlink_server_bind_fiber(s, "io.test.FiberSentinelError", method_fiber_sentinel_error));
int connfd[2];
ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_NONBLOCK|SOCK_CLOEXEC, 0, connfd));
ASSERT_OK(sd_varlink_server_add_connection(s, connfd[0], /* ret= */ NULL));
_cleanup_(sd_varlink_unrefp) sd_varlink *c = NULL;
ASSERT_OK(sd_varlink_connect_fd(&c, connfd[1]));
ASSERT_OK(sd_varlink_attach_event(c, e, 0));
ASSERT_OK(sd_varlink_bind_reply(c, reply_sentinel_error));
ASSERT_OK(sd_varlink_invoke(c, "io.test.FiberSentinelError", /* parameters= */ NULL));
ASSERT_OK(sd_event_loop(e));
}
static int method_fiber_errno(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
/* Return a negative errno without sending a reply. The fiber's post-callback logic should
* convert this into a SD_VARLINK_ERROR_SYSTEM reply. */
return -ENOSYS;
}
static int reply_fiber_errno(sd_varlink *link, sd_json_variant *parameters, const char *error_id, sd_varlink_reply_flags_t flags, void *userdata) {
ASSERT_STREQ(error_id, SD_VARLINK_ERROR_SYSTEM);
ASSERT_EQ(sd_json_variant_integer(sd_json_variant_by_key(parameters, "errno")), ENOSYS);
ASSERT_OK(sd_event_exit(sd_varlink_get_event(link), EXIT_SUCCESS));
return 0;
}
TEST(fiber_errno) {
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
ASSERT_OK(sd_event_default(&e));
_cleanup_(sd_varlink_server_unrefp) sd_varlink_server *s = NULL;
ASSERT_OK(sd_varlink_server_new(&s, 0));
ASSERT_OK(sd_varlink_server_attach_event(s, e, 0));
ASSERT_OK(varlink_server_bind_fiber(s, "io.test.FiberErrno", method_fiber_errno));
int connfd[2];
ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_NONBLOCK|SOCK_CLOEXEC, 0, connfd));
ASSERT_OK(sd_varlink_server_add_connection(s, connfd[0], /* ret= */ NULL));
_cleanup_(sd_varlink_unrefp) sd_varlink *c = NULL;
ASSERT_OK(sd_varlink_connect_fd(&c, connfd[1]));
ASSERT_OK(sd_varlink_attach_event(c, e, 0));
ASSERT_OK(sd_varlink_bind_reply(c, reply_fiber_errno));
ASSERT_OK(sd_varlink_invoke(c, "io.test.FiberErrno", /* parameters= */ NULL));
ASSERT_OK(sd_event_loop(e));
}
static int method_fiber_no_reply(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
/* Return success without replying and without stashing a ref. The fiber's post-callback
* logic should detect this and fail the connection. */
return 0;
}
static int reply_fiber_no_reply(sd_varlink *link, sd_json_variant *parameters, const char *error_id, sd_varlink_reply_flags_t flags, void *userdata) {
ASSERT_STREQ(error_id, SD_VARLINK_ERROR_DISCONNECTED);
ASSERT_OK(sd_event_exit(sd_varlink_get_event(link), EXIT_SUCCESS));
return 0;
}
TEST(fiber_no_reply) {
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
ASSERT_OK(sd_event_default(&e));
_cleanup_(sd_varlink_server_unrefp) sd_varlink_server *s = NULL;
ASSERT_OK(sd_varlink_server_new(&s, 0));
ASSERT_OK(sd_varlink_server_attach_event(s, e, 0));
ASSERT_OK(varlink_server_bind_fiber(s, "io.test.FiberNoReply", method_fiber_no_reply));
int connfd[2];
ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_NONBLOCK|SOCK_CLOEXEC, 0, connfd));
ASSERT_OK(sd_varlink_server_add_connection(s, connfd[0], /* ret= */ NULL));
_cleanup_(sd_varlink_unrefp) sd_varlink *c = NULL;
ASSERT_OK(sd_varlink_connect_fd(&c, connfd[1]));
ASSERT_OK(sd_varlink_attach_event(c, e, 0));
ASSERT_OK(sd_varlink_bind_reply(c, reply_fiber_no_reply));
ASSERT_OK(sd_varlink_invoke(c, "io.test.FiberNoReply", /* parameters= */ NULL));
ASSERT_OK(sd_event_loop(e));
}
static int fiber_stashed_deferred_reply(sd_event_source *s, void *userdata) {
_cleanup_(sd_varlink_unrefp) sd_varlink *link = ASSERT_PTR(userdata);
sd_event_source_disable_unref(s);
ASSERT_OK(sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_STRING("result", "stashed")));
return 0;
}
static int method_fiber_stash(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
/* Stash a ref on the connection so n_ref > 2 when the fiber returns, and reply later from a
* deferred event source. The fiber's post-callback logic should see the extra ref and treat
* this as a valid deferred-reply case instead of failing the connection. */
sd_event_source *source;
ASSERT_OK(sd_event_add_defer(sd_varlink_get_event(link), &source, fiber_stashed_deferred_reply, sd_varlink_ref(link)));
ASSERT_OK(sd_event_source_set_enabled(source, SD_EVENT_ONESHOT));
return 0;
}
static int reply_fiber_stash(sd_varlink *link, sd_json_variant *parameters, const char *error_id, sd_varlink_reply_flags_t flags, void *userdata) {
ASSERT_NULL(error_id);
ASSERT_STREQ(sd_json_variant_string(sd_json_variant_by_key(parameters, "result")), "stashed");
ASSERT_OK(sd_event_exit(sd_varlink_get_event(link), EXIT_SUCCESS));
return 0;
}
TEST(fiber_stash) {
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
ASSERT_OK(sd_event_default(&e));
_cleanup_(sd_varlink_server_unrefp) sd_varlink_server *s = NULL;
ASSERT_OK(sd_varlink_server_new(&s, 0));
ASSERT_OK(sd_varlink_server_attach_event(s, e, 0));
ASSERT_OK(varlink_server_bind_fiber(s, "io.test.FiberStash", method_fiber_stash));
int connfd[2];
ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_NONBLOCK|SOCK_CLOEXEC, 0, connfd));
ASSERT_OK(sd_varlink_server_add_connection(s, connfd[0], /* ret= */ NULL));
_cleanup_(sd_varlink_unrefp) sd_varlink *c = NULL;
ASSERT_OK(sd_varlink_connect_fd(&c, connfd[1]));
ASSERT_OK(sd_varlink_attach_event(c, e, 0));
ASSERT_OK(sd_varlink_bind_reply(c, reply_fiber_stash));
ASSERT_OK(sd_varlink_invoke(c, "io.test.FiberStash", /* parameters= */ NULL));
ASSERT_OK(sd_event_loop(e));
}
static int method_with_fd_sentinel(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
_cleanup_close_ int fd1 = -EBADF, fd2 = -EBADF;
@@ -768,8 +940,9 @@ static int method_upgrade(sd_varlink *link, sd_json_variant *parameters, sd_varl
if (r < 0)
return r;
/* After upgrade, do raw I/O: read until EOF, reverse, write back.
* The client shuts down its write side after sending, so we get a clean EOF. */
/* After upgrade, do raw I/O: read until the client shuts down its write side (giving us a clean
* EOF), reverse what we got, and write it back. Use suspending I/O so other fibers (the client)
* can make progress while we're waiting on the socket. */
char buf[64] = {};
ssize_t n = ASSERT_OK(loop_read(input_fd, buf, sizeof(buf) - 1, /* do_poll= */ true));
ASSERT_GT(n, 0);
@@ -789,12 +962,10 @@ static int method_upgrade_without_flag(sd_varlink *link, sd_json_variant *parame
/* Calling reply_and_upgrade without the client requesting it should fail with -EPROTO */
ASSERT_ERROR(sd_varlink_reply_and_upgrade(link, /* parameters= */ NULL, &input_fd, &output_fd), EPROTO);
sd_event_exit(sd_varlink_get_event(link), EXIT_SUCCESS);
return sd_varlink_reply(link, /* parameters= */ NULL);
}
static void *upgrade_thread(void *arg) {
static int upgrade_client_fiber(void *arg) {
_cleanup_(sd_varlink_flush_close_unrefp) sd_varlink *c = NULL;
_cleanup_close_ int input_fd = -EBADF, output_fd = -EBADF;
sd_json_variant *o = NULL;
@@ -827,14 +998,15 @@ static void *upgrade_thread(void *arg) {
ASSERT_OK(sd_varlink_call(c2, "io.test.UpgradeWithoutFlag", /* parameters= */ NULL, &o, &error_id));
ASSERT_NULL(error_id);
return NULL;
ASSERT_OK(sd_event_exit(sd_fiber_get_event(), EXIT_SUCCESS));
return 0;
}
TEST(upgrade) {
_cleanup_(sd_varlink_server_unrefp) sd_varlink_server *s = NULL;
_cleanup_(rm_rf_physical_and_freep) char *tmpdir = NULL;
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
pthread_t t;
_cleanup_(sd_future_unrefp) sd_future *f = NULL;
const char *sp;
ASSERT_OK(mkdtemp_malloc("/tmp/varlink-test-XXXXXX", &tmpdir));
@@ -844,31 +1016,23 @@ TEST(upgrade) {
ASSERT_OK(sd_varlink_server_new(&s, SD_VARLINK_SERVER_UPGRADABLE));
ASSERT_OK(sd_varlink_server_set_description(s, "upgrade-server"));
ASSERT_OK(sd_varlink_server_bind_method(s, "io.test.Upgrade", method_upgrade));
/* The method does raw I/O on the upgraded socket — bind it as a fiber method so it can
* suspend on loop_read()/loop_write() and the client fiber can make progress concurrently. */
ASSERT_OK(varlink_server_bind_fiber(s, "io.test.Upgrade", method_upgrade));
ASSERT_OK(sd_varlink_server_bind_method(s, "io.test.UpgradeWithoutFlag", method_upgrade_without_flag));
ASSERT_OK(sd_varlink_server_listen_address(s, sp, 0600));
ASSERT_OK(sd_varlink_server_attach_event(s, e, 0));
ASSERT_OK(-pthread_create(&t, NULL, upgrade_thread, (void*) sp));
ASSERT_OK(sd_fiber_new(e, "upgrade-client", upgrade_client_fiber, (void*) sp, /* destroy= */ NULL, &f));
/* Run the event loop until no more connections (the thread will disconnect when done) */
/* Run the event loop. Exits on idle once the client fiber completes and all server connections
* have been torn down. */
ASSERT_OK(sd_event_loop(e));
ASSERT_OK(-pthread_join(t, NULL));
ASSERT_OK(sd_future_result(f));
}
static int method_upgrade_and_exit(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
sd_event *event = ASSERT_PTR(userdata);
int r = method_upgrade(link, parameters, flags, /* userdata= */ NULL);
/* Exit the event loop after the upgrade is handled. We can't use sd_varlink_get_event()
* here because the connection is already disconnected after reply_and_upgrade. */
(void) sd_event_exit(event, r < 0 ? r : EXIT_SUCCESS);
return r;
}
static void *upgrade_pipelining_thread(void *arg) {
static int upgrade_pipelining_client_fiber(void *arg) {
union sockaddr_union sa = {};
_cleanup_close_ int fd = -EBADF;
@@ -895,8 +1059,8 @@ static void *upgrade_pipelining_thread(void *arg) {
/* Shut down write side so server's method_upgrade sees EOF after raw payload */
ASSERT_OK_ERRNO(shutdown(fd, SHUT_WR));
/* Read everything: upgrade reply (JSON + \0) + reversed raw payload. The server closes
* the connection after writing, so loop_read() reads until EOF and gets it all. */
/* Read everything: upgrade reply (JSON + \0) + reversed raw payload. The server closes the
* connection after writing, so loop_read() reads until EOF and gets it all. */
char buf[256] = {};
ssize_t n = ASSERT_OK(loop_read(fd, buf, sizeof(buf) - 1, /* do_poll= */ true));
ASSERT_GT(n, 0);
@@ -911,14 +1075,15 @@ static void *upgrade_pipelining_thread(void *arg) {
ASSERT_EQ(raw_size, strlen(raw_payload));
ASSERT_STREQ(strndupa_safe(raw, raw_size), "!denilepiP");
return NULL;
ASSERT_OK(sd_event_exit(sd_fiber_get_event(), EXIT_SUCCESS));
return 0;
}
TEST(upgrade_pipelining) {
_cleanup_(sd_varlink_server_unrefp) sd_varlink_server *s = NULL;
_cleanup_(rm_rf_physical_and_freep) char *tmpdir = NULL;
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
pthread_t t;
_cleanup_(sd_future_unrefp) sd_future *f = NULL;
const char *sp;
ASSERT_OK(mkdtemp_malloc("/tmp/varlink-test-XXXXXX", &tmpdir));
@@ -926,25 +1091,23 @@ TEST(upgrade_pipelining) {
ASSERT_OK(sd_event_new(&e));
ASSERT_OK(sd_varlink_server_new(&s, SD_VARLINK_SERVER_UPGRADABLE|SD_VARLINK_SERVER_INHERIT_USERDATA));
ASSERT_OK(sd_varlink_server_new(&s, SD_VARLINK_SERVER_UPGRADABLE));
ASSERT_OK(sd_varlink_server_set_description(s, "upgrade-pipelining-server"));
ASSERT_OK(sd_varlink_server_bind_method(s, "io.test.Upgrade", method_upgrade_and_exit));
/* method_upgrade does raw I/O on the upgraded socket, so bind as a fiber method. */
ASSERT_OK(varlink_server_bind_fiber(s, "io.test.Upgrade", method_upgrade));
ASSERT_OK(sd_varlink_server_listen_address(s, sp, 0600));
ASSERT_OK(sd_varlink_server_attach_event(s, e, 0));
sd_varlink_server_set_userdata(s, e);
ASSERT_OK(-pthread_create(&t, NULL, upgrade_pipelining_thread, (void*) sp));
ASSERT_OK(sd_fiber_new(e, "upgrade-pipelining-client", upgrade_pipelining_client_fiber, (void*) sp, /* destroy= */ NULL, &f));
ASSERT_OK(sd_event_loop(e));
ASSERT_OK(-pthread_join(t, NULL));
ASSERT_OK(sd_future_result(f));
}
typedef struct ExecDirServer {
sd_varlink_server *server;
sd_event *event;
const char *name;
pthread_t thread;
} ExecDirServer;
static int method_execute_dir_ping(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
@@ -953,20 +1116,6 @@ static int method_execute_dir_ping(sd_varlink *link, sd_json_variant *parameters
return sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_STRING("name", srv->name));
}
static void on_execute_dir_disconnect(sd_varlink_server *s, sd_varlink *link, void *userdata) {
ExecDirServer *srv = ASSERT_PTR(userdata);
/* Only one client (from varlink_execute_directory()) connects per server — once it's gone, we're done. */
ASSERT_OK(sd_event_exit(srv->event, 0));
}
static void *execute_dir_server_thread(void *arg) {
ExecDirServer *srv = arg;
ASSERT_OK(sd_event_loop(srv->event));
return NULL;
}
static int execute_dir_reply(sd_varlink *link, sd_json_variant *parameters, const char *error_id, sd_varlink_reply_flags_t flags, void *userdata) {
size_t *count = ASSERT_PTR(userdata);
@@ -977,51 +1126,28 @@ static int execute_dir_reply(sd_varlink *link, sd_json_variant *parameters, cons
return 0;
}
TEST(execute_directory) {
_cleanup_(rm_rf_physical_and_freep) char *tmpdir = NULL;
static const char * const names[] = { "alpha", "beta", "gamma" };
ExecDirServer servers[ELEMENTSOF(names)] = {};
size_t reply_count = 0;
typedef struct ExecDirClientArgs {
const char *tmpdir;
size_t n_servers;
size_t *reply_count;
} ExecDirClientArgs;
ASSERT_OK(mkdtemp_malloc("/tmp/varlink-execdir-XXXXXX", &tmpdir));
for (size_t i = 0; i < ELEMENTSOF(names); i++) {
ExecDirServer *eds = servers + i;
servers[i].name = names[i];
_cleanup_free_ char *j = ASSERT_PTR(path_join(tmpdir, names[i]));
ASSERT_OK(sd_event_new(&eds->event));
ASSERT_OK(varlink_server_new(&eds->server,
SD_VARLINK_SERVER_INHERIT_USERDATA,
eds));
ASSERT_OK(sd_varlink_server_bind_method(eds->server, "io.test.ExecDirPing", method_execute_dir_ping));
ASSERT_OK(sd_varlink_server_bind_disconnect(eds->server, on_execute_dir_disconnect));
ASSERT_OK(sd_varlink_server_listen_address(eds->server, j, 0600));
ASSERT_OK(sd_varlink_server_attach_event(eds->server, eds->event, 0));
ASSERT_OK(-pthread_create(&eds->thread, NULL, execute_dir_server_thread, eds));
}
static int execute_dir_client_fiber(void *arg) {
ExecDirClientArgs *a = ASSERT_PTR(arg);
ASSERT_OK_EQ(varlink_execute_directory(
tmpdir,
a->tmpdir,
"io.test.ExecDirPing",
/* parameters= */ NULL,
/* more= */ false,
/* timeout_usec= */ USEC_INFINITY,
execute_dir_reply,
&reply_count), (ssize_t) ELEMENTSOF(names));
ASSERT_EQ(reply_count, ELEMENTSOF(names));
FOREACH_ELEMENT(eds, servers) {
ASSERT_OK(-pthread_join(eds->thread, NULL));
eds->server = sd_varlink_server_unref(eds->server);
eds->event = sd_event_unref(eds->event);
}
a->reply_count), (ssize_t) a->n_servers);
ASSERT_EQ(*a->reply_count, a->n_servers);
/* Calling the helper against a non-existent directory must fail. */
_cleanup_free_ char *nope = NULL;
ASSERT_OK(asprintf(&nope, "%s/does-not-exist", tmpdir));
ASSERT_OK(asprintf(&nope, "%s/does-not-exist", a->tmpdir));
ASSERT_FAIL(varlink_execute_directory(
nope,
"io.test.ExecDirPing",
@@ -1029,13 +1155,13 @@ TEST(execute_directory) {
/* more= */ false,
/* timeout_usec= */ USEC_INFINITY,
execute_dir_reply,
&reply_count));
a->reply_count));
/* An empty directory must simply return 0 and not invoke the reply callback. */
_cleanup_free_ char *empty = ASSERT_PTR(path_join(tmpdir, "empty"));
_cleanup_free_ char *empty = ASSERT_PTR(path_join(a->tmpdir, "empty"));
ASSERT_OK_ERRNO(mkdir(empty, 0755));
size_t count_before = reply_count;
size_t count_before = *a->reply_count;
ASSERT_OK_ZERO(varlink_execute_directory(
empty,
"io.test.ExecDirPing",
@@ -1043,8 +1169,52 @@ TEST(execute_directory) {
/* more= */ false,
/* timeout_usec= */ USEC_INFINITY,
execute_dir_reply,
&reply_count));
ASSERT_EQ(reply_count, count_before);
a->reply_count));
ASSERT_EQ(*a->reply_count, count_before);
ASSERT_OK(sd_event_exit(sd_fiber_get_event(), EXIT_SUCCESS));
return 0;
}
TEST(execute_directory) {
_cleanup_(rm_rf_physical_and_freep) char *tmpdir = NULL;
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
_cleanup_(sd_future_unrefp) sd_future *f = NULL;
static const char * const names[] = { "alpha", "beta", "gamma" };
ExecDirServer servers[ELEMENTSOF(names)] = {};
size_t reply_count = 0;
ASSERT_OK(mkdtemp_malloc("/tmp/varlink-execdir-XXXXXX", &tmpdir));
ASSERT_OK(sd_event_new(&e));
for (size_t i = 0; i < ELEMENTSOF(names); i++) {
ExecDirServer *eds = servers + i;
servers[i].name = names[i];
_cleanup_free_ char *j = ASSERT_PTR(path_join(tmpdir, names[i]));
ASSERT_OK(varlink_server_new(&eds->server,
SD_VARLINK_SERVER_INHERIT_USERDATA,
eds));
ASSERT_OK(sd_varlink_server_bind_method(eds->server, "io.test.ExecDirPing", method_execute_dir_ping));
ASSERT_OK(sd_varlink_server_listen_address(eds->server, j, 0600));
ASSERT_OK(sd_varlink_server_attach_event(eds->server, e, 0));
}
ExecDirClientArgs args = {
.tmpdir = tmpdir,
.n_servers = ELEMENTSOF(names),
.reply_count = &reply_count,
};
ASSERT_OK(sd_fiber_new(e, "execute-dir-client", execute_dir_client_fiber, &args, NULL, &f));
ASSERT_OK(sd_event_loop(e));
ASSERT_OK(sd_future_result(f));
FOREACH_ELEMENT(eds, servers)
eds->server = sd_varlink_server_unref(eds->server);
}
#define CTRUNC_N_FDS 64U
@@ -1075,7 +1245,7 @@ static int reply_ctrunc(sd_varlink *link, sd_json_variant *parameters, const cha
TEST(ctrunc) {
int r;
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
ASSERT_OK(sd_event_default(&e));
+2 -1
View File
@@ -135,7 +135,8 @@ typedef struct sd_varlink_server {
LIST_HEAD(VarlinkServerSocket, sockets);
Hashmap *methods; /* Fully qualified symbol name of a method → VarlinkMethod */
Hashmap *methods; /* Fully qualified symbol name of a method → sd_varlink_method_t */
Hashmap *fiber_methods; /* Fully qualified symbol name of a fiber method → sd_varlink_method_t */
Hashmap *interfaces; /* Fully qualified interface name → VarlinkInterface* */
Hashmap *symbols; /* Fully qualified symbol name of method/error → VarlinkSymbol* */
sd_varlink_connect_t connect_callback;
+4
View File
@@ -19,6 +19,10 @@ int varlink_many_notifyb(Set *s, ...);
int varlink_many_reply(Set *s, sd_json_variant *parameters);
int varlink_many_error(Set *s, const char *error_id, sd_json_variant *parameters);
int varlink_server_bind_fiber(sd_varlink_server *s, const char *method, sd_varlink_method_t callback);
int varlink_server_bind_fiber_many_internal(sd_varlink_server *s, ...);
#define varlink_server_bind_fiber_many(s, ...) varlink_server_bind_fiber_many_internal(s, __VA_ARGS__, NULL)
int varlink_set_info_systemd(sd_varlink_server *server);
int varlink_server_new(
+210 -23
View File
@@ -1,6 +1,7 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "sd-event.h"
#include "sd-future.h"
#include "sd-json.h"
#include "alloc-util.h"
@@ -226,19 +227,23 @@ static int qmp_extract_response_id(sd_json_variant *v, uint64_t *ret) {
return 1;
}
/* Returns 0 on success (ret_result = "return" value), -EIO on QMP error (reterr_desc set). */
static int qmp_parse_response(sd_json_variant *v, sd_json_variant **ret_result, const char **reterr_desc) {
/* Returns 0 on success (ret_result = freshly reffed "return" value), -EIO on QMP error
* (ret_error_desc set to a freshly allocated string). Caller owns both outputs. */
static int qmp_parse_response(sd_json_variant *v, sd_json_variant **ret_result, char **reterr_error_desc) {
const char *desc;
desc = qmp_extract_error_description(v);
if (desc) {
if (reterr_desc)
*reterr_desc = desc;
if (reterr_error_desc) {
*reterr_error_desc = strdup(desc);
if (!*reterr_error_desc)
return -ENOMEM;
}
return -EIO;
}
if (ret_result)
*ret_result = sd_json_variant_by_key(v, "return");
*ret_result = sd_json_variant_ref(sd_json_variant_by_key(v, "return"));
return 0;
}
@@ -273,8 +278,8 @@ static int qmp_client_build_command(
/* Route c->current to event callback or matching async slot. Returns 1 on dispatch. */
static int qmp_client_dispatch(QmpClient *c) {
sd_json_variant *result = NULL;
const char *desc = NULL;
_cleanup_(sd_json_variant_unrefp) sd_json_variant *result = NULL;
_cleanup_free_ char *desc = NULL;
uint64_t id;
int error, r;
@@ -318,8 +323,8 @@ static int qmp_client_dispatch(QmpClient *c) {
}
/* Synchronous slot (no callback): leave c->current pinned so qmp_client_call() can
* pick up the reply and hand out borrowed pointers into it. The sync caller owns a
* ref on the slot and detects completion by observing slot->client turning NULL. */
* pick the reply up after its pump loop. The sync caller owns a ref on the slot and
* detects completion by observing slot->client turning NULL. */
if (!slot->callback) {
qmp_slot_disconnect(slot, /* unref= */ true);
return 1;
@@ -574,6 +579,10 @@ static void qmp_client_clear(QmpClient *c) {
qmp_client_detach_event(c);
qmp_client_clear_current(c);
json_stream_done(&c->stream);
/* qmp_client_handle_disconnect() above drained every entry via qmp_client_fail_pending();
* the set is borrow-only for non-floating slots, so set_free() can't safely run a
* destructor over leftovers — enforce the drain invariant instead. */
assert(set_isempty(c->slots));
c->slots = set_free(c->slots);
}
@@ -745,7 +754,7 @@ DEFINE_TRIVIAL_CLEANUP_FUNC(QmpClientArgs*, qmp_client_args_close_fds);
/* Shared send path for qmp_client_invoke() and qmp_client_call(). A NULL callback registers
* a "synchronous" slot: dispatch_reply leaves c->current pinned on match instead of invoking
* a callback, so qmp_client_call() can hand out borrowed pointers into the reply. If ret_slot
* a callback, so qmp_client_call() can pick the reply up after its pump loop. If ret_slot
* is NULL the slot is allocated as floating (owned by c->slots); otherwise a reference is
* handed back to the caller. */
static int qmp_client_send(
@@ -810,21 +819,193 @@ int qmp_client_invoke(
return qmp_client_send(c, command, args, callback, userdata, ret_slot);
}
typedef struct QmpFuture {
QmpSlot *slot; /* owned, non-floating; NULL once disconnected */
sd_json_variant *result;
char *error_desc;
} QmpFuture;
static void* qmp_future_alloc(void) {
return new0(QmpFuture, 1);
}
static void qmp_future_free(sd_future *f) {
QmpFuture *qf = ASSERT_PTR(sd_future_get_private(ASSERT_PTR(f)));
qmp_slot_unref(qf->slot);
sd_json_variant_unref(qf->result);
free(qf->error_desc);
free(qf);
}
static int qmp_future_cancel(sd_future *f) {
QmpFuture *qf = ASSERT_PTR(sd_future_get_private(ASSERT_PTR(f)));
/* Drop the pending slot so dispatch_reply won't try to fire our callback (and touch
* freed memory) when the reply eventually arrives. */
qf->slot = qmp_slot_unref(qf->slot);
return sd_future_resolve(f, -ECANCELED);
}
static const sd_future_ops qmp_call_future_ops = {
.size = sizeof(sd_future_ops),
.alloc = qmp_future_alloc,
.free = qmp_future_free,
.cancel = qmp_future_cancel,
};
static int qmp_future_callback(
QmpClient *c,
sd_json_variant *result,
const char *desc,
int error,
void *userdata) {
sd_future *f = ASSERT_PTR(userdata);
QmpFuture *qf = ASSERT_PTR(sd_future_get_private(f));
int r;
assert(result || desc || error);
if (result)
qf->result = sd_json_variant_ref(result);
if (desc) {
qf->error_desc = strdup(desc);
if (!qf->error_desc)
/* No usable reply payload to surface — propagate as transport-style
* failure so suspend() / sd_future_result() see the OOM. */
return sd_future_resolve(f, -ENOMEM);
}
/* Resolve with 0 on a success reply and -EIO on a QMP-level error (matching the synchronous
* path's errno for the desc-without-ret_error_desc case), so a caller awaiting the future
* learns about call failures from the resolution value alone. The reply payload itself
* (result or error_desc) is always stashed on the QmpFuture so future_get_qmp_reply() can
* hand the description string back on top of the bare -EIO. With no reply at all (transport
* failure, disconnect), resolve with the propagated transport errno; cancellation surfaces
* as -ECANCELED via qmp_future_cancel(). */
if (result)
r = 0;
else if (desc)
r = -EIO;
else
r = error;
return sd_future_resolve(f, r);
}
int qmp_client_call_future(
QmpClient *c,
const char *command,
QmpClientArgs *args,
sd_future **ret) {
int r;
assert(c);
assert(command);
assert(ret);
_cleanup_(sd_future_unrefp) sd_future *f = NULL;
r = sd_future_new(&qmp_call_future_ops, &f);
if (r < 0)
return r;
QmpFuture *qf = sd_future_get_private(f);
r = qmp_client_send(c, command, args, qmp_future_callback, f, &qf->slot);
if (r < 0)
return r;
*ret = TAKE_PTR(f);
return 0;
}
/* Extract the reply from a resolved qmp_client_call_future(). Returns 1 on success (with
* *ret_result a fresh reference the caller unrefs), -EIO on a QMP-level error (with the detail
* description copied into *ret_error_desc when the caller passed one to receive it), and the
* future's negative resume errno when no reply landed at all (transport failure / cancellation).
*/
int future_get_qmp_reply(sd_future *f, sd_json_variant **ret_result, char **reterr_error_desc) {
assert(f);
assert(sd_future_get_ops(f) == &qmp_call_future_ops);
assert(sd_future_state(f) == SD_FUTURE_RESOLVED);
QmpFuture *qf = ASSERT_PTR(sd_future_get_private(f));
/* No reply at all: transport failure or cancellation — surface the future result. */
if (!qf->result && !qf->error_desc)
return sd_future_result(f);
if (qf->error_desc) {
if (reterr_error_desc) {
char *desc = strdup(qf->error_desc);
if (!desc)
return -ENOMEM;
*reterr_error_desc = desc;
}
return -EIO;
}
if (reterr_error_desc)
*reterr_error_desc = NULL;
if (ret_result)
*ret_result = sd_json_variant_ref(qf->result);
return 1;
}
static int qmp_client_call_suspend(
QmpClient *c,
const char *command,
QmpClientArgs *args,
sd_json_variant **ret_result,
char **ret_error_desc) {
int r;
assert(c);
assert(command);
assert(sd_fiber_is_running());
_cleanup_(sd_future_cancel_wait_unrefp) sd_future *call = NULL;
r = qmp_client_call_future(c, command, args, &call);
if (r < 0)
return r;
r = sd_fiber_suspend();
/* If the future isn't resolved, the suspend was interrupted before a reply arrived (fiber
* cancelled, fiber-wide SD_FIBER_TIMEOUT scope expired, …). There's no reply to extract,
* so surface the resume error directly. When the future is resolved, future_get_qmp_reply()
* already encodes success (1), QMP-level error (-EIO with the desc captured if asked for),
* and no-reply (negative future result) — pass it through. */
if (sd_future_state(call) != SD_FUTURE_RESOLVED)
return r;
return future_get_qmp_reply(call, ret_result, ret_error_desc);
}
int qmp_client_call(
QmpClient *c,
const char *command,
QmpClientArgs *args,
sd_json_variant **ret_result,
const char **ret_error_desc) {
char **reterr_error_desc) {
_cleanup_(qmp_slot_unrefp) QmpSlot *slot = NULL;
_cleanup_(sd_json_variant_unrefp) sd_json_variant *result = NULL;
_cleanup_free_ char *desc = NULL;
int r;
assert_return(c, -EINVAL);
assert_return(command, -EINVAL);
/* Drop any reply pinned by a previous qmp_client_call() before we pin a new one. */
qmp_client_clear_current(c);
/* If we're on a fiber sharing the QMP client's event loop, use the async + suspend path so
* multiple concurrent qmp_client_call() invocations across fibers don't deadlock each other
* on the process+wait pump. */
if (sd_fiber_is_running() && qmp_client_get_event(c) == sd_fiber_get_event())
return qmp_client_call_suspend(c, command, args, ret_result, reterr_error_desc);
_cleanup_(qmp_slot_unrefp) QmpSlot *slot = NULL;
/* NULL callback marks this as a synchronous slot: dispatch_reply matches on id like
* any other slot (so stray unknown-id replies still get logged and dropped), but
@@ -855,18 +1036,24 @@ int qmp_client_call(
return r;
}
sd_json_variant *result = NULL;
const char *desc = NULL;
int error = qmp_parse_response(c->current, &result, &desc);
_cleanup_(sd_json_variant_unrefp) sd_json_variant *current = TAKE_PTR(c->current);
r = qmp_parse_response(current, &result, &desc);
if (r < 0 && r != -EIO)
return r;
/* If caller doesn't ask for the error string, surface the error as the return code. */
if (!ret_error_desc && error < 0)
return error;
/* QMP-level error: copy the description into *ret_error_desc when the caller asked for it,
* and surface the failure via the return value (matching qmp_client_call_suspend() /
* sd_bus_call()'s "negative on error" convention). */
if (desc) {
if (reterr_error_desc)
*reterr_error_desc = TAKE_PTR(desc);
return -EIO;
}
if (ret_result)
*ret_result = result;
if (ret_error_desc)
*ret_error_desc = desc;
*ret_result = TAKE_PTR(result);
if (reterr_error_desc)
*reterr_error_desc = NULL;
return 1;
}
+12 -4
View File
@@ -68,15 +68,23 @@ int qmp_client_invoke(
qmp_command_callback_t callback,
void *userdata);
/* Synchronous send + receive. Pumps the event loop until the reply arrives. *ret_result and
* *ret_error_desc are borrowed pointers into the last reply, valid until the next
* qmp_client_call(). Same contract as sd_varlink_call(). */
int qmp_client_call(
QmpClient *client,
const char *command,
QmpClientArgs *args,
sd_json_variant **ret_result,
const char **ret_error_desc);
char **reterr_error_desc);
int qmp_client_call_future(
QmpClient *client,
const char *command,
QmpClientArgs *args,
sd_future **ret);
int future_get_qmp_reply(
sd_future *f,
sd_json_variant **ret_result,
char **reterr_error_desc);
void qmp_client_bind_event(QmpClient *c, qmp_event_callback_t callback, void *userdata);
void qmp_client_bind_disconnect(QmpClient *c, qmp_disconnect_callback_t callback, void *userdata);
+14
View File
@@ -69,6 +69,20 @@ typedef void (*_sd_destroy_t)(void *userdata);
# define _SD_STRINGIFY(x) _SD_XSTRINGIFY(x)
#endif
/* Mirror of CONCATENATE / UNIQ from macro-fundamental.h, available to public sd-* headers. */
#ifndef _SD_CONCATENATE
# define _SD_XCONCATENATE(x, y) x ## y
# define _SD_CONCATENATE(x, y) _SD_XCONCATENATE(x, y)
#endif
#ifndef _SD_UNIQ
# ifdef __COUNTER__
# define _SD_UNIQ __COUNTER__
# else
# define _SD_UNIQ __LINE__
# endif
#endif
#ifndef _SD_BEGIN_DECLARATIONS
# ifdef __cplusplus
# define _SD_BEGIN_DECLARATIONS \
+1
View File
@@ -36,6 +36,7 @@ _not_installed_headers = [
'sd-dhcp6-option.h',
'sd-dhcp6-protocol.h',
'sd-dns-resolver.h',
'sd-future.h',
'sd-ipv4acd.h',
'sd-ipv4ll.h',
'sd-lldp-rx.h',
+1
View File
@@ -44,6 +44,7 @@ __extension__ enum {
SD_BUS_VTABLE_PROPERTY_EXPLICIT = 1ULL << 7,
SD_BUS_VTABLE_SENSITIVE = 1ULL << 8, /* covers both directions: method call + reply */
SD_BUS_VTABLE_ABSOLUTE_OFFSET = 1ULL << 9,
/* Bit 10 is reserved for the private SD_BUS_VTABLE_METHOD_FIBER flag (see bus-internal.h). */
_SD_BUS_VTABLE_CAPABILITY_MASK = 0xFFFFULL << 40
};
+126
View File
@@ -0,0 +1,126 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#ifndef foosdfuturefoo
#define foosdfuturefoo
/***
systemd is free software; you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
systemd is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with systemd; If not, see <https://www.gnu.org/licenses/>.
***/
#include <sys/socket.h>
#include "_sd-common.h"
_SD_BEGIN_DECLARATIONS;
struct iovec;
struct pollfd;
struct sockaddr;
struct msghdr;
struct timespec;
typedef struct sd_event sd_event;
typedef struct sd_future sd_future;
typedef struct sd_future_ops sd_future_ops;
typedef int (*sd_future_func_t)(sd_future *f);
typedef int (*sd_fiber_func_t)(void *userdata);
typedef _sd_destroy_t sd_fiber_destroy_t;
struct sd_future_ops {
size_t size;
void* (*alloc)(void);
void (*free)(sd_future *f);
int (*cancel)(sd_future *f);
int (*set_priority)(sd_future *f, int64_t priority);
};
__extension__ typedef enum _SD_ENUM_TYPE_S64(sd_future_state_t) {
SD_FUTURE_PENDING,
SD_FUTURE_RESOLVED,
_SD_ENUM_FORCE_S64(SD_FUTURE_STATE)
} sd_future_state_t;
int sd_future_new(const sd_future_ops *ops, sd_future **ret);
int sd_future_cancel(sd_future *f);
int sd_future_resolve(sd_future *f, int result);
_SD_DECLARE_TRIVIAL_REF_UNREF_FUNC(sd_future);
_SD_DEFINE_POINTER_CLEANUP_FUNC(sd_future, sd_future_unref);
void sd_future_unref_array_clear(sd_future *array[], size_t n);
void sd_future_unref_array(sd_future *array[], size_t n);
sd_future* sd_future_cancel_wait_unref(sd_future *f);
_SD_DEFINE_POINTER_CLEANUP_FUNC(sd_future, sd_future_cancel_wait_unref);
void sd_future_cancel_wait_unref_array_clear(sd_future *array[], size_t n);
void sd_future_cancel_wait_unref_array(sd_future *array[], size_t n);
int sd_future_state(sd_future *f);
int sd_future_result(sd_future *f);
void* sd_future_get_userdata(sd_future *f);
void* sd_future_get_private(sd_future *f);
const sd_future_ops* sd_future_get_ops(sd_future *f);
int sd_future_set_callback(sd_future *f, sd_future_func_t callback, void *userdata);
int sd_future_set_priority(sd_future *f, int64_t priority);
int sd_future_new_wait(sd_future *target, sd_future **ret);
int sd_fiber_new(sd_event *e, const char *name, sd_fiber_func_t func, void *userdata, sd_fiber_destroy_t destroy, sd_future **ret);
int sd_fiber_set_floating(sd_future *f, int b);
int sd_fiber_get_floating(sd_future *f);
int sd_fiber_is_running(void);
sd_future* sd_fiber_get_current(void);
int sd_fiber_get_priority(int64_t *ret);
sd_event* sd_fiber_get_event(void);
int sd_fiber_yield(void);
int sd_fiber_sleep(uint64_t usec);
int sd_fiber_await(sd_future *target);
int sd_fiber_suspend(void);
int sd_fiber_resume(sd_future *f, int result);
sd_future* sd_fiber_timeout(uint64_t timeout);
#define SD_FIBER_TIMEOUT(timeout) _SD_FIBER_TIMEOUT(_SD_UNIQ, (timeout))
#define _SD_FIBER_TIMEOUT(uniq, timeout) \
sd_future *_SD_CONCATENATE(_sd_fto_, uniq) __attribute__((cleanup(sd_future_cancel_wait_unrefp), unused)) = sd_fiber_timeout(timeout)
#define SD_FIBER_WITH_TIMEOUT(timeout) _SD_FIBER_WITH_TIMEOUT(_SD_UNIQ, (timeout))
#define _SD_FIBER_WITH_TIMEOUT(uniq, timeout) \
for (sd_future *_SD_CONCATENATE(_sd_fto_, uniq) __attribute__((cleanup(sd_future_cancel_wait_unrefp), unused)) = sd_fiber_timeout(timeout), \
*_SD_CONCATENATE(_sd_fto_b_, uniq) = (sd_future*) (uintptr_t) 1; \
_SD_CONCATENATE(_sd_fto_b_, uniq); \
_SD_CONCATENATE(_sd_fto_b_, uniq) = NULL)
/* Fiber I/O operations - use sd-event for non-blocking I/O when in fiber context */
ssize_t sd_fiber_read(int fd, void *buf, size_t count);
ssize_t sd_fiber_write(int fd, const void *buf, size_t count);
ssize_t sd_fiber_readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t sd_fiber_writev(int fd, const struct iovec *iov, int iovcnt);
ssize_t sd_fiber_recv(int sockfd, void *buf, size_t len, int flags);
ssize_t sd_fiber_send(int sockfd, const void *buf, size_t len, int flags);
int sd_fiber_connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
ssize_t sd_fiber_recvmsg(int sockfd, struct msghdr *msg, int flags);
ssize_t sd_fiber_sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t sd_fiber_recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sd_fiber_sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
int sd_fiber_accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);
#ifndef __STRICT_ANSI__
int sd_fiber_ppoll(struct pollfd *fds, size_t n_fds, const struct timespec *timeout, const sigset_t *sigmask);
#endif
_SD_END_DECLARATIONS;
#endif
+348 -601
View File
File diff suppressed because it is too large Load Diff
@@ -3,7 +3,7 @@
integration_tests += [
integration_test_template + {
'name' : fs.name(meson.current_source_dir()),
'coredump-exclude-regex' : '/(bash|python3.[0-9]+|systemd-executor)$',
'coredump-exclude-regex' : '/(bash|python3.[0-9]+|systemd-executor|test-fiber)$',
'cmdline' : integration_test_template['cmdline'] + [
'''
+1
View File
@@ -28,6 +28,7 @@ LIBC_LIB_PREFIXES: tuple[str, ...] = (
'libm.so.',
'libresolv.so.',
'libc.musl-',
'libucontext.',
)
# GCC runtime support libraries (stack unwinding, soft-float helpers, C++ standard library). The