9e165d07ef
A general cleanup of how we start indexer.
424 lines
15 KiB
C++
424 lines
15 KiB
C++
/*
|
|
* This file is part of the Flowee project
|
|
* Copyright (C) 2019-2026 Tom Zander <tom@flowee.org>
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program 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 General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
#include "FloweeServiceApplication.h"
|
|
#include <config/flowee-config.h>
|
|
|
|
#include <QCommandLineParser>
|
|
#include <QStandardPaths>
|
|
#include <QTextStream>
|
|
#include <QDateTime>
|
|
#include <signal.h>
|
|
#include <QTimer>
|
|
#include <QFileInfo>
|
|
#include <QDir>
|
|
|
|
#include <utilstrencodings.h> // for SplitHostPort
|
|
#include <clientversion.h>
|
|
#include <boost/asio.hpp>
|
|
#ifdef Qt6Network_FOUND
|
|
#include <QtNetwork/QNetworkInterface>
|
|
#include <QTcpServer>
|
|
#endif
|
|
|
|
namespace {
|
|
void HandleSigTerm(int) {
|
|
QCoreApplication::quit();
|
|
}
|
|
|
|
void HandleSigHup(int) {
|
|
FloweeServiceApplication *app = qobject_cast<FloweeServiceApplication*>(QCoreApplication::instance());
|
|
Q_ASSERT(app);
|
|
app->handleSigHub();
|
|
}
|
|
}
|
|
|
|
#ifdef __linux__
|
|
#include <sys/socket.h>
|
|
#include <linux/netlink.h>
|
|
#include <linux/rtnetlink.h>
|
|
namespace {
|
|
|
|
void netWatch()
|
|
{
|
|
// Create a netlink socket
|
|
const int sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
|
|
if (sock_fd < 0) {
|
|
logFatal() << "Failed to get netlink socket";
|
|
return;
|
|
}
|
|
|
|
// Bind to the socket
|
|
struct sockaddr_nl sa;
|
|
memset(&sa, 0, sizeof(sa));
|
|
sa.nl_family = AF_NETLINK;
|
|
sa.nl_groups = RTMGRP_LINK; // Subscribe to link events
|
|
if (bind(sock_fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
|
|
logFatal() << "Failed to bind to socket";
|
|
close(sock_fd);
|
|
return;
|
|
}
|
|
|
|
char buffer[4096];
|
|
while (true) {
|
|
ssize_t len = recv(sock_fd, buffer, sizeof(buffer), 0);
|
|
if (len < 0) {
|
|
logFatal() << "Disconnected netlink";
|
|
break;
|
|
}
|
|
|
|
for (struct nlmsghdr *nlh = (struct nlmsghdr *) buffer; NLMSG_OK(nlh, len); nlh = NLMSG_NEXT(nlh, len)) {
|
|
if (nlh->nlmsg_type == RTM_NEWLINK) {
|
|
FloweeServiceApplication *app = qobject_cast<FloweeServiceApplication*>(QCoreApplication::instance());
|
|
assert(app);
|
|
app->networkSetupChanged();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
close(sock_fd);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
FloweeServiceApplication::FloweeServiceApplication(int &argc, char **argv, short appLogSection)
|
|
: QCoreApplication(argc, argv),
|
|
m_debug(QStringList() << "debug", "Use debug level logging"),
|
|
m_verbose(QStringList() << "verbose" << "v", "Be more verbose"),
|
|
m_quiet(QStringList() << "quiet" << "q", "Be quiet, only errors are shown"),
|
|
m_version(QStringList() << "version", "Display version"),
|
|
m_bindAddress(QStringList() << "bind", "Bind to this IP:port", "IP-ADDRESS"),
|
|
m_connect("connect", "Server location and port", "Hostname"),
|
|
m_appLogSection(appLogSection)
|
|
{
|
|
connect (this, &FloweeServiceApplication::startRebindTriggered, this, [=]() {
|
|
QTimer::singleShot(2000, this, &FloweeServiceApplication::rebind);
|
|
}, Qt::QueuedConnection);
|
|
}
|
|
|
|
FloweeServiceApplication::~FloweeServiceApplication()
|
|
{
|
|
if (!m_logFile.isEmpty()) // only log when a logfile was passed to the setup()
|
|
logFatal(m_appLogSection) << "Shutdown";
|
|
}
|
|
|
|
void FloweeServiceApplication::addServerOptions(QCommandLineParser &parser, Options options)
|
|
{
|
|
m_isServer = true;
|
|
addClientOptions(parser, options);
|
|
}
|
|
|
|
void FloweeServiceApplication::addClientOptions(QCommandLineParser &parser, Options options)
|
|
{
|
|
m_parser = &parser;
|
|
#ifndef BCH_NO_DEBUG_OUTPUT
|
|
if (!m_isServer)
|
|
parser.addOption(m_debug);
|
|
#endif
|
|
parser.addOption(m_version);
|
|
if (!options.testFlag(NoConnect))
|
|
parser.addOption(m_connect);
|
|
if (m_isServer) {
|
|
parser.addOption(m_bindAddress);
|
|
} else if (!options.testFlag(NoVerbosity)) {
|
|
parser.addOption(m_verbose);
|
|
parser.addOption(m_quiet);
|
|
}
|
|
}
|
|
|
|
void FloweeServiceApplication::setup(const char *logFilename, const QString &configFilePath) {
|
|
if (m_parser && m_parser->isSet(m_version)) {
|
|
QTextStream out(stdout);
|
|
out << applicationName() << " " << applicationVersion() << Qt::endl;
|
|
out << "Flowee Libs version: " << FormatFullVersion().c_str() << Qt::endl;
|
|
out << "License GPLv3+: GNU GPL version 3 or later" << Qt::endl;
|
|
out << "This is free software: you are free to change and redistribute it." << Qt::endl << Qt::endl;
|
|
|
|
::exit(0);
|
|
return;
|
|
}
|
|
if (m_parser && (!m_isServer && (m_parser->isSet(m_verbose) || m_parser->isSet(m_quiet)
|
|
#ifndef BCH_NO_DEBUG_OUTPUT
|
|
|| m_parser->isSet(m_debug)
|
|
#endif
|
|
))) {
|
|
auto *logger = Log::Manager::instance();
|
|
logger->clearChannels();
|
|
Log::Verbosity v = Log::WarningLevel;
|
|
#ifndef BCH_NO_DEBUG_OUTPUT
|
|
if (m_parser->isSet(m_debug))
|
|
v = Log::DebugLevel;
|
|
else
|
|
#endif
|
|
if (m_parser->isSet(m_verbose))
|
|
v = Log::InfoLevel;
|
|
else if (m_parser->isSet(m_quiet))
|
|
v = Log::FatalLevel;
|
|
logger->clearLogLevels(v);
|
|
logger->addConsoleChannel();
|
|
}
|
|
else if (logFilename) {
|
|
m_logFile = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)
|
|
+ QString("/%1").arg(logFilename);
|
|
if (!configFilePath.isEmpty()) {
|
|
QFileInfo item(configFilePath);
|
|
if (item.isDir())
|
|
m_logsconf = configFilePath + "/logs.conf";
|
|
else if (item.isFile())
|
|
m_logsconf = configFilePath;
|
|
}
|
|
if (m_logsconf.isEmpty())
|
|
m_logsconf = QStandardPaths::locate(QStandardPaths::AppConfigLocation, "logs.conf");
|
|
if (m_logsconf.isEmpty())
|
|
m_logsconf = QStandardPaths::locate(QStandardPaths::ConfigLocation,
|
|
organizationName() + "/logs.conf");
|
|
if (m_logsconf.isEmpty()) { // last try, homedir (for system installs).
|
|
auto inHome = QDir::homePath() + "/logs.conf";
|
|
if (QFile::exists(inHome))
|
|
m_logsconf = inHome;
|
|
}
|
|
if (m_logsconf.isEmpty()) {
|
|
logCritical().nospace() << applicationName() << "] No logs config found";
|
|
for (auto &p : QStandardPaths::standardLocations(QStandardPaths::AppConfigLocation)) {
|
|
logWarning(m_appLogSection).nospace() << " tried " << p << "/logs.conf";
|
|
}
|
|
for (auto &p : QStandardPaths::standardLocations(QStandardPaths::ConfigLocation)) {
|
|
logWarning(m_appLogSection).nospace() << " tried " << p << '/' << organizationName() << "/logs.conf";
|
|
}
|
|
logWarning(m_appLogSection).nospace() << " tried " << QDir::homePath() << "/logs.conf";
|
|
logCritical().nospace() << "Log output goes to: " << m_logFile;
|
|
Log::Manager::instance()->setLogLevel(m_appLogSection, Log::WarningLevel);
|
|
} else {
|
|
logCritical().nospace() << applicationName() << "] Trying logs config at " << m_logsconf;
|
|
}
|
|
|
|
Log::Manager::instance()->parseConfig(m_logsconf.toLocal8Bit().toStdString(), m_logFile.toLocal8Bit().toStdString());
|
|
logFatal() << organizationName() << applicationName() << "starting. Version:"
|
|
<< applicationVersion() << "Libs version:" << FormatFullVersion().c_str();
|
|
logFatal() << "Start-time UTC:" << QDateTime::currentDateTimeUtc().toString();
|
|
logFatal() << "Machine local :" << QDateTime::currentDateTime().toString();
|
|
logCritical() << "Main Log-Section:" << m_appLogSection;
|
|
}
|
|
|
|
// Reopen log on SIGHUP (to allow for log-rotate)
|
|
struct sigaction sa_hup;
|
|
sa_hup.sa_handler = HandleSigHup;
|
|
sigemptyset(&sa_hup.sa_mask);
|
|
sa_hup.sa_flags = 0;
|
|
sigaction(SIGHUP, &sa_hup, nullptr);
|
|
|
|
struct sigaction sa;
|
|
sa.sa_handler = HandleSigTerm;
|
|
sigemptyset(&sa.sa_mask);
|
|
sa.sa_flags = 0;
|
|
sigaction(SIGTERM, &sa, nullptr);
|
|
sigaction(SIGINT, &sa, nullptr);
|
|
|
|
|
|
// Ignore SIGPIPE, otherwise it will bring the daemon down if the client closes unexpectedly
|
|
signal(SIGPIPE, SIG_IGN);
|
|
}
|
|
|
|
EndPoint FloweeServiceApplication::serverAddressFromArguments(uint16_t defaultPort) const
|
|
{
|
|
Q_ASSERT(m_parser);
|
|
EndPoint ep;
|
|
ep.announcePort = defaultPort;
|
|
if (m_parser->isSet(m_connect))
|
|
SplitHostPort(m_parser->value(m_connect).toStdString(), ep.announcePort, ep.hostname);
|
|
else
|
|
ep.ipAddress = boost::asio::ip::address_v4::loopback();
|
|
return ep;
|
|
}
|
|
|
|
QList<boost::asio::ip::tcp::endpoint> FloweeServiceApplication::bindingEndPoints(uint16_t defaultPort, DefaultBindOption defaultBind) const
|
|
{
|
|
assert(m_parser);
|
|
QStringList addresses = m_parser->values(m_bindAddress);
|
|
if (addresses.isEmpty()) {
|
|
switch (defaultBind) {
|
|
case FloweeServiceApplication::YggAsDefault:
|
|
addresses << "yggdrasil";
|
|
break;
|
|
case FloweeServiceApplication::LocalhostAsDefault:
|
|
addresses << "localhost";
|
|
break;
|
|
case FloweeServiceApplication::AllInterfacesAsDefault:
|
|
addresses << "0.0.0.0";
|
|
break;
|
|
case FloweeServiceApplication::UserSupplied: break;
|
|
}
|
|
}
|
|
|
|
QList<boost::asio::ip::tcp::endpoint> answer;
|
|
for (QString &address : addresses) {
|
|
std::string hostname;
|
|
uint16_t port = defaultPort;
|
|
SplitHostPort(address.toStdString(), port, hostname);
|
|
std::transform(hostname.begin(), hostname.end(), hostname.begin(), ::tolower);
|
|
const bool all = hostname == "0.0.0.0";
|
|
const bool ygg = hostname == "yggdrasil";
|
|
using namespace boost::asio;
|
|
if (hostname.empty() || hostname == "localhost" || all || ygg) {
|
|
answer.push_back(ip::tcp::endpoint(ip::address_v4::loopback(), port));
|
|
answer.push_back(ip::tcp::endpoint(ip::address_v6::loopback(), port));
|
|
if (all || ygg) {
|
|
#ifdef Qt6Network_FOUND
|
|
for (auto &net : QNetworkInterface::allAddresses()) {
|
|
if (net.isLoopback())
|
|
continue;
|
|
if (net.isLinkLocal())
|
|
continue;
|
|
bool use = all;
|
|
if (!all && ygg && net.protocol() == QAbstractSocket::IPv6Protocol) {
|
|
// yggdrasil uses normal IPv6 addresses, but with a specific prefix not
|
|
// seen on the wider Internet.
|
|
Q_IPV6ADDR bytes = net.toIPv6Address();
|
|
use = (bytes[0] & 0xfe) == 2; // address is in range '0200/7';
|
|
}
|
|
if (!use)
|
|
continue;
|
|
boost::system::error_code ec;
|
|
auto ip = ip::make_address(net.toString().toStdString(), ec);
|
|
if (ec) {
|
|
logDebug() << "Internal issue with address:" << net.toString() << ec.message();
|
|
continue;
|
|
}
|
|
answer.push_back(ip::tcp::endpoint(ip, port));
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
else {
|
|
try {
|
|
answer.append(boost::asio::ip::tcp::endpoint(boost::asio::ip::make_address(hostname), port));
|
|
} catch (std::runtime_error &e) {
|
|
logFatal().nospace() << "Bind address didn't parse: `" << address << "'. Skipping.";
|
|
logDebug() << e;
|
|
}
|
|
}
|
|
}
|
|
return answer;
|
|
}
|
|
|
|
void FloweeServiceApplication::handleSigHub() const
|
|
{
|
|
Log::Manager::instance()->reopenLogFiles();
|
|
Log::Manager::instance()->parseConfig(m_logsconf.toLocal8Bit().toStdString(), m_logFile.toLocal8Bit().toStdString());
|
|
|
|
emit reparseConfig();
|
|
}
|
|
|
|
int FloweeServiceApplication::bindTo(QTcpServer *server, int defaultPort)
|
|
{
|
|
#ifdef Qt6Network_FOUND
|
|
QStringList addresses = m_parser->values(m_bindAddress);
|
|
QHostAddress address;
|
|
int port = defaultPort;
|
|
if (!addresses.empty()) {
|
|
if (addresses.size() > 1) {
|
|
logFatal() << "More than one --bind passsed, please limit to one or use 'localhost' / '0.0.0.0' wildcards";
|
|
return 1;
|
|
}
|
|
QString ip(addresses.front());
|
|
int index = ip.indexOf(":");
|
|
if (index > 0) {
|
|
bool ok;
|
|
port = ip.mid(index + 1).toInt(&ok);
|
|
if (!ok) {
|
|
logFatal() << "Could not parse port portion of bind address.";
|
|
return 2;
|
|
}
|
|
ip = ip.left(index);
|
|
}
|
|
if (ip.compare("localhost", Qt::CaseInsensitive) == 0)
|
|
address = QHostAddress::LocalHost;
|
|
else if (ip == QLatin1String("0.0.0.0"))
|
|
address = QHostAddress::Any;
|
|
else {
|
|
address = QHostAddress(ip);
|
|
}
|
|
if (address.isNull()) {
|
|
logFatal() << "Did not understand bind address";
|
|
return 2;
|
|
}
|
|
}
|
|
else {
|
|
address = QHostAddress::Any;
|
|
}
|
|
|
|
if (!server->listen(address, port)) {
|
|
logCritical() << " Failed to listen on interface";
|
|
return 1;
|
|
}
|
|
logCritical() << "Listening on" << address.toString() << "port:" << port;
|
|
return 0;
|
|
#else
|
|
return 1; // it obviously failed if we didn't do anything.
|
|
#endif
|
|
}
|
|
|
|
int FloweeServiceApplication::bindWith(uint16_t defaultPort,
|
|
DefaultBindOption defaultBind,
|
|
const std::function<void(const boost::asio::ip::tcp::endpoint&)> &callback)
|
|
{
|
|
assert(m_parser);
|
|
assert(m_rebind.get() == nullptr);
|
|
m_rebind.reset(new NetworkRebindData());
|
|
m_rebind->bindOne_function = callback;
|
|
m_rebind->defaultPort = defaultPort;
|
|
m_rebind->defaultBind = defaultBind;
|
|
auto options = bindingEndPoints(defaultPort, defaultBind);
|
|
for (auto i = options.begin(); i != options.end(); ++i) {
|
|
try {
|
|
m_rebind->bindOne_function(*i);
|
|
m_rebind->boundInterfaces.push_back(*i);
|
|
} catch (const std::exception &e) {
|
|
}
|
|
}
|
|
#ifdef __linux__
|
|
m_rebind->netWatch = new std::thread(netWatch);
|
|
#endif
|
|
return (int) m_rebind->boundInterfaces.size();
|
|
}
|
|
|
|
|
|
void FloweeServiceApplication::networkSetupChanged()
|
|
{
|
|
assert(m_rebind.get());
|
|
if (m_rebind->triggeredRebind)
|
|
return;
|
|
m_rebind->triggeredRebind = true;
|
|
emit startRebindTriggered();
|
|
}
|
|
|
|
void FloweeServiceApplication::rebind()
|
|
{
|
|
m_rebind->triggeredRebind = false;
|
|
auto options = bindingEndPoints(m_rebind->defaultPort, m_rebind->defaultBind);
|
|
for (auto i = options.begin(); i != options.end(); ++i) {
|
|
if (m_rebind->boundInterfaces.contains(*i)) // skip allready bound ones.
|
|
continue;
|
|
try {
|
|
m_rebind->bindOne_function(*i);
|
|
m_rebind->boundInterfaces.push_back(*i);
|
|
} catch (const std::exception &e) {
|
|
}
|
|
}
|
|
}
|