Add procedures field & fields to get information about mu's database fields, similar to "mu info fields". Add docs & tests as well.
431 lines
11 KiB
C++
431 lines
11 KiB
C++
/*
|
|
** Copyright (C) 2025-2026 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
|
**
|
|
** 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, 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, write to the Free Software Foundation,
|
|
** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
**
|
|
*/
|
|
#include "config.h"
|
|
|
|
#include "mu-scm.hh"
|
|
|
|
#include <thread>
|
|
#include <unistd.h>
|
|
#include <errno.h>
|
|
|
|
#include <sys/stat.h>
|
|
#include <fcntl.h>
|
|
#include <unistd.h>
|
|
|
|
#include "mu-utils.hh"
|
|
|
|
#include "mu-scm-types.hh"
|
|
|
|
#include "mu-config.hh"
|
|
|
|
#ifdef HAVE_PTHREAD_SETNAME_NP
|
|
#include <pthread.h>
|
|
#endif
|
|
|
|
using namespace Mu;
|
|
using namespace Mu::Scm;
|
|
|
|
namespace {
|
|
SCM mu_mod; // The mu module
|
|
}
|
|
|
|
/**
|
|
* Create an alist for the relevant option items (i.e., command-line parameters)
|
|
*
|
|
* @param opts
|
|
*/
|
|
static void
|
|
init_options(const Options& opts)
|
|
{
|
|
SCM scm_opts = alist_add(SCM_EOL,
|
|
make_symbol("verbose?"), opts.verbose,
|
|
make_symbol("debug?"), opts.debug,
|
|
make_symbol("quiet?"), opts.quiet);
|
|
|
|
if (opts.muhome.empty())
|
|
scm_opts = alist_add(scm_opts, make_symbol("mu-home"), SCM_BOOL_F);
|
|
else
|
|
scm_opts = alist_add(scm_opts, make_symbol("mu-home"), opts.muhome);
|
|
|
|
|
|
scm_c_define("%options", scm_reverse_x(scm_opts, SCM_EOL));
|
|
}
|
|
|
|
/**
|
|
* Create an alist for the relevant system-configuration parameters
|
|
*/
|
|
static void
|
|
init_configuration()
|
|
{
|
|
SCM conf{SCM_EOL};
|
|
|
|
for (const auto& prop: Config::properties) {
|
|
|
|
if (none_of(prop.flags & Property::Flags::System))
|
|
continue;
|
|
|
|
const auto val{Config::default_value(prop)};
|
|
if (!val)
|
|
continue;
|
|
|
|
switch(prop.type) {
|
|
case Config::Type::Boolean:
|
|
conf = alist_add(conf, make_symbol(mu_format("{}?", prop.name, "?")),
|
|
Config::decode<Config::Type::Boolean>(*val));
|
|
break;
|
|
case Config::Type::String:
|
|
conf = alist_add(conf, make_symbol(prop.name), *val);
|
|
break;
|
|
default:
|
|
// other types are not used here yet.
|
|
break;
|
|
}
|
|
}
|
|
|
|
scm_c_define("%configuration", scm_reverse_x(conf, SCM_EOL));
|
|
}
|
|
|
|
static void
|
|
init_fields_info()
|
|
{
|
|
SCM fields_scm{SCM_EOL};
|
|
|
|
const auto search_type = [&](const Field& field)->SCM {
|
|
if (field.is_boolean_term())
|
|
return make_symbol("boolean");
|
|
else if (field.is_phrasable_term())
|
|
return make_symbol("phrase");
|
|
else if (field.is_contact())
|
|
return make_symbol("contact");
|
|
else if (field.is_range())
|
|
return make_symbol("range");
|
|
else
|
|
return SCM_BOOL_F;
|
|
};
|
|
|
|
field_for_each([&](const auto& field) {
|
|
SCM field_scm = alist_add(SCM_EOL,
|
|
make_symbol("field"), make_symbol(field.name),
|
|
make_symbol("name"), field.name,
|
|
make_symbol("shortcut"),
|
|
field.shortcut ? to_scm(field.shortcut) : SCM_BOOL_F,
|
|
make_symbol("value?"), field.is_value(),
|
|
make_symbol("search-type"), search_type(field));
|
|
fields_scm = scm_cons(scm_reverse_x(field_scm, SCM_EOL), fields_scm);
|
|
});
|
|
|
|
scm_c_define("%fields", scm_reverse_x(fields_scm, SCM_EOL));
|
|
}
|
|
|
|
|
|
static void
|
|
init_misc()
|
|
{
|
|
scm_define(make_symbol("level-critical"), to_scm(G_LOG_LEVEL_CRITICAL));
|
|
scm_define(make_symbol("level-warning"), to_scm(G_LOG_LEVEL_WARNING));
|
|
scm_define(make_symbol("level-info"), to_scm(G_LOG_LEVEL_INFO));
|
|
scm_define(make_symbol("level-debug"), to_scm(G_LOG_LEVEL_DEBUG));
|
|
}
|
|
|
|
static SCM
|
|
subr_cc_log(SCM level_scm, SCM str_scm) try {
|
|
constexpr auto func{"cc-log"};
|
|
|
|
const auto level{static_cast<GLogLevelFlags>(from_scm<int>(level_scm, func, 1))};
|
|
if (level != G_LOG_LEVEL_CRITICAL && level != G_LOG_LEVEL_WARNING &&
|
|
level != G_LOG_LEVEL_INFO && level != G_LOG_LEVEL_DEBUG)
|
|
throw ScmError{ScmError::Id::WrongType, func, 1, level_scm, "level"};
|
|
|
|
const auto str{from_scm<std::string>(str_scm, func, 2)};
|
|
|
|
g_log("mu-scm", level, "%s", str.c_str());
|
|
|
|
return SCM_UNSPECIFIED;
|
|
|
|
} catch (const ScmError& err) {
|
|
err.throw_scm();
|
|
}
|
|
|
|
static void
|
|
init_subrs()
|
|
{
|
|
#pragma GCC diagnostic push
|
|
#pragma GCC diagnostic ignored "-Wcast-function-type"
|
|
scm_c_define_gsubr("cc-log", 2/*req*/, 0/*opt*/, 0/*rst*/,
|
|
reinterpret_cast<scm_t_subr>(subr_cc_log));
|
|
#pragma GCC diagnostic pop
|
|
}
|
|
|
|
|
|
|
|
static const Result<std::string>
|
|
make_mu_scm_path(const std::string& fname) {
|
|
|
|
const std::string dir = []() {
|
|
if (const char *altpath{::getenv("MU_SCM_DIR")}; altpath)
|
|
return altpath;
|
|
else
|
|
return MU_SCM_DIR;
|
|
}();
|
|
|
|
auto fpath{join_paths(dir, fname)};
|
|
if (::access(fpath.c_str(), R_OK) != 0)
|
|
return Err(Error::Code::File, "cannot read {}: {}",
|
|
fpath, ::strerror(errno));
|
|
else
|
|
return Ok(std::move(fpath));
|
|
}
|
|
|
|
namespace {
|
|
std::string mu_scm_path;
|
|
std::string mu_scm_repl_path;
|
|
std::string mu_scm_socket_path;
|
|
constexpr auto SOCKET_PATH_ENV = "MU_SCM_SOCKET_PATH";
|
|
using StrVec = std::vector<std::string>;
|
|
StrVec scm_args;
|
|
std::thread scm_worker;
|
|
}
|
|
|
|
static Result<void>
|
|
prepare_run(const Mu::Options& opts, StrVec& args)
|
|
{
|
|
// do a checks _before_ entering guile, so we get a bit more civilized
|
|
// error message.
|
|
if (const auto path = make_mu_scm_path("mu-scm.scm"); path)
|
|
mu_scm_path = *path;
|
|
else
|
|
return Err(path.error());
|
|
|
|
if (const auto path = make_mu_scm_path("mu-scm-repl.scm"); path)
|
|
mu_scm_repl_path = *path;
|
|
else
|
|
return Err(path.error());
|
|
|
|
args = {"mu", "-l", mu_scm_path};
|
|
|
|
return Ok();
|
|
}
|
|
|
|
static void
|
|
maybe_remove_socket_path()
|
|
{
|
|
struct stat statbuf{};
|
|
const auto sock{mu_scm_socket_path};
|
|
|
|
// opportunistic, so no real warnings, but be careful deleting!
|
|
|
|
if (::stat(sock.c_str(), &statbuf) != 0) {
|
|
mu_debug("can't stat '{}': {}", sock, ::strerror(errno));
|
|
} else if ((statbuf.st_mode & S_IFMT) != S_IFSOCK) {
|
|
mu_debug("{} is not a socket", sock);
|
|
} else if (::unlink(sock.c_str()) != 0) {
|
|
mu_debug("failed to unlink '{}': {}", sock, ::strerror(errno));
|
|
} else {
|
|
mu_debug("unlinked {}", sock);
|
|
}
|
|
}
|
|
|
|
struct ModMuData { const Mu::Store& store; const Mu::Options& opts; };
|
|
|
|
static void
|
|
init_module_mu(void* data)
|
|
{
|
|
const ModMuData& conf{*reinterpret_cast<ModMuData*>(data)};
|
|
|
|
init_options(conf.opts);
|
|
init_configuration();
|
|
init_fields_info();
|
|
|
|
init_misc();
|
|
init_subrs();
|
|
|
|
init_store(conf.store);
|
|
init_message();
|
|
init_mime();
|
|
}
|
|
|
|
static void
|
|
run_scm(const Mu::Store& store, const Mu::Options& opts)
|
|
{
|
|
static ModMuData mu_data{store, opts};
|
|
|
|
scm_boot_guile(0, {},
|
|
[](auto _data, auto _argc, auto _argv) {
|
|
mu_mod = scm_c_define_module ("mu", init_module_mu, &mu_data);
|
|
std::vector<char*> args;
|
|
std::ranges::transform(scm_args, std::back_inserter(args),
|
|
[](const std::string& strarg){
|
|
/* ahem...*/
|
|
return const_cast<char*>(strarg.c_str());
|
|
});
|
|
scm_shell(args.size(), args.data());
|
|
|
|
}, {}); // never returns.
|
|
}
|
|
|
|
Result<void>
|
|
Mu::Scm::run_repl(const Mu::Store& store, const Mu::Options& opts,
|
|
const std::string& socket_path)
|
|
{
|
|
if (const auto res = prepare_run(opts, scm_args); !res)
|
|
return Err(res.error());
|
|
|
|
scm_args.emplace_back("--no-auto-compile");
|
|
scm_args.emplace_back("-l");
|
|
scm_args.emplace_back(mu_scm_repl_path);
|
|
|
|
if (!socket_path.empty()) {
|
|
mu_scm_socket_path = socket_path;
|
|
g_setenv(SOCKET_PATH_ENV, mu_scm_socket_path.c_str(), 1);
|
|
mu_info("setting up socket-path {}", mu_scm_socket_path);
|
|
::atexit(maybe_remove_socket_path); //opportunistic cleanup
|
|
|
|
// if a socket-path is provided, run in a background thread
|
|
// and offer a REPL on a Unix domain socket on said socket_path
|
|
auto worker = std::thread([&](){
|
|
set_thread_name("mu-scm");
|
|
run_scm(store, opts);
|
|
});
|
|
worker.detach();
|
|
} else { // otherwise, a normal, interactive shell
|
|
g_unsetenv(SOCKET_PATH_ENV);
|
|
run_scm(store, opts);
|
|
}
|
|
|
|
return Ok();
|
|
}
|
|
|
|
Result<void>
|
|
Mu::Scm::run_script(const Mu::Store& store, const Mu::Options& opts,
|
|
const std::string& script_path)
|
|
{
|
|
if (script_path.empty())
|
|
return Err(Error::Code::InvalidArgument, "missing script path");
|
|
|
|
if (const auto res = ::access(script_path.c_str(), R_OK); res != 0)
|
|
return Err(Error::Code::InvalidArgument,
|
|
"cannot read '{}': {}", script_path, ::strerror(errno));
|
|
|
|
if (const auto res = prepare_run(opts, scm_args); !res)
|
|
return Err(res.error());
|
|
|
|
// XXX: couldn't get another combination of -l/-s/-e/-c to work
|
|
// a) invokes `main' with arguments, and
|
|
// b) exits (rather than drop to a shell)
|
|
// but, what works is to manually specify (main ....)
|
|
std::string cmd = "(main " + quote(script_path);
|
|
for (const auto& scriptarg : opts.scm.params)
|
|
cmd += " " + quote(scriptarg);
|
|
cmd += ")";
|
|
|
|
scm_args.emplace_back("-l");
|
|
scm_args.emplace_back(script_path);
|
|
scm_args.emplace_back("-c");
|
|
scm_args.emplace_back(cmd);
|
|
|
|
run_scm(store, opts);
|
|
|
|
return Ok();
|
|
}
|
|
|
|
Result<void>
|
|
Mu::Scm::run_eval(const Mu::Store& store, const Mu::Options& opts, const std::string& expr)
|
|
{
|
|
if (const auto res = prepare_run(opts, scm_args); !res)
|
|
return Err(res.error());
|
|
|
|
scm_args.emplace_back("-c");
|
|
// a little hacky...
|
|
scm_args.emplace_back("(use-modules ((mu))) " + expr);
|
|
|
|
run_scm(store, opts);
|
|
|
|
return Ok();
|
|
}
|
|
|
|
|
|
|
|
#ifdef BUILD_TESTS
|
|
|
|
/*
|
|
* Tests.
|
|
*
|
|
*/
|
|
#include <config.h>
|
|
#include <mu-store.hh>
|
|
#include "utils/mu-test-utils.hh"
|
|
|
|
static void
|
|
test_scm_script()
|
|
{
|
|
TempDir tempdir{};
|
|
const auto MuTestMaildir{ Mu::canonicalize_filename(MU_TESTMAILDIR, "/")};
|
|
|
|
::setenv("MU_TESTTEMPDIR", tempdir.path().c_str(), 1);
|
|
|
|
MemDb mdb;
|
|
Config conf{mdb};
|
|
conf.set<Config::Id::PersonalAddresses>(
|
|
std::vector<std::string>{"user@example.com"});
|
|
|
|
auto store{Store::make_new(tempdir.path(), MuTestMaildir, conf)};
|
|
assert_valid_result(store);
|
|
|
|
{
|
|
const auto res = store->indexer().start({}, true/*block*/);
|
|
g_assert_true(res);
|
|
}
|
|
|
|
// add some label for testing
|
|
{
|
|
auto res = store->run_query("optimization");
|
|
const Labels::DeltaLabelVec labels{*Labels::parse_delta_label("+performance")};
|
|
assert_valid_result(res);
|
|
g_assert_cmpuint(res->size(), ==, 4);
|
|
for (auto& it: *res) {
|
|
auto msg{it.message()};
|
|
g_assert_true(!!msg);
|
|
const auto updateres{store->update_labels(*msg, labels)};
|
|
assert_valid_result(updateres);
|
|
}
|
|
}
|
|
|
|
Mu::Options opts{};
|
|
{
|
|
const auto script_path{join_paths(MU_SCM_SRCDIR, "mu-scm-test.scm")};
|
|
const auto res = Mu::Scm::run_script(*store, opts, script_path);
|
|
assert_valid_result(res);
|
|
}
|
|
}
|
|
|
|
int
|
|
main(int argc, char* argv[])
|
|
{
|
|
::setenv("MU_SCM_DIR", MU_SCM_SRCDIR, 1);
|
|
::setenv("MU_TESTDATADIR", MU_TESTDATADIR, 1);
|
|
|
|
mu_test_init(&argc, &argv);
|
|
|
|
g_test_add_func("/scm/script", test_scm_script);
|
|
|
|
return g_test_run();
|
|
}
|
|
|
|
#endif /*BUILD_TESTS*/
|