0
0
Fork 0
mirror of https://github.com/matrix-construct/construct synced 2024-12-27 07:54:05 +01:00

construct: Restructure signal handling to object, unit; restructure console into object.

This commit is contained in:
Jason Volk 2018-12-11 14:55:40 -08:00
parent 31798dd09a
commit 1906ac57b7
5 changed files with 487 additions and 406 deletions

View file

@ -75,6 +75,7 @@ construct_LDADD = \
construct_SOURCES = \
construct.cc \
signals.cc \
console.cc \
lgetopt.cc \
###

View file

@ -15,220 +15,176 @@ using namespace ircd;
IRCD_EXCEPTION_HIDENAME(ircd::error, bad_command)
bool console_active;
bool console_inwork;
ircd::ctx::ctx *console_ctx;
ircd::module *console_module;
std::string record_path;
decltype(construct::console)
construct::console;
const char *const
generic_message
{R"(
*** - To end the console session: type exit, or ctrl-d -> EOF
*** - To shutdown cleanly: type die, or ctrl-\ -> SIGQUIT
*** - To generate a coredump for developers, type ABORT -> abort()
***
)"};
conf::item<size_t>
stack_sz
decltype(construct::console::stack_sz)
construct::console::stack_sz
{
{ "name", "construct.console.stack.size" },
{ "default", long(2_MiB) },
};
conf::item<milliseconds>
ratelimit_sleep
decltype(construct::console::input_max)
construct::console::input_max
{
{ "name", "construct.console.input.max" },
{ "default", long(64_KiB) },
};
decltype(construct::console::ratelimit_sleep)
construct::console::ratelimit_sleep
{
{ "name", "construct.console.ratelimit.sleep" },
{ "default", 75L },
};
conf::item<size_t>
ratelimit_bytes
decltype(construct::console::ratelimit_bytes)
construct::console::ratelimit_bytes
{
{ "name", "construct.console.ratelimit.bytes" },
{ "default", long(2_KiB) },
};
static void check_console_active();
static void console_fini();
static void console_init();
static bool console_cmd__record(const string_view &line);
static int handle_line_bymodule(const string_view &line);
static bool handle_line(const string_view &line);
static void execute(const std::vector<std::string> lines);
static void console();
decltype(construct::console::generic_message)
construct::console::generic_message
{R"(
*** - To end the console session: type exit, or ctrl-d -> EOF
*** - To shutdown cleanly: type die, or ctrl-\ -> SIGQUIT
*** - To generate a coredump for developers, type ABORT -> abort()
***)"_sv
};
void
console_spawn()
{
check_console_active();
ircd::context
{
"console",
stack_sz,
console,
ircd::context::DETACH | ircd::context::POST
};
}
void
console_execute(const std::vector<std::string> &lines)
{
check_console_active();
ircd::context
{
"execute",
stack_sz,
std::bind(&execute, lines),
ircd::context::DETACH | ircd::context::POST
};
}
void
console_init()
{
console_cancel();
console_active = true;
console_ctx = &ctx::cur();
console_module = new ircd::module{"console"};
}
void
console_fini()
{
console_active = false;
console_ctx = nullptr;
delete console_module; console_module = nullptr;
std::cin.clear();
}
const char *const console_message
decltype(construct::console::console_message)
construct::console::console_message
{R"(
***
*** The server is still running in the background. This is the
*** terminal console also available in your !control room.
***)"};
***)"_sv
};
decltype(construct::console::seen_message)
construct::console::seen_message;
decltype(construct::console::queue)
construct::console::queue;
bool
construct::console::spawn()
{
if(active())
return false;
construct::console = new console;
return true;
}
bool
construct::console::execute(std::string cmd)
{
console::queue.emplace_back(std::move(cmd));
console::spawn();
return true;
}
bool
construct::console::interrupt()
{
if(active())
{
construct::console->context.interrupt();
return true;
}
return false;
}
bool
construct::console::terminate()
{
if(active())
{
construct::console->context.terminate();
return true;
}
return false;
}
bool
construct::console::active()
{
return construct::console != nullptr;
}
//
// console::console
//
construct::console::console()
:context
{
"console",
stack_sz,
std::bind(&console::main, this),
ircd::context::POST
}
,runlevel_changed
{
std::bind(&console::on_runlevel, this, std::placeholders::_1)
}
{
}
void
console()
construct::console::main()
try
{
ircd::runlevel_changed::dock.wait([]
const unwind destruct{[]
{
return ircd::runlevel == ircd::runlevel::RUN ||
ircd::runlevel == ircd::runlevel::QUIT ||
ircd::runlevel == ircd::runlevel::HALT;
});
construct::console->context.detach();
delete construct::console;
construct::console = nullptr;
}};
if(ircd::runlevel != ircd::runlevel::RUN)
if(!wait_running())
return;
const unwind atexit([]
ircd::module module{"console"};
this->module = &module;
if(next_command())
{
console_fini();
});
while(handle_line())
if(!next_command())
break;
console_init();
static std::once_flag seen_message;
std::call_once(seen_message, []
{
std::cout << console_message << generic_message;
});
while(1)
{
std::cout << "\n> " << std::flush;
thread_local char buf[64_KiB];
string_view line;
// Suppression scope ends after the command is entered
// so the output of the command (if log messages) can be seen.
{
const log::console_quiet quiet(false);
line = fs::stdin::readline(buf);
}
if(line.empty())
continue;
if(!handle_line(line))
break;
ctx::interruption_point();
return;
}
std::cout << std::endl;
show_message(); do
{
wait_input();
}
while(handle_line());
}
catch(const std::exception &e)
{
std::cout << std::endl;
std::cout << "***" << std::endl;
std::cout << "*** The console session has ended: " << e.what() << std::endl;
std::cout << "***" << std::endl;
std::cout
<< "\n***"
<< "\n*** The console session has ended: " << e.what()
<< "\n***"
<< std::endl;
ircd::log::debug
log::debug
{
"The console session has ended: %s", e.what()
};
}
void
execute(const std::vector<std::string> lines)
try
{
ircd::runlevel_changed::dock.wait([]
{
return ircd::runlevel == ircd::runlevel::RUN ||
ircd::runlevel == ircd::runlevel::HALT;
});
if(ircd::runlevel == ircd::runlevel::HALT)
return;
const unwind atexit([]
{
console_fini();
});
console_init();
for(const auto &line : lines)
{
if(line.empty())
continue;
if(!handle_line(line))
break;
}
}
catch(const std::exception &e)
{
std::cout << std::endl;
std::cout << "***\n";
std::cout << "*** The execution aborted: " << e.what() << "\n";
std::cout << "***" << std::endl;
std::cout << std::flush;
std::cout.clear();
ircd::log::debug
{
"The execution aborted: %s", e.what()
};
}
bool
handle_line(const string_view &line)
construct::console::handle_line()
try
{
// _theirs is copied for recursive reentrance
const unwind outwork{[console_inwork_theirs(console_inwork)]
{
console_inwork = console_inwork_theirs;
}};
console_inwork = true;
if(line == "ABORT")
abort();
@ -236,10 +192,10 @@ try
exit(0);
if(startswith(line, "record"))
return console_cmd__record(tokens_after(line, ' ', 0));
return cmd__record();
int ret{-1};
if(console_module) switch((ret = handle_line_bymodule(line)))
if(module) switch((ret = handle_line_bymodule()))
{
default: break;
case 0: return false;
@ -261,9 +217,9 @@ catch(const bad_command &e)
std::cerr << "Bad command or file name: " << e.what() << std::endl;
return true;
}
catch(const ircd::http::error &e)
catch(const http::error &e)
{
ircd::log::error
log::error
{
"%s %s", e.what(), e.content
};
@ -272,7 +228,7 @@ catch(const ircd::http::error &e)
}
catch(const std::exception &e)
{
ircd::log::error
log::error
{
"%s", e.what()
};
@ -281,12 +237,13 @@ catch(const std::exception &e)
}
int
handle_line_bymodule(const string_view &line)
construct::console::handle_line_bymodule()
{
using prototype = int (std::ostream &, const string_view &, const string_view &);
const ircd::mods::import<prototype> command
const mods::import<prototype> command
{
*console_module, "console_command"
*module, "console_command"
};
std::ostringstream out;
@ -322,7 +279,7 @@ handle_line_bymodule(const string_view &line)
// to the output following it.
const std::string cmdline
{
"\n> "s + std::string{line} + "\n\n"
"\n> "s + line + "\n\n"
};
append(fd, string_view(cmdline));
@ -369,15 +326,20 @@ handle_line_bymodule(const string_view &line)
}
bool
console_cmd__record(const string_view &line)
construct::console::cmd__record()
{
if(empty(line) && empty(record_path))
const string_view &args
{
tokens_after(line, ' ', 0)
};
if(empty(args) && empty(record_path))
{
std::cout << "Console not currently recorded to any file." << std::endl;
return true;
}
if(empty(line) && !empty(record_path))
if(empty(args) && !empty(record_path))
{
std::cout << "Stopped recording to file `" << record_path << "'" << std::endl;
record_path = {};
@ -386,7 +348,7 @@ console_cmd__record(const string_view &line)
const auto path
{
token(line, ' ', 0)
token(args, ' ', 0)
};
std::cout << "Recording console to file `" << path << "'" << std::endl;
@ -395,57 +357,80 @@ console_cmd__record(const string_view &line)
}
void
check_console_active()
construct::console::wait_input()
{
if(console_active)
throw ircd::error
line = {}; do
{
// Suppression scope ends after the command is entered
// so the output of the command (if log messages) can be seen.
const log::console_quiet quiet(false);
std::cout << "\n> " << std::flush;
line.resize(size_t(input_max));
const mutable_buffer buffer
{
"Console is already active and cannot be reentered"
const_cast<char *>(line.data()), line.size()
};
const string_view read
{
fs::stdin::readline(buffer)
};
line.resize(size(read));
}
while(line.empty());
}
void
console_hangup()
try
bool
construct::console::next_command()
{
using log::console_quiet;
console_cancel();
static console_quiet *quieted;
if(!quieted)
line = {};
while(!queue.empty() && line.empty())
{
log::notice("Suppressing console log output after terminal hangup");
quieted = new console_quiet;
return;
line = std::move(queue.front());
queue.pop_front();
}
log::notice("Reactivating console logging after second hangup");
delete quieted;
quieted = nullptr;
}
catch(const std::exception &e)
{
ircd::log::error
{
"console_hangup(): %s", e.what()
};
return !line.empty();
}
void
console_cancel()
try
construct::console::on_runlevel(const enum ircd::runlevel &runlevel)
{
if(!console_active)
return;
if(console_ctx)
interrupt(*console_ctx);
}
catch(const std::exception &e)
{
ircd::log::error
switch(runlevel)
{
"Interrupting console: %s", e.what()
};
case ircd::runlevel::QUIT:
case ircd::runlevel::HALT:
case ircd::runlevel::FAULT:
console::terminate();
break;
default:
break;
}
}
bool
construct::console::wait_running()
const
{
ircd::runlevel_changed::dock.wait([]
{
return ircd::runlevel == ircd::runlevel::RUN ||
ircd::runlevel == ircd::runlevel::QUIT ||
ircd::runlevel == ircd::runlevel::HALT;
});
return ircd::runlevel == ircd::runlevel::RUN;
}
void
construct::console::show_message()
const
{
std::call_once(seen_message, []
{
std::cout << console_message << generic_message;
});
}

View file

@ -14,7 +14,6 @@
#include "lgetopt.h"
#include "construct.h"
static void sigfd_handler(const boost::system::error_code &, int) noexcept;
static bool startup_checks();
static void applyargs();
static void enable_coredumps();
@ -77,11 +76,6 @@ std::unique_ptr<boost::asio::io_context> ios
std::make_unique<boost::asio::io_context>()
};
boost::asio::signal_set sigs
{
*ios
};
int main(int argc, char *const *argv)
try
{
@ -147,39 +141,15 @@ try
// event loop. This means we lose the true instant hardware-interrupt gratitude
// of signals but with the benefit of unconditional safety and cross-
// platformness with windows etc.
sigs.add(SIGHUP);
sigs.add(SIGINT);
sigs.add(SIGQUIT);
sigs.add(SIGTERM);
sigs.add(SIGUSR1);
sigs.async_wait(sigfd_handler);
// Because we registered signal handlers with the io_context, ios->run()
// is now shared between those handlers and libircd. This means the run()
// won't return even if we call ircd::quit(). We use the callback to then
// cancel the handlers so run() can return and the program can exit.
const ircd::runlevel_changed handler{[]
(const auto &runlevel)
{
switch(runlevel)
{
case ircd::runlevel::HALT:
case ircd::runlevel::FAULT:
sigs.cancel();
break;
default:
break;
}
}};
const construct::signals signals{*ios};
// If the user wants to immediately drop to a command line without having to
// send a ctrl-c for it, that is provided here.
if(cmdline)
console_spawn();
construct::console::spawn();
if(execute)
console_execute({execute});
construct::console::execute({execute});
// Execution.
// Blocks until a clean exit from a quit() or an exception comes out of it.
@ -305,145 +275,3 @@ applyargs()
else
ircd::fs::aio::enable.set("true");
}
//
// Signal handling
//
static void handle_quit();
static void handle_interruption();
static void handle_hangup();
static void handle_usr1();
static void handle(const int &signum);
void
sigfd_handler(const boost::system::error_code &ec,
int signum)
noexcept
{
switch(ec.value())
{
using namespace boost::system::errc;
case success:
break;
case operation_canceled:
console_cancel();
return;
default:
console_cancel();
throw std::runtime_error(ec.message());
}
handle(signum);
switch(ircd::runlevel)
{
case ircd::runlevel::QUIT:
case ircd::runlevel::FAULT:
return;
default:
sigs.async_wait(sigfd_handler);
}
}
void
handle(const int &signum)
{
switch(signum)
{
case SIGINT: return handle_interruption();
case SIGHUP: return handle_hangup();
case SIGQUIT: return handle_quit();
case SIGTERM: return handle_quit();
case SIGUSR1: return handle_usr1();
}
}
void
handle_quit()
try
{
console_cancel();
ircd::quit();
}
catch(const std::exception &e)
{
ircd::log::error
{
"SIGQUIT handler: %s", e.what()
};
}
void
handle_hangup()
try
{
console_hangup();
}
catch(const std::exception &e)
{
ircd::log::error
{
"SIGHUP handler: %s", e.what()
};
}
void
handle_interruption()
try
{
if(console_active)
console_cancel();
else
console_spawn();
}
catch(const std::exception &e)
{
ircd::log::error
{
"SIGINT handler: %s", e.what()
};
}
void
handle_usr1()
try
{
// Spawning the context that follows this branch and doing a rehash
// when not in a stable state like runlevel::RUN will just make a mess
// so any signal received is just dropped and the user can try again.
if(ircd::runlevel != ircd::runlevel::RUN)
{
ircd::log::warning
{
"Not rehashing conf from SIGUSR1 in runlevel %s",
reflect(ircd::runlevel)
};
return;
}
// This signal handler (though not a *real* signal handler) is still
// running on the main async stack and not an ircd::ctx. The reload
// function does a lot of IO so it requires an ircd::ctx.
ircd::context{[]
{
ircd::mods::import<void ()> reload_conf
{
"s_conf", "reload_conf"
};
reload_conf();
}};
}
catch(const std::exception &e)
{
ircd::log::error
{
"SIGUSR1 handler: %s", e.what()
};
}

