Actual search results! Safer cleanup when interrupted. Consistent include order. And a bunch of other polish

This commit is contained in:
John Sennesael 2021-10-20 17:48:43 -05:00
parent f7831afc9d
commit 6b5bdadeb5
28 changed files with 424 additions and 251 deletions

View File

@ -56,6 +56,8 @@ class Application
std::string m_configFile{"usenetsearch.conf"}; std::string m_configFile{"usenetsearch.conf"};
Database m_db; Database m_db;
Filter m_filter; Filter m_filter;
/// Holds a pointer to the singleton instance.
static std::unique_ptr<Application> m_instance;
void ExecuteCustomOption( void ExecuteCustomOption(
std::shared_ptr<CommandLineOption>&, std::shared_ptr<CommandLineOption>&,
@ -93,10 +95,15 @@ public:
std::string defaultValue = "" std::string defaultValue = ""
); );
bool CanRun() const; bool CanRun() const;
/**
* Gets an instance to the application singleton.
*/
static Application& Get();
Configuration& GetConfig(); Configuration& GetConfig();
Database& GetDb(); Database& GetDb();
Filter& GetFilter(); Filter& GetFilter();
bool Init(int argc, char* argv[]); bool Init(int argc, char* argv[]);
bool ShouldStop() const;
void Usage(const std::string& programName); void Usage(const std::string& programName);
}; };

View File

