xbot/irc_coroutine.hpp

279 lines
7.4 KiB
C++
Raw Normal View History

2025-01-23 12:46:52 -08:00
#pragma once
#include "connection.hpp"
2025-01-24 14:48:15 -08:00
#include <chrono>
2025-01-23 12:46:52 -08:00
#include <coroutine>
2025-01-24 14:48:15 -08:00
#include <initializer_list>
#include <utility>
2025-01-23 12:46:52 -08:00
#include <vector>
struct irc_promise;
2025-01-24 14:48:15 -08:00
/// A coroutine that can co_await on various IRC events
2025-01-25 15:45:31 -08:00
struct irc_coroutine : std::coroutine_handle<irc_promise>
{
2025-01-23 12:46:52 -08:00
using promise_type = irc_promise;
2025-01-24 14:48:15 -08:00
/// Start the coroutine and associate it with a specific connection.
2025-01-23 12:46:52 -08:00
auto start(Connection &connection) -> void;
2025-01-24 14:48:15 -08:00
/// Returns true when this coroutine is still waiting on events
2025-01-23 12:46:52 -08:00
auto is_running() -> bool;
2025-01-24 14:48:15 -08:00
/// Returns the exception that terminated this coroutine, if there is one.
2025-01-23 12:46:52 -08:00
auto exception() -> std::exception_ptr;
};
struct irc_promise
{
// Pointer to the connection while running. Cleared on termination.
std::shared_ptr<Connection> connection_;
// Pointer to exception that terminated this coroutine if there is one.
std::exception_ptr exception_;
2025-01-24 14:48:15 -08:00
auto get_return_object() -> irc_coroutine
2025-01-23 12:46:52 -08:00
{
return {irc_coroutine::from_promise(*this)};
}
// Suspend waiting for start() to initialize connection_
2025-01-24 14:48:15 -08:00
auto initial_suspend() noexcept -> std::suspend_always { return {}; }
2025-01-23 12:46:52 -08:00
// Suspend so that is_running() and exception() work
2025-01-24 14:48:15 -08:00
auto final_suspend() noexcept -> std::suspend_always { return {}; }
2025-01-23 12:46:52 -08:00
// Normal termination
2025-01-25 15:45:31 -08:00
auto return_void() -> void
{
2025-01-23 12:46:52 -08:00
connection_.reset();
}
// Abnormal termination - remember the exception
2025-01-25 15:45:31 -08:00
auto unhandled_exception() -> void
{
2025-01-23 12:46:52 -08:00
connection_.reset();
exception_ = std::current_exception();
}
};
2025-01-25 15:45:31 -08:00
template <typename... Ts>
2025-01-24 14:48:15 -08:00
class Wait;
/// Argument to a Wait that expects one or more IRC messages
2025-01-25 15:45:31 -08:00
class wait_ircmsg
{
2025-01-24 14:48:15 -08:00
// Vector of commands this wait is expecting. Leave empty to accept all messages.
2025-01-23 12:46:52 -08:00
std::vector<IrcCommand> want_cmds_;
2025-01-24 14:48:15 -08:00
// Slot for the ircmsg event
boost::signals2::scoped_connection ircmsg_slot_;
public:
using result_type = std::pair<IrcCommand, const IrcMsg &>;
2025-01-25 15:45:31 -08:00
wait_ircmsg(std::initializer_list<IrcCommand> want_cmds)
: want_cmds_{want_cmds}
{
}
2025-01-24 14:48:15 -08:00
2025-01-25 15:45:31 -08:00
template <size_t I, typename... Ts>
auto start(Wait<Ts...> &command) -> void;
2025-01-28 20:01:51 -08:00
auto stop() -> void { ircmsg_slot_.disconnect(); }
};
class wait_snote
{
// Vector of tags this wait is expecting. Leave empty to accept all messages.
std::vector<SnoteTag> want_tags_;
// Slot for the snote event
boost::signals2::scoped_connection snote_slot_;
public:
using result_type = SnoteMatch;
wait_snote(std::initializer_list<SnoteTag> want_tags)
: want_tags_{want_tags}
{
}
template <size_t I, typename... Ts>
auto start(Wait<Ts...> &command) -> void;
auto stop() -> void { snote_slot_.disconnect(); }
2025-01-23 12:46:52 -08:00
};
2025-01-25 15:45:31 -08:00
class wait_timeout
{
2025-01-24 14:48:15 -08:00
std::optional<boost::asio::steady_timer> timer_;
std::chrono::milliseconds timeout_;
public:
2025-01-25 15:45:31 -08:00
struct result_type
{
};
wait_timeout(std::chrono::milliseconds timeout)
: timeout_{timeout}
{
}
2025-01-24 14:48:15 -08:00
2025-01-25 15:45:31 -08:00
template <size_t I, typename... Ts>
auto start(Wait<Ts...> &command) -> void;
2025-01-28 20:01:51 -08:00
auto stop() -> void { timer_->cancel(); }
2025-01-23 12:46:52 -08:00
};
2025-01-25 15:45:31 -08:00
template <typename... Ts>
class Wait
{
2025-01-23 12:46:52 -08:00
2025-01-24 14:48:15 -08:00
// State associated with each wait mode
std::tuple<Ts...> modes_;
// Result from any one of the wait modes
std::optional<std::variant<typename Ts::result_type...>> result_;
// Handle of the continuation to be resumed when one of the wait
// modes is ready.
std::coroutine_handle<irc_promise> handle_;
2025-01-23 12:46:52 -08:00
2025-01-24 14:48:15 -08:00
// Slot for tearing down the irc_coroutine in case the connection
// fails before any wait modes complete.
boost::signals2::scoped_connection disconnect_slot_;
2025-01-23 12:46:52 -08:00
2025-01-24 14:48:15 -08:00
template <size_t I>
2025-01-25 15:45:31 -08:00
auto start_mode() -> void
{
2025-01-24 14:48:15 -08:00
std::get<I>(modes_).template start<I, Ts...>(*this);
}
template <std::size_t... Indices>
2025-01-25 15:45:31 -08:00
auto start_modes(std::index_sequence<Indices...>) -> void
{
2025-01-24 14:48:15 -08:00
(start_mode<Indices>(), ...);
}
2025-01-23 12:46:52 -08:00
2025-01-24 14:48:15 -08:00
template <std::size_t... Indices>
2025-01-25 15:45:31 -08:00
auto stop_modes(std::index_sequence<Indices...>) -> void
{
2025-01-24 14:48:15 -08:00
(std::get<Indices>(modes_).stop(), ...);
2025-01-23 12:46:52 -08:00
}
public:
2025-01-25 15:45:31 -08:00
Wait(Ts &&...modes)
: modes_{std::forward<Ts>(modes)...}
{
}
2025-01-23 12:46:52 -08:00
2025-01-24 14:48:15 -08:00
// Get the connection that this coroutine was started with.
2025-01-25 15:45:31 -08:00
auto get_connection() const -> Connection &
{
2025-01-24 14:48:15 -08:00
return *handle_.promise().connection_;
}
// Store a successful result and resume the coroutine
template <size_t I, typename... Args>
2025-01-25 15:45:31 -08:00
auto complete(Args &&...args) -> void
{
2025-01-24 14:48:15 -08:00
result_.emplace(std::in_place_index<I>, std::forward<Args>(args)...);
handle_.resume();
}
// The coroutine always needs to wait for a message. It will never
// be ready immediately.
auto await_ready() noexcept -> bool { return false; }
2025-01-23 12:46:52 -08:00
/// Install event handles in the connection that will resume this coroutine.
2025-01-24 14:48:15 -08:00
auto await_suspend(std::coroutine_handle<irc_promise> handle) -> void;
2025-01-23 12:46:52 -08:00
2025-01-24 14:48:15 -08:00
auto await_resume() -> std::variant<typename Ts::result_type...>;
2025-01-23 12:46:52 -08:00
};
2025-01-24 14:48:15 -08:00
template <size_t I, typename... Ts>
auto wait_ircmsg::start(Wait<Ts...> &command) -> void
{
ircmsg_slot_ = command.get_connection().sig_ircmsg.connect([this, &command](auto cmd, auto &msg) {
2025-01-25 15:45:31 -08:00
const auto wanted = want_cmds_.empty() || std::find(want_cmds_.begin(), want_cmds_.end(), cmd) != want_cmds_.end();
if (wanted)
{
2025-01-24 14:48:15 -08:00
command.template complete<I>(cmd, msg);
}
});
}
2025-01-28 20:01:51 -08:00
template <size_t I, typename... Ts>
auto wait_snote::start(Wait<Ts...> &command) -> void
{
snote_slot_ = command.get_connection().sig_snote.connect([this, &command](auto &match) {
const auto tag = match.get_tag();
const auto wanted = want_tags_.empty() || std::find(want_tags_.begin(), want_tags_.end(), tag) != want_tags_.end();
if (wanted)
{
command.template complete<I>(match);
}
});
}
2025-01-24 14:48:15 -08:00
template <size_t I, typename... Ts>
2025-01-25 15:45:31 -08:00
auto wait_timeout::start(Wait<Ts...> &command) -> void
2025-01-24 14:48:15 -08:00
{
timer_.emplace(command.get_connection().get_executor());
timer_->expires_after(timeout_);
2025-01-25 15:45:31 -08:00
timer_->async_wait([this, &command](const auto &error) {
if (not error)
{
2025-01-24 14:48:15 -08:00
timer_.reset();
command.template complete<I>();
}
});
}
template <typename... Ts>
auto Wait<Ts...>::await_suspend(std::coroutine_handle<irc_promise> handle) -> void
{
handle_ = handle;
2025-01-25 15:45:31 -08:00
const auto tuple_size = std::tuple_size_v<decltype(modes_)>;
2025-01-24 14:48:15 -08:00
start_modes(std::make_index_sequence<tuple_size>{});
disconnect_slot_ = get_connection().sig_disconnect.connect([this]() {
handle_.resume();
});
}
template <typename... Ts>
auto Wait<Ts...>::await_resume() -> std::variant<typename Ts::result_type...>
{
2025-01-25 15:45:31 -08:00
const auto tuple_size = std::tuple_size_v<decltype(modes_)>;
2025-01-24 14:48:15 -08:00
stop_modes(std::make_index_sequence<tuple_size>{});
disconnect_slot_.disconnect();
2025-01-25 15:45:31 -08:00
if (result_)
{
2025-01-24 14:48:15 -08:00
return std::move(*result_);
2025-01-25 15:45:31 -08:00
}
else
{
2025-01-24 14:48:15 -08:00
throw std::runtime_error{"connection terminated"};
}
}
2025-01-28 20:01:51 -08:00
/// Start the coroutine and associate it with a specific connection.
inline auto irc_coroutine::start(Connection &connection) -> void
{
promise().connection_ = connection.shared_from_this();
resume();
}
/// Returns true when this coroutine is still waiting on events
inline auto irc_coroutine::is_running() -> bool
{
return promise().connection_ != nullptr;
}
inline auto irc_coroutine::exception() -> std::exception_ptr
{
return promise().exception_;
}