View file

@ -8,15 +8,61 @@
// copyright notice and this permission notice is present in all copies. The
// full license for this software is available in the LICENSE file.
//
// console.cc
//
namespace construct
{
struct signals;
struct console;
extern bool quietmode;
extern bool console_active;
extern struct console *console;
}
void console_spawn();
void console_execute(const std::vector<std::string> &lines);
void console_cancel();
void console_hangup();
void console_termstop();
struct construct::signals
{
std::unique_ptr<boost::asio::signal_set> signal_set;
ircd::runlevel_changed runlevel_changed;
void set_handle();
void on_signal(const boost::system::error_code &, int) noexcept;
void on_runlevel(const enum ircd::runlevel &);
public:
signals(boost::asio::io_context &ios);
};
struct construct::console
{
static ircd::conf::item<size_t> stack_sz;
static ircd::conf::item<size_t> input_max;
static ircd::conf::item<size_t> ratelimit_bytes;
static ircd::conf::item<ircd::milliseconds> ratelimit_sleep;
static const ircd::string_view generic_message;
static const ircd::string_view console_message;
static std::once_flag seen_message;
static std::deque<std::string> queue;
std::string line;
std::string record_path;
ircd::module *module {nullptr};
ircd::context context;
ircd::runlevel_changed runlevel_changed;
void show_message() const;
void on_runlevel(const enum ircd::runlevel &);
bool wait_running() const;
bool next_command();
void wait_input();
bool cmd__record();
int handle_line_bymodule();
bool handle_line();
void main();
console();
static bool active();
static bool interrupt();
static bool terminate();
static bool execute(std::string cmd);
static bool spawn();
};