@ -27,8 +27,8 @@ namespace usenetsearch {
struct ConfigurationException: public UsenetSearchException struct ConfigurationException: public UsenetSearchException
{ {
ConfigurationException(int errorCode, const std::string& message): ConfigurationException(const std::string& message):
UsenetSearchException(errorCode, message){} UsenetSearchException(message){}
virtual ~ConfigurationException() = default; virtual ~ConfigurationException() = default;
}; };

View File

@ -47,15 +47,14 @@ std::fstream& operator>>(std::fstream& in, ArticleEntry& obj);
struct DatabaseException: public UsenetSearchException struct DatabaseException: public UsenetSearchException
{ {
DatabaseException(int errorCode, const std::string& message): DatabaseException(const std::string& message):
UsenetSearchException(errorCode, message){} UsenetSearchException(message){}
virtual ~DatabaseException() = default; virtual ~DatabaseException() = default;
}; };
class Database class Database
{ {
Application& m_app;
std::filesystem::path m_databasePath; std::filesystem::path m_databasePath;
std::uint64_t m_databaseVersion{DatabaseVersion}; std::uint64_t m_databaseVersion{DatabaseVersion};
std::vector<std::filesystem::path> m_lockedFiles; std::vector<std::filesystem::path> m_lockedFiles;
@ -91,8 +90,9 @@ class Database
public: public:
explicit Database(Application& app); Database() = default;
~Database(); ~Database() = default;
std::unique_ptr<NntpListEntry> FindNntpEntry(std::uint64_t id);
std::unique_ptr<NntpListEntry> FindNntpEntry(const std::string& subject); std::unique_ptr<NntpListEntry> FindNntpEntry(const std::string& subject);
std::uint32_t GetLastIndexedArticle(std::uint64_t newsgroupID); std::uint32_t GetLastIndexedArticle(std::uint64_t newsgroupID);
std::unique_ptr<std::vector<NntpListEntry>> LoadNewsgroupList(); std::unique_ptr<std::vector<NntpListEntry>> LoadNewsgroupList();

View File

@ -29,8 +29,8 @@ namespace usenetsearch {
struct DnsResolveException: public UsenetSearchException struct DnsResolveException: public UsenetSearchException
{ {
DnsResolveException(int errorCode, const std::string& message): DnsResolveException(const std::string& message):
UsenetSearchException(errorCode, message){} UsenetSearchException(message){}
virtual ~DnsResolveException() = default; virtual ~DnsResolveException() = default;
}; };

View File

@ -23,15 +23,12 @@ namespace usenetsearch {
class UsenetSearchException: public std::exception class UsenetSearchException: public std::exception
{ {
int m_errorCode;
std::string m_message; std::string m_message;
public: public:
UsenetSearchException(int errorCode, const std::string& message); UsenetSearchException(const std::string& message);
virtual ~UsenetSearchException() = default; virtual ~UsenetSearchException() = default;
int Code() const;
virtual const char* what() const noexcept override; virtual const char* what() const noexcept override;
}; };

View File

@ -17,7 +17,7 @@
#pragma once #pragma once
#include "usenetsearch/Application.h" #include "usenetsearch/Database.h"
#include "usenetsearch/Filter.h" #include "usenetsearch/Filter.h"
#include "usenetsearch/UsenetClient.h" #include "usenetsearch/UsenetClient.h"
#include "usenetsearch/ThreadPool.h" #include "usenetsearch/ThreadPool.h"
@ -61,14 +61,13 @@ typedef std::vector<SearchResult> SearchResults;
class Indexer class Indexer
{ {
Application& m_app;
UsenetClient& m_client; UsenetClient& m_client;
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> m_conv; std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> m_conv;
ThreadPool m_threads; ThreadPool m_threads;
public: public:
Indexer(Application& app, UsenetClient& client); Indexer(UsenetClient& client);
void Connect(); void Connect();
void Index(const std::vector<NntpListEntry>& newsgroups); void Index(const std::vector<NntpListEntry>& newsgroups);

View File

@ -20,6 +20,7 @@
#include <string> #include <string>
#include <type_traits> #include <type_traits>
#include <memory> #include <memory>
#include <mutex>
#include <stdexcept> #include <stdexcept>
namespace usenetsearch { namespace usenetsearch {
@ -32,6 +33,8 @@ class Logger
/// Holds a pointer to the singleton instance. /// Holds a pointer to the singleton instance.
static std::unique_ptr<Logger> m_instance; static std::unique_ptr<Logger> m_instance;
std::mutex m_stdOutLock;
std::mutex m_stdErrLock;
public: public:
/** /**
@ -47,28 +50,28 @@ public:
/** /**
* Gets an instance to the logger. * Gets an instance to the logger.
*/ */
static Logger& get(); static Logger& Get();
/** /**
* Logs a debug message. * Logs a debug message.
* *
* @param[in] msg Message to be logged. * @param[in] msg Message to be logged.
*/ */
void debug(const std::string& msg) const; void Debug(const std::string& func, const std::string& msg);
/** /**
* Logs an info message. * Logs an info message.
* *
* @param[in] msg Message to be logged. * @param[in] msg Message to be logged.
*/ */
void info(const std::string& msg) const; void Info(const std::string& func, const std::string& msg);
/** /**
* Logs an error message. * Logs an error message.
* *
* @param[in] msg Message to be logged. * @param[in] msg Message to be logged.
*/ */
void error(const std::string& msg) const; void Error(const std::string& func, const std::string& msg);
/** /**
* Logs an error message and throws. * Logs an error message and throws.
@ -77,14 +80,18 @@ public:
* @param[in] msg Error message to be logged. * @param[in] msg Error message to be logged.
*/ */
template <typename EXCEPTION> template <typename EXCEPTION>
inline void fatal(const std::string& msg) const inline void Fatal(const std::string& func, const std::string& msg)
{ {
static_assert( static_assert(
std::is_base_of<std::exception, EXCEPTION>::value, std::is_base_of<std::exception, EXCEPTION>::value,
"EXCEPTION must be an std::exception or derrived child."); "EXCEPTION must be an std::exception or derrived child.");
error(msg); Error(func, msg);
throw EXCEPTION(msg); throw EXCEPTION(msg);
}; };
}; };
std::string LogName(const std::string& module, const std::string& func);
#define LOGID(module) LogName(module, __FUNCTION__)
} // namespace usenetsearch } // namespace usenetsearch

View File

@ -28,8 +28,8 @@ namespace usenetsearch {
struct SSLException: public UsenetSearchException struct SSLException: public UsenetSearchException
{ {
SSLException(int errorCode, const std::string& message): SSLException(const std::string& message):
UsenetSearchException(errorCode, message){} UsenetSearchException(message){}
virtual ~SSLException() = default; virtual ~SSLException() = default;
}; };

View File

@ -33,16 +33,16 @@ struct NntpListEntry;
struct FileIOException: public UsenetSearchException struct FileIOException: public UsenetSearchException
{ {
FileIOException(int errorCode, const std::string& message): FileIOException(const std::string& message):
UsenetSearchException(errorCode, message){} UsenetSearchException(message){}
virtual ~FileIOException() = default; virtual ~FileIOException() = default;
}; };
struct SerializeException: public UsenetSearchException struct SerializeException: public UsenetSearchException
{ {
SerializeException(int errorCode, const std::string& message): SerializeException(const std::string& message):
UsenetSearchException(errorCode, message){} UsenetSearchException(message){}
virtual ~SerializeException() = default; virtual ~SerializeException() = default;
}; };
@ -125,5 +125,4 @@ SerializableFile& operator>>(SerializableFile& in, NntpHeader& obj);
SerializableFile& operator<<(SerializableFile& out, const NntpListEntry& obj); SerializableFile& operator<<(SerializableFile& out, const NntpListEntry& obj);
SerializableFile& operator>>(SerializableFile& in, NntpListEntry& obj); SerializableFile& operator>>(SerializableFile& in, NntpListEntry& obj);
} // namespace usenetsearch } // namespace usenetsearch

View File

@ -29,8 +29,8 @@ namespace usenetsearch {
struct StringException: public UsenetSearchException struct StringException: public UsenetSearchException
{ {
StringException(int errorCode, const std::string& message): StringException(const std::string& message):
UsenetSearchException(errorCode, message){} UsenetSearchException(message){}
virtual ~StringException() = default; virtual ~StringException() = default;
}; };

View File

@ -27,8 +27,8 @@ namespace usenetsearch {
struct SocketException: public UsenetSearchException struct SocketException: public UsenetSearchException
{ {
SocketException(int errorCode, const std::string& message): SocketException(const std::string& message):
UsenetSearchException(errorCode, message){} UsenetSearchException(message){}
virtual ~SocketException() = default; virtual ~SocketException() = default;
}; };

View File

@ -36,8 +36,8 @@ class Application;
struct UsenetClientException: public UsenetSearchException struct UsenetClientException: public UsenetSearchException
{ {
UsenetClientException(int errorCode, const std::string& message): UsenetClientException(const std::string& message):
UsenetSearchException(errorCode, message){} UsenetSearchException(message){}
virtual ~UsenetClientException() = default; virtual ~UsenetClientException() = default;
}; };
@ -77,7 +77,6 @@ struct NntpListEntry
class UsenetClient class UsenetClient
{ {
Application& m_app;
std::unique_ptr<SSLConnection> m_ssl; std::unique_ptr<SSLConnection> m_ssl;
std::unique_ptr<TcpConnection> m_tcp; std::unique_ptr<TcpConnection> m_tcp;
bool m_useSSL{false}; bool m_useSSL{false};
@ -89,7 +88,7 @@ class UsenetClient
public: public:
UsenetClient(Application& app); UsenetClient() = default;
void Authenticate(const std::wstring& user, const std::wstring& password); void Authenticate(const std::wstring& user, const std::wstring& password);

View File

@ -17,8 +17,10 @@
#include "usenetsearch/Application.h" #include "usenetsearch/Application.h"
#include "usenetsearch/Logger.h"
#include "usenetsearch/StringUtils.h" #include "usenetsearch/StringUtils.h"
#include <csignal>
#include <filesystem> #include <filesystem>
#include <iostream> #include <iostream>
#include <string> #include <string>
@ -26,6 +28,15 @@
namespace usenetsearch { namespace usenetsearch {
sig_atomic_t _signalled_to_stop{0};
void sigHandler(int)
{
_signalled_to_stop = 1;
}
std::unique_ptr<Application> Application::m_instance;
void Application::AddBooleanOption( void Application::AddBooleanOption(
char option, char option,
const std::string& help, const std::string& help,
@ -88,6 +99,25 @@ void Application::AddStringOption(
m_commandLineArguments.emplace_back(std::move(val)); m_commandLineArguments.emplace_back(std::move(val));
} }
Application& Application::Get()
{
if (m_instance == nullptr)
{
m_instance = std::make_unique<Application>();
}
return *m_instance;
}
bool Application::ShouldStop() const
{
const bool result = _signalled_to_stop == 1;
if (result)
{
Logger::Get().Info(LOGID("Application"), "Been signalled to stop.");
}
return result;
}
void Application::Usage(const std::string& programName) void Application::Usage(const std::string& programName)
{ {
std::cout << "UsenetSearch - usenet search indexer" << std::endl; std::cout << "UsenetSearch - usenet search indexer" << std::endl;
@ -147,7 +177,7 @@ void Application::Usage(const std::string& programName)
std::cout << std::endl; std::cout << std::endl;
} }
Application::Application() : m_db(*this), m_filter(m_config) Application::Application() : m_filter(m_config)
{ {
std::cout.setf(std::ios::unitbuf); std::cout.setf(std::ios::unitbuf);
} }
@ -249,6 +279,11 @@ bool Application::Init(int argc, char* argv[])
{ {
ParseArgs(argc, argv); ParseArgs(argc, argv);
if (!m_canRun) return false; if (!m_canRun) return false;
// Install signal handlers.
void (*prev_handler)(int);
prev_handler = signal(SIGINT, sigHandler);
prev_handler = signal(SIGTERM, sigHandler);
prev_handler = signal(SIGTSTP, sigHandler);
// Read config, setup db // Read config, setup db
m_config.Open(m_configFile); m_config.Open(m_configFile);
m_db.MaxTreeDepth(m_config.MaxTreeDepth()); m_db.MaxTreeDepth(m_config.MaxTreeDepth());

View File

@ -17,6 +17,7 @@
#include "usenetsearch/Configuration.h" #include "usenetsearch/Configuration.h"
#include "usenetsearch/Logger.h"
#include "usenetsearch/ScopeExit.h" #include "usenetsearch/ScopeExit.h"
#include "usenetsearch/StringUtils.h" #include "usenetsearch/StringUtils.h"
@ -103,7 +104,8 @@ void Configuration::Open(const std::string& filename)
std::ifstream fin(filename.c_str()); std::ifstream fin(filename.c_str());
if (!fin.is_open()) if (!fin.is_open())
{ {
throw ConfigurationException(EINVAL, Logger::Get().Fatal<ConfigurationException>(
LOGID("Configuration"),
"Could not open configuration file: " + filename "Could not open configuration file: " + filename
); );
} }
@ -121,7 +123,8 @@ void Configuration::Open(const std::string& filename)
const auto kvp = StringSplit(line, std::string{":"}, 2); const auto kvp = StringSplit(line, std::string{":"}, 2);
if (kvp.size() != 2) if (kvp.size() != 2)
{ {
throw ConfigurationException(EINVAL, Logger::Get().Fatal<ConfigurationException>(
LOGID("Configuration"),
std::string("Invalid configuration in ") std::string("Invalid configuration in ")
+ filename + std::string(" line ") + filename + std::string(" line ")
+ std::to_string(line_nr) + std::to_string(line_nr)
@ -158,7 +161,8 @@ void Configuration::Open(const std::string& filename)
} }
catch (const std::regex_error& e) catch (const std::regex_error& e)
{ {
throw ConfigurationException(EINVAL, Logger::Get().Fatal<ConfigurationException>(
LOGID("Configuration"),
std::string("Invalid configuration in ") std::string("Invalid configuration in ")
+ filename + std::string(" line ") + filename + std::string(" line ")
+ std::to_string(line_nr) + " : Regular expression \"" + std::to_string(line_nr) + " : Regular expression \""
@ -175,7 +179,8 @@ void Configuration::Open(const std::string& filename)
} }
catch (const std::regex_error& e) catch (const std::regex_error& e)
{ {
throw ConfigurationException(EINVAL, Logger::Get().Fatal<ConfigurationException>(
LOGID("Configuration"),
std::string("Invalid configuration in ") std::string("Invalid configuration in ")
+ filename + std::string(" line ") + filename + std::string(" line ")
+ std::to_string(line_nr) + " : Regular expression \"" + std::to_string(line_nr) + " : Regular expression \""
@ -228,7 +233,8 @@ void Configuration::Open(const std::string& filename)
catch (const StringException& e) catch (const StringException& e)
{ {
fin.close(); fin.close();
throw ConfigurationException(EINVAL, Logger::Get().Fatal<ConfigurationException>(
LOGID("Configuration"),
std::string("Invalid configuration in ") std::string("Invalid configuration in ")
+ filename + std::string(" line ") + filename + std::string(" line ")
+ std::to_string(line_nr) + " - " + e.what() + std::to_string(line_nr) + " - " + e.what()
@ -241,7 +247,8 @@ void Configuration::Open(const std::string& filename)
} }
else else
{ {
throw ConfigurationException(EINVAL, Logger::Get().Fatal<ConfigurationException>(
LOGID("Configuration"),
std::string("Invalid configuration in ") std::string("Invalid configuration in ")
+ filename + std::string(" line ") + filename + std::string(" line ")
+ std::to_string(line_nr) + std::to_string(line_nr)

View File

@ -18,13 +18,13 @@
#include "usenetsearch/Database.h" #include "usenetsearch/Database.h"
#include "usenetsearch/Application.h" #include "usenetsearch/Application.h"
#include "usenetsearch/Logger.h"
#include "usenetsearch/StringUtils.h" #include "usenetsearch/StringUtils.h"
#include "usenetsearch/UsenetClient.h"
#include "usenetsearch/ScopeExit.h" #include "usenetsearch/ScopeExit.h"
#include "usenetsearch/Serialize.h" #include "usenetsearch/Serialize.h"
#include "usenetsearch/UsenetClient.h"
#include <iomanip> #include <iomanip>
#include <iostream>
#include <chrono> #include <chrono>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
@ -36,27 +36,42 @@ namespace usenetsearch {
// Database class -------------------------------------------------------------- // Database class --------------------------------------------------------------
Database::Database(Application& app): m_app(app)
{
}
Database::~Database()
{
}
void Database::CheckDbVersion(const SerializableFile& f) const void Database::CheckDbVersion(const SerializableFile& f) const
{ {
f.Seek(0); f.Seek(0);
const std::uint64_t ver = f.ReadInt64(); const std::uint64_t ver = f.ReadInt64();
if (ver != m_databaseVersion) if (ver != m_databaseVersion)
{ {
throw DatabaseException(EINVAL, Logger::Get().Fatal<DatabaseException>(
LOGID("Database"),
"Wrong database version - Got: " + std::to_string(ver) + " want: " "Wrong database version - Got: " + std::to_string(ver) + " want: "
+ std::to_string(m_databaseVersion) + std::to_string(m_databaseVersion)
); );
} }
} }
std::unique_ptr<NntpListEntry> Database::FindNntpEntry(std::uint64_t id)
{
const auto path = GetNewsGroupFilePath();
if (!std::filesystem::exists(path)) return nullptr;
SerializableFile io;
io.Open(path);
CheckDbVersion(io);
const std::uint64_t numGroups = io.ReadInt64();
std::unique_ptr<NntpListEntry> result = nullptr;
for (std::uint64_t n = 0; n != numGroups; ++n)
{
NntpListEntry entry;
io >> entry;
if (entry.id == id)
{
result = std::make_unique<NntpListEntry>(entry);
break;
}
}
return result;
}
std::unique_ptr<NntpListEntry> Database::FindNntpEntry( std::unique_ptr<NntpListEntry> Database::FindNntpEntry(
const std::string& subject) const std::string& subject)
{ {
@ -85,8 +100,11 @@ std::uint32_t Database::GetLastIndexedArticle(std::uint64_t newsgroupID)
const auto path = GetNewsGroupFilePath(); const auto path = GetNewsGroupFilePath();
if (!std::filesystem::exists(path)) if (!std::filesystem::exists(path))
{ {
throw DatabaseException(ENOTFOUND, "No indexed articles for newsgroup: " Logger::Get().Fatal<DatabaseException>(
+ std::to_string(newsgroupID)); LOGID("Database"),
"No indexed articles for newsgroup: "
+ std::to_string(newsgroupID)
);
} }
SerializableFile io; SerializableFile io;
io.Open(path); io.Open(path);
@ -101,8 +119,11 @@ std::uint32_t Database::GetLastIndexedArticle(std::uint64_t newsgroupID)
return entry.lastIndexedArticle; return entry.lastIndexedArticle;
} }
} }
throw DatabaseException(ENOTFOUND, "No indexed articles for newsgroup: " Logger::Get().Fatal<DatabaseException>(
+ std::to_string(newsgroupID)); LOGID("Database"),
"No indexed articles for newsgroup: " + std::to_string(newsgroupID)
);
return NntpListEntry::NOT_INDEXED;
} }
std::filesystem::path Database::GetTokenFilePath( std::filesystem::path Database::GetTokenFilePath(
@ -185,8 +206,8 @@ void Database::ParseTokenFile(
{ {
if (!std::filesystem::exists(dbFile)) if (!std::filesystem::exists(dbFile))
{ {
throw DatabaseException( Logger::Get().Fatal<DatabaseException>(
ENOTFOUND, LOGID("Database"),
"File does not exist: " + dbFile.string() "File does not exist: " + dbFile.string()
); );
} }
@ -195,6 +216,7 @@ void Database::ParseTokenFile(
const std::uint64_t tokenCount = io.ReadInt64(); const std::uint64_t tokenCount = io.ReadInt64();
for (std::uint64_t i = 0; i != tokenCount; ++i) for (std::uint64_t i = 0; i != tokenCount; ++i)
{ {
if (Application::Get().ShouldStop()) return;
ArticleEntry token; ArticleEntry token;
io >> token; io >> token;
onParse(token); onParse(token);
@ -221,9 +243,11 @@ void Database::SetLastIndexedArticle(
} }
if (!found) if (!found)
{ {
throw DatabaseException(EINVAL, Logger::Get().Fatal<DatabaseException>(
LOGID("Database"),
"Attempt to update newsgroup not found in database - id: " "Attempt to update newsgroup not found in database - id: "
+ std::to_string(newsgroupID)); + std::to_string(newsgroupID)
);
} }
UpdateNewsgroupList(*outItems); UpdateNewsgroupList(*outItems);
} }
@ -239,7 +263,7 @@ void Database::SaveSearchTokens(
" ", " ",
m_maxTreeDepth, m_maxTreeDepth,
[&](const std::string& subToken, const std::string& str){ [&](const std::string& subToken, const std::string& str){
const std::string tok = m_app.GetFilter().ProcessToken( const std::string tok = Application::Get().GetFilter().ProcessToken(
subToken, subToken,
str str
); );
@ -322,11 +346,6 @@ void Database::SaveToken(
io << std::uint64_t{1}; io << std::uint64_t{1};
} }
// write out token. // write out token.
#if 0
std::cout << "Token: " << subtoken << std::endl;
std::cout << "Saving into file: " << path << std::endl;
std::cout << "Token hash: " << HashBytesToString(token.hash) << std::endl << std::endl;
#endif
io << token; io << token;
} }
@ -352,15 +371,6 @@ std::unique_ptr<std::vector<ArticleEntry>> Database::Search(
const auto foundTokens = LoadTokens(path, searchToken); const auto foundTokens = LoadTokens(path, searchToken);
if (foundTokens->empty()) continue; if (foundTokens->empty()) continue;
result->insert(result->end(), foundTokens->begin(), foundTokens->end()); result->insert(result->end(), foundTokens->begin(), foundTokens->end());
std::cout << std::left << std::setw(searchString.length() + 7)
<< "token: " + searchToken
<< std::setw(3) << " | "
<< std::setw(10)
<< "db file: " << path.string()
<< std::setw(3) << " | "
<< std::setw(9)
<< "#results: " + std::to_string(foundTokens->size())
<< std::endl;
} }
return result; return result;
} }

View File

@ -17,6 +17,8 @@
#include "usenetsearch/Dns.h" #include "usenetsearch/Dns.h"
#include "usenetsearch/Logger.h"
#include <cerrno> #include <cerrno>
#include <thread> #include <thread>
@ -64,7 +66,8 @@ std::vector<struct addrinfo> DnsResolve(
} }
else else
{ {
throw DnsResolveException(getAddrInfoResult, Logger::Get().Fatal<DnsResolveException>(
LOGID("Dns"),
"Could not resolve host " + host + ": - Error (" "Could not resolve host " + host + ": - Error ("
+ std::to_string(getAddrInfoResult) + ") - " + std::to_string(getAddrInfoResult) + ") - "
+ gai_strerror(getAddrInfoResult) + gai_strerror(getAddrInfoResult)
@ -73,7 +76,8 @@ std::vector<struct addrinfo> DnsResolve(
} }
if (result == nullptr) if (result == nullptr)
{ {
throw DnsResolveException(ETIMEDOUT, Logger::Get().Fatal<DnsResolveException>(
LOGID("Dns"),
"Timed out trying to resolve host: " + host "Timed out trying to resolve host: " + host
); );
} }

View File

@ -19,17 +19,11 @@
namespace usenetsearch { namespace usenetsearch {
UsenetSearchException::UsenetSearchException(int errorCode, const std::string& message) UsenetSearchException::UsenetSearchException(const std::string& message)
{ {
m_errorCode = errorCode;
m_message = message; m_message = message;
} }
int UsenetSearchException::Code() const
{
return m_errorCode;
}
const char* UsenetSearchException::what() const noexcept const char* UsenetSearchException::what() const noexcept
{ {
return m_message.c_str(); return m_message.c_str();

View File

@ -36,7 +36,7 @@ void Filter::Init()
// Pre-compile regexes for all the subtokens that should be erased. // Pre-compile regexes for all the subtokens that should be erased.
std::for_each(eraseTokens.begin(), eraseTokens.end(), std::for_each(eraseTokens.begin(), eraseTokens.end(),
[&](const std::string& tok){ [&](const std::string& tok){
const std::wstring wtok = WideStringFromString(tok); const std::wstring wtok = StringToLower(WideStringFromString(tok));
m_eraseTokenRegexes.emplace( m_eraseTokenRegexes.emplace(
std::make_unique<std::wregex>(L"^" + wtok + L"\\s+"), std::make_unique<std::wregex>(L"^" + wtok + L"\\s+"),
std::wstring{} std::wstring{}
@ -80,17 +80,15 @@ bool Filter::ProcessNewsgroup(const std::string& newsgroup) const
std::string Filter::ProcessSearchString(const std::string& searchString) const std::string Filter::ProcessSearchString(const std::string& searchString) const
{ {
std::wstring str = WideStringFromString(searchString); std::wstring str = StringToLower(WideStringFromString(searchString));
std::remove_if(str.begin(), str.end(), [](wchar_t c){ std::remove_if(str.begin(), str.end(), [](wchar_t c){
// Remove control characters. // Remove control characters.
if (c < 0x20) return true; // ascii control chars if (c < 0x20) return true; // ascii control chars
if ((c > 0x7e) && (c < 0xa0)) return true; // utf8 control chars if ((c > 0x7e) && (c < 0xa0)) return true; // utf8 control chars
return false; // don't delete anything else return false; // don't delete anything else
}); });
// Remove Re: for obvious reasons
str = StringRemove(StringToLower(str), std::wstring{L"re:"});
// Remove punctuation and stuff by converting to whitespace // Remove punctuation and stuff by converting to whitespace
static std::wregex rxPunctuation(L"[\\.!?#$%^&~*\\(\\)\\+\\[\\]\"\\-<>]+"); static std::wregex rxPunctuation(L"[\\.!?#$%^&~*\\(\\)\\[\\]\"\\-<>]+");
str = std::regex_replace(str, rxPunctuation, L" "); str = std::regex_replace(str, rxPunctuation, L" ");
// Process erase subtoken list. // Process erase subtoken list.
std::for_each(m_eraseTokenRegexes.begin(), m_eraseTokenRegexes.end(), std::for_each(m_eraseTokenRegexes.begin(), m_eraseTokenRegexes.end(),

View File

@ -17,11 +17,10 @@
#include "usenetsearch/Indexer.h" #include "usenetsearch/Indexer.h"
#include "usenetsearch/Application.h"
#include "usenetsearch/Logger.h" #include "usenetsearch/Logger.h"
#include "usenetsearch/StringUtils.h" #include "usenetsearch/StringUtils.h"
#include <iostream>
namespace usenetsearch { namespace usenetsearch {
// SearchResult class ---------------------------------------------------------- // SearchResult class ----------------------------------------------------------
@ -110,48 +109,46 @@ bool SearchResult::operator<=(const SearchResult& other) const
// Indexer class --------------------------------------------------------------- // Indexer class ---------------------------------------------------------------
Indexer::Indexer(Application& app, UsenetClient& client) Indexer::Indexer(UsenetClient& client)
: m_app(app), m_client(client) : m_client(client)
{ {
m_threads.MaxThreads(m_app.GetConfig().MaxThreads()); m_threads.MaxThreads(Application::Get().GetConfig().MaxThreads());
} }
void Indexer::Connect() void Indexer::Connect()
{ {
m_client.Connect( m_client.Connect(
m_app.GetConfig().NNTPServerHost(), Application::Get().GetConfig().NNTPServerHost(),
m_app.GetConfig().NNTPServerPort(), Application::Get().GetConfig().NNTPServerPort(),
m_app.GetConfig().NNTPServerSSL() Application::Get().GetConfig().NNTPServerSSL()
); );
m_client.Authenticate( m_client.Authenticate(
m_conv.from_bytes(m_app.GetConfig().NNTPServerUser()), m_conv.from_bytes(Application::Get().GetConfig().NNTPServerUser()),
m_conv.from_bytes(m_app.GetConfig().NNTPServerPassword()) m_conv.from_bytes(Application::Get().GetConfig().NNTPServerPassword())
); );
} }
void Indexer::Index(const std::vector<NntpListEntry>& newsgroups) void Indexer::Index(const std::vector<NntpListEntry>& newsgroups)
{ {
/** const size_t batchSize = Application::Get().GetConfig().BatchSize();
* @todo Replace all stdout stuff with Logger class.
*/
const size_t batchSize = m_app.GetConfig().BatchSize();
for (const auto& group: newsgroups) for (const auto& group: newsgroups)
{ {
const std::wstring newsgroup = m_conv.from_bytes(group.name); const std::wstring newsgroup = m_conv.from_bytes(group.name);
std::cout << "Setting group to " << group.name << "..."; Logger::Get().Debug(LOGID("Indexer"), "Setting group to " + group.name);
std::cout.flush();
m_client.Group(newsgroup); m_client.Group(newsgroup);
std::cout << "DONE." << std::endl; Logger::Get().Debug(
std::cout << "Reading headers in " << group.name << " " LOGID("Indexer"),
<< "(.=" << batchSize << " headers)." << std::endl; "Reading headers in " + group.name + " "
std::cout.flush(); + "(batch size = " + std::to_string(batchSize) + " headers)."
);
std::atomic<std::uint64_t> headerCount{0}; std::atomic<std::uint64_t> headerCount{0};
const std::atomic<std::uint64_t> groupID = group.id; const std::atomic<std::uint64_t> groupID = group.id;
std::reference_wrapper<Database> dbref = std::ref(m_app.GetDb());
std::uint32_t startMessage = 0; std::uint32_t startMessage = 0;
try try
{ {
startMessage = dbref.get().GetLastIndexedArticle(groupID); startMessage = Application::Get().GetDb().GetLastIndexedArticle(
groupID
);
if (startMessage == NntpListEntry::NOT_INDEXED) if (startMessage == NntpListEntry::NOT_INDEXED)
{ {
startMessage = 0; startMessage = 0;
@ -165,22 +162,27 @@ void Indexer::Index(const std::vector<NntpListEntry>& newsgroups)
{ {
startMessage = 0; startMessage = 0;
} }
Logger::Get().Debug(
std::cout << "Indexing starting at message: " LOGID("Indexer"),
<< std::to_string(startMessage) << std::endl; "Indexing starting at message: "
+ std::to_string(startMessage)
);
if (Application::Get().ShouldStop()) return;
m_client.ProcessHeaders(startMessage, m_client.ProcessHeaders(startMessage,
[this, &startMessage, &headerCount, &dbref, &groupID](std::shared_ptr<NntpHeaders> headers){ [this, &startMessage, &headerCount, &groupID](std::shared_ptr<NntpHeaders> headers){
m_threads.Queue([this, headers, &startMessage, &headerCount, &dbref, &groupID](){ if (Application::Get().ShouldStop()) return;
m_threads.Queue([this, headers, &startMessage, &headerCount, &groupID](){
std::uint64_t lastArticle{0}; std::uint64_t lastArticle{0};
for (const auto& header: *headers) for (const auto& header: *headers)
{ {
if (Application::Get().ShouldStop()) return;
const std::uint64_t articleID{header.articleID}; const std::uint64_t articleID{header.articleID};
std::string subject = header.subject; std::string subject = header.subject;
subject = m_app.GetFilter().ProcessSearchString( subject = Application::Get().GetFilter().ProcessSearchString(
subject subject
); );
if (subject == "") continue; if (subject == "") continue;
dbref.get().SaveSearchTokens( Application::Get().GetDb().SaveSearchTokens(
groupID, groupID,
articleID, articleID,
subject subject
@ -195,20 +197,20 @@ void Indexer::Index(const std::vector<NntpListEntry>& newsgroups)
// Update last-indexed id for the newsgroup. // Update last-indexed id for the newsgroup.
if (startMessage < lastArticle) if (startMessage < lastArticle)
{ {
dbref.get().SetLastIndexedArticle( Application::Get().GetDb().SetLastIndexedArticle(
groupID, lastArticle groupID, lastArticle
); );
} }
std::cout << "."; Logger::Get().Debug("Indexer::Index", "Finished batch.");
std::cout.flush();
}); });
}, },
batchSize batchSize
); );
m_threads.JoinThreads(); m_threads.JoinThreads();
std::cout << "DONE." << std::endl; Logger::Get().Debug(
std::cout << "Saved " << headerCount << " headers." << std::endl; LOGID("Indexer"),
std::cout.flush(); "Saved " + std::to_string(headerCount) + " headers."
);
} }
} }
@ -216,13 +218,21 @@ std::unique_ptr<SearchResults> Indexer::Search(
const std::string& searchString) const std::string& searchString)
{ {
auto result = std::make_unique<SearchResults>(); auto result = std::make_unique<SearchResults>();
const std::string sstr = m_app.GetFilter().ProcessSearchString( const std::string sstr = Application::Get().GetFilter().ProcessSearchString(
searchString searchString
); );
auto searchResults = m_app.GetDb().Search(sstr); const auto searchHash = StringHashBytes(sstr);
auto searchResults = Application::Get().GetDb().Search(sstr);
if (!searchResults) return result; if (!searchResults) return result;
for(const ArticleEntry& entry: *searchResults) for(const ArticleEntry& entry: *searchResults)
{ {
if (Application::Get().ShouldStop())
{
Logger::Get().Fatal<UsenetSearchException>(
"Indexer",
"Interrupted."
);
}
SearchResult sr(entry); SearchResult sr(entry);
// Check if a matching entry already exists in the result set, if so, // Check if a matching entry already exists in the result set, if so,
// increment count. Otherwise, append a new entry. // increment count. Otherwise, append a new entry.
@ -230,6 +240,9 @@ std::unique_ptr<SearchResults> Indexer::Search(
if (it != result->end()) if (it != result->end())
{ {
(*it).Inc(); (*it).Inc();
// An exact match gets double points to ensure it's above other
// partial matches.
if (entry.hash == searchHash) (*it).Inc();
} }
else else
{ {

View File

@ -20,6 +20,7 @@
#include <string> #include <string>
#include <iostream> #include <iostream>
#include <memory> #include <memory>
#include <mutex>
namespace usenetsearch { namespace usenetsearch {
@ -27,7 +28,7 @@ std::unique_ptr<Logger> Logger::m_instance;
Logger::Logger() {} Logger::Logger() {}
Logger& Logger::get() Logger& Logger::Get()
{ {
if (m_instance == nullptr) if (m_instance == nullptr)
{ {
@ -36,19 +37,27 @@ Logger& Logger::get()
return *m_instance; return *m_instance;
} }
void Logger::debug(const std::string& msg) const void Logger::Debug(const std::string& func, const std::string& msg)
{ {
std::cout << "[debug] " << msg << std::endl << std::flush; std::lock_guard<std::mutex> lock(m_stdOutLock);
std::cout << "[debug] [" + func + "] " << msg << std::endl << std::flush;
} }
void Logger::info(const std::string& msg) const void Logger::Info(const std::string& func, const std::string& msg)
{ {
std::cout << "[info] " << msg << std::endl << std::flush; std::lock_guard<std::mutex> lock(m_stdOutLock);
std::cout << "[info] [" + func + "] " << msg << std::endl << std::flush;
} }
void Logger::error(const std::string& msg) const void Logger::Error(const std::string& func, const std::string& msg)
{ {
std::cerr << "[error] " << msg << std::endl << std::flush; std::lock_guard<std::mutex> lock(m_stdErrLock);
std::cerr << "[error] [" + func + "] " << msg << std::endl << std::flush;
}
std::string LogName(const std::string& module, const std::string& func)
{
return module + std::string{"::"} + func;
} }
} // namespace usenetsearch } // namespace usenetsearch

View File

@ -17,6 +17,8 @@
#include "usenetsearch/SSLConnection.h" #include "usenetsearch/SSLConnection.h"
#include "usenetsearch/Logger.h"
#include <openssl/ssl.h> #include <openssl/ssl.h>
#include <openssl/err.h> #include <openssl/err.h>
@ -38,8 +40,10 @@ SSLConnection::SSLReturnState SSLConnection::CheckSSLReturn(int ret)
if (result == SSL_ERROR_SYSCALL) if (result == SSL_ERROR_SYSCALL)
{ {
if (errno == 0) return SSLReturnState::SUCCESS; if (errno == 0) return SSLReturnState::SUCCESS;
throw SSLException(errno, std::string{"SSL error: "} Logger::Get().Fatal<SSLException>(
+ std::strerror(errno)); LOGID("SSLConnection"),
std::string{"SSL error: "} + std::strerror(errno)
);
} }
if (result == SSL_ERROR_SSL) if (result == SSL_ERROR_SSL)
{ {
@ -48,8 +52,10 @@ SSLConnection::SSLReturnState SSLConnection::CheckSSLReturn(int ret)
errorCode, errorCode,
nullptr nullptr
); );
throw SSLException(errorCode, Logger::Get().Fatal<SSLException>(
"SSL error: " + errorString); LOGID("SSLConnection"),
"SSL error: " + errorString
);
} }
return SSLReturnState::RETRY; return SSLReturnState::RETRY;
} }
@ -58,14 +64,16 @@ void SSLConnection::Connect()
{ {
if (m_tcpConnection == nullptr) if (m_tcpConnection == nullptr)
{ {
throw SSLException(EBADF, Logger::Get().Fatal<SSLException>(
LOGID("SSLConnection"),
"Null tcp connection when attempting ssl connect." "Null tcp connection when attempting ssl connect."
); );
} }
int fd = m_tcpConnection->FileDescriptor(); int fd = m_tcpConnection->FileDescriptor();
if (!fd) if (!fd)
{ {
throw SSLException(EBADF, Logger::Get().Fatal<SSLException>(
LOGID("SSLConnection"),
"Bad file descriptor (" + std::to_string(fd) "Bad file descriptor (" + std::to_string(fd)
+ ") when attempting to ssl connect." + ") when attempting to ssl connect."
); );
@ -93,8 +101,8 @@ void SSLConnection::Connect()
); );
if (timeDelta > m_connectionTimeout) if (timeDelta > m_connectionTimeout)
{ {
throw SSLException( Logger::Get().Fatal<SSLException>(
ETIMEDOUT, LOGID("SSLConnection"),
"Timed out while trying to establish SSL connection." "Timed out while trying to establish SSL connection."
); );
} }
@ -115,7 +123,8 @@ std::string SSLConnection::Read(size_t amount)
{ {
if (m_sslContext == nullptr) if (m_sslContext == nullptr)
{ {
throw SSLException(EBADF, Logger::Get().Fatal<SSLException>(
LOGID("SSLConnection"),
"Attempted to write over SSL socket without SSL context." "Attempted to write over SSL socket without SSL context."
); );
} }
@ -155,7 +164,8 @@ void SSLConnection::Write(const std::string& data)
{ {
if (m_sslContext == nullptr) if (m_sslContext == nullptr)
{ {
throw SSLException(EBADF, Logger::Get().Fatal<SSLException>(
LOGID("SSLConnection"),
"Attempted to write over SSL socket without SSL context." "Attempted to write over SSL socket without SSL context."
); );
} }
@ -184,8 +194,8 @@ void SSLConnection::Write(const std::string& data)
(timeDelta > m_ioTimeout) (timeDelta > m_ioTimeout)
|| (m_ioTimeout == std::chrono::milliseconds{0}) ) || (m_ioTimeout == std::chrono::milliseconds{0}) )
{ {
throw SSLException( Logger::Get().Fatal<SSLException>(
ETIMEDOUT, LOGID("SSLConnection"),
"Timed out while trying to write to SSL connection." "Timed out while trying to write to SSL connection."
); );
} }

View File

@ -15,7 +15,9 @@
#include "usenetsearch/Serialize.h" #include "usenetsearch/Serialize.h"
#include "usenetsearch/Application.h"
#include "usenetsearch/Database.h" #include "usenetsearch/Database.h"
#include "usenetsearch/Logger.h"
#include "usenetsearch/ScopeExit.h" #include "usenetsearch/ScopeExit.h"
#include "usenetsearch/UsenetClient.h" #include "usenetsearch/UsenetClient.h"
@ -26,11 +28,13 @@
#include <unistd.h> #include <unistd.h>
#include <cerrno> #include <cerrno>
#include <chrono>
#include <codecvt> #include <codecvt>
#include <cstdint> #include <cstdint>
#include <cstring> #include <cstring>
#include <locale> #include <locale>
#include <fstream> #include <fstream>
#include <thread>
#include <vector> #include <vector>
namespace usenetsearch { namespace usenetsearch {
@ -50,17 +54,34 @@ void SerializableFile::FileLock()
{ {
if (!m_fd) if (!m_fd)
{ {
throw FileIOException(EBADFD, Logger::Get().Fatal<FileIOException>(
LOGID("SerializableFile"),
"Attempt to write to a file (" + m_fileName.string() "Attempt to write to a file (" + m_fileName.string()
+ " ) that isn't open." + " ) that isn't open."
); );
} }
int ret{EWOULDBLOCK}; int ret{EWOULDBLOCK};
while ((ret == EWOULDBLOCK) || (ret == EINTR)) ret = flock(m_fd, LOCK_EX); while ((ret == EWOULDBLOCK) || (ret == EINTR))
{
if (Application::Get().ShouldStop())
{
Logger::Get().Fatal<FileIOException>(
LOGID("SerializableFile"),
"Interrupt while trying to establish a lock on file ("
+ m_fileName.string() + ")."
);
}
ret = flock(m_fd, LOCK_EX);
if ((ret == EWOULDBLOCK) || (ret == EINTR))
{
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
}
if (ret != 0) if (ret != 0)
{ {
throw FileIOException( Logger::Get().Fatal<FileIOException>(
errno, "Error trying to lock file: " + m_fileName.string() LOGID("SerializableFile"),
"Error trying to lock file: " + m_fileName.string()
+ " : " + std::strerror(errno) + " : " + std::strerror(errno)
); );
} }
@ -71,7 +92,8 @@ void SerializableFile::FileUnlock()
{ {
if (!m_fd) if (!m_fd)
{ {
throw FileIOException(EBADFD, Logger::Get().Fatal<FileIOException>(
LOGID("SerializableFile"),
"Attempt to write to a file (" + m_fileName.string() "Attempt to write to a file (" + m_fileName.string()
+ " ) that isn't open." + " ) that isn't open."
); );
@ -79,8 +101,9 @@ void SerializableFile::FileUnlock()
const int ret = flock(m_fd, LOCK_UN); const int ret = flock(m_fd, LOCK_UN);
if (ret != 0) if (ret != 0)
{ {
throw FileIOException( Logger::Get().Fatal<FileIOException>(
errno, "Error trying to unlock file: " + m_fileName.string() LOGID("SerializableFile"),
"Error trying to unlock file: " + m_fileName.string()
+ " : " + std::strerror(errno) + " : " + std::strerror(errno)
); );
} }
@ -108,8 +131,11 @@ void SerializableFile::Open(const std::string& fileName)
int ret = open(fileName.c_str(), flags, 0644); int ret = open(fileName.c_str(), flags, 0644);
if (ret < 0) if (ret < 0)
{ {
throw FileIOException(errno, "Could not open file: " + fileName Logger::Get().Fatal<FileIOException>(
+ " : " + std::string{std::strerror(errno)}); LOGID("SerializableFile"),
"Could not open file: " + fileName
+ " : " + std::string{std::strerror(errno)}
);
} }
m_fd = ret; m_fd = ret;
if (m_lockOnOpen) FileLock(); if (m_lockOnOpen) FileLock();
@ -119,7 +145,8 @@ void SerializableFile::RangeLock(size_t offset, size_t size) const
{ {
if (!m_fd) if (!m_fd)
{ {
throw FileIOException(EBADFD, Logger::Get().Fatal<FileIOException>(
LOGID("SerializableFile"),
"Attempt to write to a file (" + m_fileName.string() "Attempt to write to a file (" + m_fileName.string()
+ " ) that isn't open." + " ) that isn't open."
); );
@ -128,9 +155,11 @@ void SerializableFile::RangeLock(size_t offset, size_t size) const
Seek(offset, std::ios_base::beg); Seek(offset, std::ios_base::beg);
if (lockf(m_fd, F_LOCK, size) == -1) if (lockf(m_fd, F_LOCK, size) == -1)
{ {
throw FileIOException(errno, Logger::Get().Fatal<FileIOException>(
LOGID("SerializableFile"),
"Could not acquire a write lock on file: " + m_fileName.string() "Could not acquire a write lock on file: " + m_fileName.string()
+ " : " + std::strerror(errno)); + " : " + std::strerror(errno)
);
} }
Seek(pos, std::ios_base::beg); Seek(pos, std::ios_base::beg);
} }
@ -139,7 +168,8 @@ void SerializableFile::RangeUnlock(size_t offset, size_t size) const
{ {
if (!m_fd) if (!m_fd)
{ {
throw FileIOException(EBADFD, Logger::Get().Fatal<FileIOException>(
LOGID("SerializableFile"),
"Attempt to write to a file (" + m_fileName.string() "Attempt to write to a file (" + m_fileName.string()
+ " ) that isn't open." + " ) that isn't open."
); );
@ -148,9 +178,11 @@ void SerializableFile::RangeUnlock(size_t offset, size_t size) const
Seek(offset, std::ios_base::beg); Seek(offset, std::ios_base::beg);
if (lockf(m_fd, F_ULOCK, size) == -1) if (lockf(m_fd, F_ULOCK, size) == -1)
{ {
throw FileIOException(errno, Logger::Get().Fatal<FileIOException>(
LOGID("SerializableFile"),
"Could not release a write lock on file: " + m_fileName.string() "Could not release a write lock on file: " + m_fileName.string()
+ " : " + std::strerror(errno)); + " : " + std::strerror(errno)
);
} }
Seek(pos, std::ios_base::beg); Seek(pos, std::ios_base::beg);
} }
@ -159,7 +191,8 @@ std::string SerializableFile::ReadStr(size_t size) const
{ {
if (!m_fd) if (!m_fd)
{ {
throw FileIOException(EBADFD, Logger::Get().Fatal<FileIOException>(
LOGID("SerializableFile"),
"Attempt to write to a file (" + m_fileName.string() "Attempt to write to a file (" + m_fileName.string()
+ " ) that isn't open." + " ) that isn't open."
); );
@ -176,7 +209,8 @@ std::string SerializableFile::ReadStr(size_t size) const
const auto readNow = read(m_fd, &result[0], size); const auto readNow = read(m_fd, &result[0], size);
if (readNow == -1) if (readNow == -1)
{ {
throw FileIOException(errno, Logger::Get().Fatal<FileIOException>(
LOGID("SerializableFile"),
"Error while reading from file: " + m_fileName.string() "Error while reading from file: " + m_fileName.string()
+ " : " + std::strerror(errno) + " : " + std::strerror(errno)
); );
@ -219,7 +253,8 @@ void SerializableFile::Seek(
{ {
if (!m_fd) if (!m_fd)
{ {
throw FileIOException(EBADFD, Logger::Get().Fatal<FileIOException>(
LOGID("SerializableFile"),
"Attempt to write to a file (" + m_fileName.string() "Attempt to write to a file (" + m_fileName.string()
+ " ) that isn't open." + " ) that isn't open."
); );
@ -241,7 +276,8 @@ void SerializableFile::Seek(
const auto newOffset = lseek(m_fd, offset, whence); const auto newOffset = lseek(m_fd, offset, whence);
if (newOffset == -1) if (newOffset == -1)
{ {
throw FileIOException(errno, Logger::Get().Fatal<FileIOException>(
LOGID("SerializableFile"),
"Could not set cursor position in file: " + m_fileName.string() "Could not set cursor position in file: " + m_fileName.string()
+ " : " + std::strerror(errno) + " : " + std::strerror(errno)
); );
@ -261,7 +297,8 @@ size_t SerializableFile::Tell() const
{ {
if (!m_fd) if (!m_fd)
{ {
throw FileIOException(EBADFD, Logger::Get().Fatal<FileIOException>(
LOGID("SerializableFile"),
"Attempt to write to a file (" + m_fileName.string() "Attempt to write to a file (" + m_fileName.string()
+ " ) that isn't open." + " ) that isn't open."
); );
@ -269,7 +306,8 @@ size_t SerializableFile::Tell() const
const auto result = lseek(m_fd, 0, SEEK_CUR); const auto result = lseek(m_fd, 0, SEEK_CUR);
if (result == -1) if (result == -1)
{ {
throw FileIOException(errno, Logger::Get().Fatal<FileIOException>(
LOGID("SerializableFile"),
"Could not get cursor position in file: " + m_fileName.string() "Could not get cursor position in file: " + m_fileName.string()
+ " : " + std::strerror(errno) + " : " + std::strerror(errno)
); );
@ -301,7 +339,8 @@ void SerializableFile::Write(const char* bytes, size_t size) const
{ {
if (!m_fd) if (!m_fd)
{ {
throw FileIOException(EBADFD, Logger::Get().Fatal<FileIOException>(
LOGID("SerializableFile"),
"Attempt to write to a file (" + m_fileName.string() "Attempt to write to a file (" + m_fileName.string()
+ " ) that isn't open." + " ) that isn't open."
); );
@ -323,7 +362,8 @@ void SerializableFile::Write(const char* bytes, size_t size) const
{ {
if (errno == 0) continue; if (errno == 0) continue;
} }
throw FileIOException(errno, Logger::Get().Fatal<FileIOException>(
LOGID("SerializableFile"),
"Failure while writing to file: " + m_fileName.string() "Failure while writing to file: " + m_fileName.string()
+ " : " + std::strerror(errno) + " : " + std::strerror(errno)
); );
@ -452,7 +492,10 @@ SerializableFile& operator>>(SerializableFile& in, ArticleEntry& obj)
in >> STX; in >> STX;
if ((SOH != 1) || (STX != 2)) if ((SOH != 1) || (STX != 2))
{ {
throw SerializeException(EBADMSG, "Bad magic number in entry header."); Logger::Get().Fatal<SerializeException>(
LOGID("SerializableFile"),
"Bad magic number in entry header."
);
} }
for (std::uint8_t i = 0; i != 16; ++i) for (std::uint8_t i = 0; i != 16; ++i)
{ {
@ -464,7 +507,10 @@ SerializableFile& operator>>(SerializableFile& in, ArticleEntry& obj)
in >> EOT; in >> EOT;
if ((ETX != 3) || (EOT != 4)) if ((ETX != 3) || (EOT != 4))
{ {
throw SerializeException(EBADMSG, "Bad magic number in entry footer."); Logger::Get().Fatal<SerializeException>(
LOGID("SerializableFile"),
"Bad magic number in entry footer."
);
} }
return in; return in;
} }
@ -509,7 +555,8 @@ SerializableFile& operator>>(SerializableFile& in, NntpListEntry& obj)
in >> STX; in >> STX;
if ((SOH != 1) || (STX != 2)) if ((SOH != 1) || (STX != 2))
{ {
throw SerializeException(EBADMSG, Logger::Get().Fatal<SerializeException>(
LOGID("SerializableFile"),
"Bad magic number in NNTP entry header." "Bad magic number in NNTP entry header."
); );
} }
@ -524,7 +571,8 @@ SerializableFile& operator>>(SerializableFile& in, NntpListEntry& obj)
in >> EOT; in >> EOT;
if ((ETX != 3) || (EOT != 4)) if ((ETX != 3) || (EOT != 4))
{ {
throw SerializeException(EBADMSG, Logger::Get().Fatal<SerializeException>(
LOGID("SerializableFile"),
"Bad magic number in NNTP entry footer." "Bad magic number in NNTP entry footer."
); );
} }

View File

@ -17,6 +17,8 @@
#include "usenetsearch/StringUtils.h" #include "usenetsearch/StringUtils.h"
#include "usenetsearch/Logger.h"
#include <openssl/md5.h> #include <openssl/md5.h>
#include <algorithm> #include <algorithm>
@ -111,9 +113,11 @@ bool StringToBoolean(const std::string& str)
const std::string lstr = StringTrim(StringToLower(str)); const std::string lstr = StringTrim(StringToLower(str));
if ((lstr == "true") || (lstr == "yes") || (lstr == "1")) return true; if ((lstr == "true") || (lstr == "yes") || (lstr == "1")) return true;
if ((lstr == "false") || (lstr == "no") || (lstr == "0")) return false; if ((lstr == "false") || (lstr == "no") || (lstr == "0")) return false;
throw StringException(EINVAL, Logger::Get().Fatal<StringException>(
LOGID("StringUtils"),
"The string \"" + str + "\" is not a valid boolean value." "The string \"" + str + "\" is not a valid boolean value."
); );
return false;
} }
bool StringToBoolean(const std::wstring& str) bool StringToBoolean(const std::wstring& str)
@ -122,10 +126,12 @@ bool StringToBoolean(const std::wstring& str)
if ((lstr == L"true") || (lstr == L"yes") || (lstr == L"1")) return true; if ((lstr == L"true") || (lstr == L"yes") || (lstr == L"1")) return true;
if ((lstr == L"false") || (lstr == L"no") || (lstr == L"0")) return false; if ((lstr == L"false") || (lstr == L"no") || (lstr == L"0")) return false;
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> conv; std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> conv;
throw StringException(EINVAL, Logger::Get().Fatal<StringException>(
LOGID("StringUtils"),
"The string \"" + conv.to_bytes(str) "The string \"" + conv.to_bytes(str)
+ "\" is not a valid boolean value." + "\" is not a valid boolean value."
); );
return false;
} }
void StringTreeOperation( void StringTreeOperation(
@ -135,6 +141,7 @@ void StringTreeOperation(
std::function<void(const std::string& subToken, const std::string& str)> Fn) std::function<void(const std::string& subToken, const std::string& str)> Fn)
{ {
const auto tokens = StringSplit(searchString, splitBy); const auto tokens = StringSplit(searchString, splitBy);
std::vector<std::string> tokenList;
for (auto outerIt = tokens.begin(); outerIt != tokens.end(); outerIt++) for (auto outerIt = tokens.begin(); outerIt != tokens.end(); outerIt++)
{ {
for (size_t depth = 1; depth != maxDepth + 1; ++depth) for (size_t depth = 1; depth != maxDepth + 1; ++depth)
@ -142,7 +149,16 @@ void StringTreeOperation(
const auto endIt = outerIt + depth; const auto endIt = outerIt + depth;
const auto subset = std::vector<std::string>(outerIt, endIt); const auto subset = std::vector<std::string>(outerIt, endIt);
const auto subToken = StringJoin(subset, splitBy); const auto subToken = StringJoin(subset, splitBy);
Fn(subToken, searchString); // Check if we already have this token.
//
// For phrases like "we went here and then we went there" this would
// avoid indexing the tokens 'we' and 'went' and 'we went' twice.
if (std::find(tokenList.begin(), tokenList.end(), subToken)
== tokenList.end())
{
Fn(subToken, searchString);
tokenList.emplace_back(subToken);
}
if (endIt == tokens.end()) break; if (endIt == tokens.end()) break;
} }
} }

View File

@ -15,6 +15,7 @@
along with UsenetSearch. If not, see <https://www.gnu.org/licenses/>. along with UsenetSearch. If not, see <https://www.gnu.org/licenses/>.
*/ */
#include "usenetsearch/Logger.h"
#include "usenetsearch/TcpConnection.h" #include "usenetsearch/TcpConnection.h"
#include "usenetsearch/Dns.h" #include "usenetsearch/Dns.h"
@ -45,7 +46,8 @@ void TcpConnection::Connect(const std::string& host, std::uint16_t port)
const std::vector<struct addrinfo> addresses = DnsResolve(host, port); const std::vector<struct addrinfo> addresses = DnsResolve(host, port);
if (addresses.empty()) if (addresses.empty())
{ {
throw DnsResolveException(EDESTADDRREQ, Logger::Get().Fatal<DnsResolveException>(
LOGID("TcpConnection"),
"The provided host (" + host + ") did not resolve to an address." "The provided host (" + host + ") did not resolve to an address."
); );
} }
@ -66,8 +68,8 @@ void TcpConnection::Connect(const std::string& host, std::uint16_t port)
); );
if (timeDelta > m_connectionTimeout) if (timeDelta > m_connectionTimeout)
{ {
throw SocketException( Logger::Get().Fatal<SocketException>(
ETIMEDOUT, LOGID("TcpConnection"),
"Timed out while trying to connect to " + host + ":" "Timed out while trying to connect to " + host + ":"
+ std::to_string(port) + "." + std::to_string(port) + "."
); );
@ -75,7 +77,8 @@ void TcpConnection::Connect(const std::string& host, std::uint16_t port)
fd = socket(addr.ai_family, SOCK_STREAM, 0); fd = socket(addr.ai_family, SOCK_STREAM, 0);
if (fd < 0) if (fd < 0)
{ {
throw SocketException(errno, Logger::Get().Fatal<SocketException>(
LOGID("TcpConnection"),
"Failed to create socket - Error (" + std::to_string(errno) "Failed to create socket - Error (" + std::to_string(errno)
+ ") - " + std::strerror(errno) + ") - " + std::strerror(errno)
); );
@ -101,7 +104,8 @@ void TcpConnection::Connect(const std::string& host, std::uint16_t port)
else else
{ {
close(fd); close(fd);
throw SocketException(errno, Logger::Get().Fatal<SocketException>(
LOGID("TcpConnection"),
"Failed to connect to " + host + ":" "Failed to connect to " + host + ":"
+ std::to_string(port) + " - Error (" + std::to_string(port) + " - Error ("
+ std::to_string(errno) + ") - " + strerror(errno) + std::to_string(errno) + ") - " + strerror(errno)
@ -152,7 +156,8 @@ std::string TcpConnection::Read(size_t amount)
} }
else else
{ {
throw SocketException(errno, Logger::Get().Fatal<SocketException>(
LOGID("TcpConnection"),
"Error while reading from TCP socket (" + std::to_string(errno) "Error while reading from TCP socket (" + std::to_string(errno)
+ ") - " + std::strerror(errno) + ") - " + std::strerror(errno)
); );
@ -178,7 +183,8 @@ void TcpConnection::Write(const std::string& data)
if ((timeDelta > m_ioTimeout) if ((timeDelta > m_ioTimeout)
|| m_ioTimeout == std::chrono::milliseconds{0}) || m_ioTimeout == std::chrono::milliseconds{0})
{ {
throw SocketException(ETIMEDOUT, Logger::Get().Fatal<SocketException>(
LOGID("TcpConnection"),
"Timed out writing to TCP socket." "Timed out writing to TCP socket."
); );
} }
@ -191,7 +197,8 @@ void TcpConnection::Write(const std::string& data)
} }
else else
{ {
throw SocketException(errno, Logger::Get().Fatal<SocketException>(
LOGID("TcpConnection"),
"Error writing to tcp socket (" + std::to_string(errno) "Error writing to tcp socket (" + std::to_string(errno)
+ ") - " + std::strerror(errno) + ") - " + std::strerror(errno)
); );

View File

@ -19,6 +19,7 @@
#include "usenetsearch/Application.h" #include "usenetsearch/Application.h"
#include "usenetsearch/Except.h" #include "usenetsearch/Except.h"
#include "usenetsearch/Logger.h"
#include "usenetsearch/StringUtils.h" #include "usenetsearch/StringUtils.h"
#include <codecvt> #include <codecvt>
@ -32,10 +33,6 @@ namespace usenetsearch {
// UsenetClient class ---------------------------------------------------------- // UsenetClient class ----------------------------------------------------------
UsenetClient::UsenetClient(Application& app): m_app(app)
{
}
void UsenetClient::Authenticate( void UsenetClient::Authenticate(
const std::wstring& user, const std::wstring& user,
const std::wstring& password) const std::wstring& password)
@ -45,8 +42,8 @@ void UsenetClient::Authenticate(
auto response = ReadLine(); auto response = ReadLine();
if (IsError(response)) if (IsError(response))
{ {
throw UsenetClientException( Logger::Get().Fatal<UsenetClientException>(
response.code, LOGID("UsenetClient"),
"Error authenticating with NNTP server: " "Error authenticating with NNTP server: "
+ response.message + response.message
); );
@ -56,8 +53,8 @@ void UsenetClient::Authenticate(
response = ReadLine(); response = ReadLine();
if (IsError(response)) if (IsError(response))
{ {
throw UsenetClientException( Logger::Get().Fatal<UsenetClientException>(
response.code, LOGID("UsenetClient"),
"Error authenticating with NNTP server: " "Error authenticating with NNTP server: "
+ response.message + response.message
); );
@ -83,8 +80,8 @@ void UsenetClient::Connect(
} }
catch (const UsenetSearchException& e) catch (const UsenetSearchException& e)
{ {
throw UsenetClientException( Logger::Get().Fatal<UsenetClientException>(
e.Code(), LOGID("UsenetClient"),
"Error while trying to connect to host: " + host + ":" "Error while trying to connect to host: " + host + ":"
+ std::to_string(port) + " - " + e.what() + std::to_string(port) + " - " + e.what()
); );
@ -93,8 +90,8 @@ void UsenetClient::Connect(
const auto serverHello = ReadLine(); const auto serverHello = ReadLine();
if (IsError(serverHello)) if (IsError(serverHello))
{ {
throw UsenetClientException( Logger::Get().Fatal<UsenetClientException>(
serverHello.code, LOGID("UsenetClient"),
"Error received from NNTP server: " "Error received from NNTP server: "
+ serverHello.message + serverHello.message
); );
@ -108,8 +105,8 @@ void UsenetClient::Group(const std::wstring& newsgroup)
auto response = ReadLine(); auto response = ReadLine();
if (IsError(response)) if (IsError(response))
{ {
throw UsenetClientException( Logger::Get().Fatal<UsenetClientException>(
response.code, LOGID("UsenetClient"),
"Error changing group to " + StringFromWideString(newsgroup) + " : " "Error changing group to " + StringFromWideString(newsgroup) + " : "
+ response.message + response.message
); );
@ -124,8 +121,8 @@ NntpHeader UsenetClient::Head(std::uint64_t articleID)
auto response = ReadLine(); auto response = ReadLine();
if (IsError(response)) if (IsError(response))
{ {
throw UsenetClientException( Logger::Get().Fatal<UsenetClientException>(
response.code, LOGID("UsenetClient"),
"Error getting headers for article id " "Error getting headers for article id "
+ std::to_string(articleID) + std::to_string(articleID)
+ " : " + response.message + " : " + response.message
@ -164,8 +161,8 @@ std::unique_ptr<std::vector<NntpListEntry>> UsenetClient::List()
const auto response = ReadLine(); const auto response = ReadLine();
if (IsError(response)) if (IsError(response))
{ {
throw UsenetClientException( Logger::Get().Fatal<UsenetClientException>(
response.code, LOGID("UsenetClient"),
"Failed to fetch newsgroup list from server, " "Failed to fetch newsgroup list from server, "
+ std::string{"server responded with: "} + std::string{"server responded with: "}
+ response.message + response.message
@ -189,7 +186,7 @@ std::unique_ptr<std::vector<NntpListEntry>> UsenetClient::List()
entry.status = fields[4]; entry.status = fields[4];
entry.id = 0; // incremented by db when saving. entry.id = 0; // incremented by db when saving.
entry.lastIndexedArticle = NntpListEntry::NOT_INDEXED; entry.lastIndexedArticle = NntpListEntry::NOT_INDEXED;
if (m_app.GetFilter().ProcessNewsgroup(entry.name)) if (Application::Get().GetFilter().ProcessNewsgroup(entry.name))
{ {
result->emplace_back(entry); result->emplace_back(entry);
} }
@ -202,7 +199,9 @@ std::unique_ptr<std::vector<std::uint64_t>> UsenetClient::ListGroup(
const std::wstring& newsGroup) const std::wstring& newsGroup)
{ {
auto result = std::make_unique<std::vector<std::uint64_t>>(); auto result = std::make_unique<std::vector<std::uint64_t>>();
if (!m_app.GetFilter().ProcessNewsgroup(StringFromWideString(newsGroup))) if (!Application::Get().GetFilter().ProcessNewsgroup(
StringFromWideString(newsGroup))
)
{ {
return result; return result;
} }
@ -212,8 +211,8 @@ std::unique_ptr<std::vector<std::uint64_t>> UsenetClient::ListGroup(
const auto response = ReadLine(); const auto response = ReadLine();
if (IsError(response)) if (IsError(response))
{ {
throw UsenetClientException( Logger::Get().Fatal<UsenetClientException>(
response.code, LOGID("UsenetClient"),
"Failed to fetch newsgroup list from server, " "Failed to fetch newsgroup list from server, "
+ std::string{"server responded with: "} + std::string{"server responded with: "}
+ response.message + response.message
@ -241,8 +240,8 @@ void UsenetClient::ProcessHeaders(
const auto response = ReadLine(); const auto response = ReadLine();
if (IsError(response)) if (IsError(response))
{ {
throw UsenetClientException( Logger::Get().Fatal<UsenetClientException>(
response.code, LOGID("UsenetClient"),
"Error getting headers: " "Error getting headers: "
+ response.message + response.message
); );
@ -293,9 +292,11 @@ NntpMessage UsenetClient::ReadLine()
line = ReadUntil("\r\n"); line = ReadUntil("\r\n");
if (line.length() < 2) if (line.length() < 2)
{ {
throw UsenetSearchException(EPROTONOSUPPORT, Logger::Get().Fatal<UsenetClientException>(
LOGID("UsenetClient"),
"NNTP protocol error - invalid response from server: " "NNTP protocol error - invalid response from server: "
+ line); + line
);
} }
std::string codeStr = line.substr(0, 3); std::string codeStr = line.substr(0, 3);
result.code = std::stoi(codeStr); result.code = std::stoi(codeStr);

View File

@ -25,10 +25,9 @@ using namespace usenetsearch;
int main(int argc, char* argv[]) int main(int argc, char* argv[])
{ {
Application app;
std::string tokenFile{""}; std::string tokenFile{""};
std::string newsgroupFile{""}; std::string newsgroupFile{""};
app.AddFileOption( Application::Get().AddFileOption(
't', 't',
"token db file to dump.", "token db file to dump.",
[&tokenFile](const std::string& val) [&tokenFile](const std::string& val)
@ -36,7 +35,7 @@ int main(int argc, char* argv[])
tokenFile = val; tokenFile = val;
} }
); );
app.AddFileOption( Application::Get().AddFileOption(
'n', 'n',
"newsgroup file to dump.", "newsgroup file to dump.",
[&newsgroupFile](const std::string& val) [&newsgroupFile](const std::string& val)
@ -44,20 +43,24 @@ int main(int argc, char* argv[])
newsgroupFile = val; newsgroupFile = val;
} }
); );
if (!app.Init(argc, argv)) return 1; if (!Application::Get().Init(argc, argv)) return 1;
if (!tokenFile.empty()) if (!tokenFile.empty())
{ {
app.GetDb().ParseTokenFile(tokenFile, [](const ArticleEntry& token){ Application::Get().GetDb().ParseTokenFile(
std::cout << "Hash: " << HashBytesToString(token.hash) << " | " tokenFile,
<< "NewsgroupID: " << token.newsgroupID << " | " [](const ArticleEntry& token){
<< "ArticleID: " << token.articleID << std::endl; std::cout << "Hash: " << HashBytesToString(token.hash) << " | "
}); << "NewsgroupID: " << token.newsgroupID << " | "
<< "ArticleID: " << token.articleID << std::endl;
}
);
} }
if (!newsgroupFile.empty()) if (!newsgroupFile.empty())
{ {
const auto groups = app.GetDb().LoadNewsgroupList(); const auto groups = Application::Get().GetDb().LoadNewsgroupList();
for(const auto& group: *groups) for(const auto& group: *groups)
{ {
if (Application::Get().ShouldStop()) return 1;
std::cout << std::left std::cout << std::left
<< std::setw(9) << "Id: " + std::to_string(group.id) << std::setw(9) << "Id: " + std::to_string(group.id)
<< std::setw(3) << " | " << std::setw(3) << " | "

View File

@ -1,7 +1,8 @@
#include "usenetsearch/Application.h" #include "usenetsearch/Application.h"
#include "usenetsearch/UsenetClient.h"
#include "usenetsearch/Indexer.h" #include "usenetsearch/Indexer.h"
#include "usenetsearch/StringUtils.h"
#include "usenetsearch/UsenetClient.h"
#include <iostream> #include <iostream>
@ -9,28 +10,27 @@ using namespace usenetsearch;
int main(int argc, char* argv[]) int main(int argc, char* argv[])
{ {
Application app;
std::string searchString; std::string searchString;
app.AddStringOption('s', "Search string", Application::Get().AddStringOption('s', "Search string",
[&searchString](const std::string& s){ [&searchString](const std::string& s){
searchString = s; searchString = s;
} }
); );
int maxResults{0}; int maxResults{0};
app.AddIntegerOption('n', "Maximum results", Application::Get().AddIntegerOption('n', "Maximum results",
[&maxResults](int n){ [&maxResults](int n){
maxResults = n; maxResults = n;
} }
); );
if (!app.Init(argc, argv)) return 1; if (!Application::Get().Init(argc, argv)) return 1;
if (searchString.empty()) if (searchString.empty())
{ {
std::cerr << "Missing search string." << std::endl; std::cerr << "Missing search string." << std::endl;
app.Usage(argv[0]); Application::Get().Usage(argv[0]);
return 1; return 1;
} }
UsenetClient client(app); UsenetClient client;
Indexer idx(app, client); Indexer idx(client);
std::unique_ptr<SearchResults> results = idx.Search( std::unique_ptr<SearchResults> results = idx.Search(
searchString searchString
); );
@ -39,15 +39,24 @@ int main(int argc, char* argv[])
std::cout << "Nothing found." << std::endl; std::cout << "Nothing found." << std::endl;
return 0; return 0;
} }
idx.Connect();
size_t resultCounter{0}; size_t resultCounter{0};
for (const auto& sr: *results) for (const auto& sr: *results)
{ {
std::cout << std::left if (Application::Get().ShouldStop()) return 1;
<< std::setw(18) << "Newsgroup id: " + std::to_string(sr.NewsgroupId()) const std::unique_ptr<NntpListEntry> newsGroup
<< std::setw(4) << " | " = Application::Get().GetDb().FindNntpEntry(sr.NewsgroupId());
<< std::setw(17) << "Article id: " + std::to_string(sr.ArticleId()) if (!newsGroup) continue;
<< std::setw(4) << " | " client.Group(WideStringFromString(newsGroup->name));
<< std::setw(10) << "Hits: " + std::to_string(sr.Hits()) const NntpHeader headers = client.Head(sr.ArticleId());
std::cout
<< "Score: " + std::to_string(sr.Hits())
<< " | "
<< "Newsgroup: " + newsGroup->name
<< " | "
<< "Article id: " + std::to_string(sr.ArticleId())
<< " | "
<< "Subject: " + headers.subject
<< std::endl; << std::endl;
resultCounter++; resultCounter++;
if ((maxResults > 0) && (resultCounter >= maxResults)) break; if ((maxResults > 0) && (resultCounter >= maxResults)) break;

View File

@ -18,6 +18,7 @@
#include "usenetsearch/Application.h" #include "usenetsearch/Application.h"
#include "usenetsearch/Except.h" #include "usenetsearch/Except.h"
#include "usenetsearch/Indexer.h" #include "usenetsearch/Indexer.h"
#include "usenetsearch/Logger.h"
#include "usenetsearch/UsenetClient.h" #include "usenetsearch/UsenetClient.h"
#include <iostream> #include <iostream>
@ -26,30 +27,30 @@ using namespace usenetsearch;
int main(int argc, char* argv[]) int main(int argc, char* argv[])
{ {
Application app; if (!Application::Get().Init(argc, argv)) return 1;
if (!app.Init(argc, argv)) return 1;
UsenetClient client(app); UsenetClient client;
Indexer indexer(app, client); Indexer indexer(client);
std::cout << "Connecting to newsgroup server..."; Logger::Get().Debug(
LOGID("usenetindexd"),
"Connecting to newsgroup server..."
);
indexer.Connect(); indexer.Connect();
std::cout << "<OK>" << std::endl; Logger::Get().Debug(LOGID("usenetindexd"), "Connected.");
try try
{ {
std::cout << "Getting newsgroup list..."; Logger::Get().Debug(LOGID("usenetindexd"), "Getting newsgroup list...");
std::cout.flush();
auto list = client.List(); auto list = client.List();
app.GetDb().UpdateNewsgroupList(*list); Application::Get().GetDb().UpdateNewsgroupList(*list);
std::cout << "<DONE>" << std::endl; Logger::Get().Debug(
std::cout.flush(); LOGID("usenetindexd"),
std::cout << "Found " << list->size() << " newsgroups." << std::endl; "Found " + std::to_string(list->size()) + " newsgroups."
std::cout.flush(); );
indexer.Index(*list); indexer.Index(*list);
} }
catch (const UsenetSearchException& e) catch (const UsenetSearchException& e)
{ {
std::cerr << e.what() << std::endl;;
return 1; return 1;
} }
return 0; return 0;