221
construct/signals.cc Normal file
View file

@ -0,0 +1,221 @@
// Matrix Construct
//
// Copyright (C) Matrix Construct Developers, Authors & Contributors
// Copyright (C) 2016-2018 Jason Volk <jason@zemos.net>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice is present in all copies. The
// full license for this software is available in the LICENSE file.
#include <ircd/ircd.h>
#include <ircd/asio.h>
#include "construct.h"
namespace construct
{
namespace ph = std::placeholders;
static void handle_usr1();
static void handle_quit();
static void handle_interrupt();
static void handle_hangup();
static void handle_signal(const int &);
}
construct::signals::signals(boost::asio::io_context &ios)
:signal_set
{
std::make_unique<boost::asio::signal_set>(ios)
}
,runlevel_changed
{
std::bind(&signals::on_runlevel, this, ph::_1)
}
{
signal_set->add(SIGHUP);
signal_set->add(SIGINT);
signal_set->add(SIGQUIT);
signal_set->add(SIGTERM);
signal_set->add(SIGUSR1);
set_handle();
}
// Because we registered signal handlers with the io_context, ios->run()
// is now shared between those handlers and libircd. This means the run()
// won't return even if we call ircd::quit(). We use this callback to
// cancel the signal handlers so run() can return and the program can exit.
void
construct::signals::on_runlevel(const enum ircd::runlevel &runlevel)
{
switch(runlevel)
{
case ircd::runlevel::HALT:
case ircd::runlevel::FAULT:
signal_set->cancel();
break;
default:
break;
}
}
void
construct::signals::on_signal(const boost::system::error_code &ec,
int signum)
noexcept
{
assert(!ec || ec.category() == boost::system::system_category());
switch(ec.value())
{
// Signal received
case boost::system::errc::success:
break;
// Shutdown
case boost::system::errc::operation_canceled:
return;
// Not expected
default:
ircd::throw_system_error(ec);
__builtin_unreachable();
}
handle_signal(signum);
switch(ircd::runlevel)
{
// Reinstall handler for next signal
default:
set_handle();
break;
// No reinstall of handler.
case ircd::runlevel::QUIT:
case ircd::runlevel::FAULT:
break;
}
}
void
construct::signals::set_handle()
{
auto handler
{
std::bind(&signals::on_signal, this, ph::_1, ph::_2)
};
signal_set->async_wait(std::move(handler));
}
void
construct::handle_signal(const int &signum)
{
switch(signum)
{
case SIGINT: return handle_interrupt();
case SIGHUP: return handle_hangup();
case SIGQUIT: return handle_quit();
case SIGTERM: return handle_quit();
case SIGUSR1: return handle_usr1();
default: break;
}
ircd::log::error
{
"Caught unhandled signal %d", signum
};
}
void
construct::handle_quit()
try
{
ircd::quit();
}
catch(const std::exception &e)
{
ircd::log::error
{
"SIGQUIT handler: %s", e.what()
};
}
void
construct::handle_hangup()
try
{
static bool console_disabled;
console_disabled =! console_disabled;
if(console_disabled)
ircd::log::console_disable();
else
ircd::log::console_enable();
}
catch(const std::exception &e)
{
ircd::log::error
{
"SIGHUP handler: %s", e.what()
};
}
void
construct::handle_interrupt()
try
{
if(!console::active())
console::spawn();
else
console::interrupt();
}
catch(const std::exception &e)
{
ircd::log::error
{
"SIGINT handler: %s", e.what()
};
}
void
construct::handle_usr1()
try
{
// Spawning the context that follows this branch and doing a rehash
// when not in a stable state like runlevel::RUN will just make a mess
// so any signal received is just dropped and the user can try again.
if(ircd::runlevel != ircd::runlevel::RUN)
{
ircd::log::warning
{
"Not rehashing conf from SIGUSR1 in runlevel %s",
reflect(ircd::runlevel)
};
return;
}
// This signal handler (though not a *real* signal handler) is still
// running on the main async stack and not an ircd::ctx. The reload
// function does a lot of IO so it requires an ircd::ctx.
ircd::context{[]
{
ircd::mods::import<void ()> reload_conf
{
"s_conf", "reload_conf"
};
reload_conf();
}};
}
catch(const std::exception &e)
{
ircd::log::error
{
"SIGUSR1 handler: %s", e.what()
};
}