scm: implement blocking / non-blocking modes

Implement running the REPL on background thread. That way, we can _share_ the
store&.
This commit is contained in:
Dirk-Jan C. Binnema
2025-08-23 09:13:40 +03:00
parent 81ff303d2e
commit d5a0fce4cf
5 changed files with 177 additions and 113 deletions

View File

@ -1,5 +1,5 @@
/* /*
** Copyright (C) 2010-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> ** Copyright (C) 2010-2025 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
** **
** This program is free software; you can redistribute it and/or modify it ** 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 ** under the terms of the GNU General Public License as published by the
@ -73,7 +73,7 @@ cmd_scm(const Store& store, const Options& opts)
return Err(Error::Code::InvalidArgument, return Err(Error::Code::InvalidArgument,
"scm/guile is not available in this build"); "scm/guile is not available in this build");
#else #else
return Mu::Scm::run(Mu::Scm::Config{store, opts}); return Mu::Scm::run(store, opts, true/*blocking*/);
#endif /*BUILD_SCM*/ #endif /*BUILD_SCM*/
} }

View File

@ -21,7 +21,7 @@
;; after printing UNIX-CONNECT:<socket-file>\n on stdout ;; after printing UNIX-CONNECT:<socket-file>\n on stdout
(let ((socket-path (getenv "MU_SCM_SOCKET_PATH"))) (let ((socket-path (getenv "MU_SCM_SOCKET_PATH")))
(when socket-path (when socket-path
(format #t "UNIX-CONNECT:~a\n" socket-path) (format #t "~a\n" socket-path)
(run-server (run-server
(make-unix-domain-server-socket #:path socket-path)))) (make-unix-domain-server-socket #:path socket-path))))

View File

@ -16,23 +16,31 @@
** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. ** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
** **
*/ */
#include "config.h"
#include "mu-scm.hh" #include "mu-scm.hh"
#include <thread>
#include <unistd.h> #include <unistd.h>
#include <errno.h> #include <errno.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include "mu-utils.hh" #include "mu-utils.hh"
#include "config.h"
#include "mu-scm-types.hh" #include "mu-scm-types.hh"
#ifdef HAVE_PTHREAD_SETNAME_NP
#include <pthread.h>
#endif
using namespace Mu; using namespace Mu;
using namespace Mu::Scm; using namespace Mu::Scm;
namespace { namespace {
static const Mu::Scm::Config *config{}; SCM mu_mod; // The mu module
static SCM mu_mod; // The mu module
} }
/** /**
@ -56,15 +64,6 @@ init_options(const Options& opts)
scm_c_define("%options", scm_opts); scm_c_define("%options", scm_opts);
} }
static void
init_module_mu(void* _data)
{
init_options(config->options);
init_store(config->store);
init_message();
init_mime();
}
static const Result<std::string> static const Result<std::string>
make_mu_scm_path(const std::string& fname) { make_mu_scm_path(const std::string& fname) {
@ -84,23 +83,20 @@ make_mu_scm_path(const std::string& fname) {
} }
namespace { namespace {
static std::string mu_scm_path; std::string mu_scm_path;
static std::string mu_scm_repl_path; std::string mu_scm_repl_path;
static std::string mu_scm_socket_path; std::string mu_scm_socket_path;
constexpr auto SOCKET_PATH_ENV = "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> static Result<void>
prepare_run(const Mu::Scm::Config& conf) prepare_run(const Mu::Options& opts)
{ {
if (config)
return Err(Error{Error::Code::AccessDenied,
"already prepared"});
config = &conf;
// do a checks _before_ entering guile, so we get a bit more civilized // do a checks _before_ entering guile, so we get a bit more civilized
// error message. // error message.
if (const auto path = make_mu_scm_path("mu-scm.scm"); path) if (const auto path = make_mu_scm_path("mu-scm.scm"); path)
mu_scm_path = *path; mu_scm_path = *path;
else else
@ -111,8 +107,8 @@ prepare_run(const Mu::Scm::Config& conf)
else else
return Err(path.error()); return Err(path.error());
if (config->options.scm.script_path) { if (opts.scm.script_path) {
const auto path{config->options.scm.script_path->c_str()}; const auto path{opts.scm.script_path->c_str()};
if (const auto res = ::access(path, R_OK); res != 0) { if (const auto res = ::access(path, R_OK); res != 0) {
return Err(Error::Code::InvalidArgument, return Err(Error::Code::InvalidArgument,
"cannot read '{}': {}", path, ::strerror(errno)); "cannot read '{}': {}", path, ::strerror(errno));
@ -122,71 +118,129 @@ prepare_run(const Mu::Scm::Config& conf)
return Ok(); return Ok();
} }
// make a unique unix-socket path static void
static std::string prepare_script(const Options& opts, StrVec& args)
maybe_set_uds_path(bool set)
{ {
if (set) { static std::string cmd; // keep alive
GRand* grand{g_rand_new()};
auto path = join_paths(g_get_user_runtime_dir(), // XXX: couldn't get another combination of -l/-s/-e/-c to work
mu_format("mu-scm-socket-{:08x}", // a) invokes `main' with arguments, and
g_rand_int(grand))); // b) exits (rather than drop to a shell)
g_rand_free(grand); // but, what works is to manually specify (main ....)
g_setenv(SOCKET_PATH_ENV, path.c_str(), 1); cmd = "(main " + quote(*opts.scm.script_path);
return path; for (const auto& scriptarg : opts.scm.params)
} else { cmd += " " + quote(scriptarg);
g_unsetenv(SOCKET_PATH_ENV); cmd += ")";
return {};
args.emplace_back("-l");
args.emplace_back(*opts.scm.script_path);
args.emplace_back("-c");
args.emplace_back(cmd);
}
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 (const int res = ::stat(sock.c_str(), &statbuf); res != 0) {
mu_debug("can't stat '{}'; err={}", sock, -res);
} else if ((statbuf.st_mode & S_IFMT) != S_IFSOCK) {
mu_debug("{} is not a socket", sock);
} else if (const int ulres = ::unlink(sock.c_str()); ulres != 0) {
mu_debug("failed to unlink '{}'; err={}", sock, -ulres);
} else {
mu_debug("unlinked {}", sock);
} }
} }
Result<void>
Mu::Scm::run(const Mu::Scm::Config& conf) static void
prepare_shell(const Options& opts, StrVec& args)
{ {
if (const auto res = prepare_run(conf); !res) // drop us into an interactive shell/repl or start listening on a domain socket.
return Err(res.error()); if (opts.scm.listen && opts.scm.socket_path) {
mu_scm_socket_path = *opts.scm.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
}
else
g_unsetenv(SOCKET_PATH_ENV);
scm_boot_guile(0, {}, [](void *data, int argc, char **argv) { args.emplace_back("--no-auto-compile");
mu_mod = scm_c_define_module ("mu", init_module_mu, {}); args.emplace_back("-l");
args.emplace_back(mu_scm_repl_path);
std::vector<const char*> args {
"mu",
"-l", mu_scm_path.c_str(),
};
std::string cmd;
const auto opts{config->options.scm};
// if a script-path was specified, run a script
if (opts.script_path) {
// 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 ....)
cmd = "(main " + quote(*opts.script_path);
for (const auto& scriptarg : opts.params)
cmd += " " + quote(scriptarg);
cmd += ")";
for (const auto& arg: {
"-l", opts.script_path->c_str(),
"-c", cmd.c_str()})
args.emplace_back(arg);
} else {
// otherwise, drop us into an interactive shell/repl
// or start listening on a domain socket.
mu_scm_socket_path =
maybe_set_uds_path(config->options.scm.listen);
args.emplace_back("--no-auto-compile");
args.emplace_back("-l");
args.emplace_back(mu_scm_repl_path.c_str());
}
/* ahem...*/
scm_shell(std::size(args), const_cast<char**>(args.data()));
}, {}); // never returns.
return Ok();
} }
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_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::transform(scm_args.begin(),
scm_args.end(), 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(const Mu::Store& store, const Mu::Options& opts, bool blocking)
{
if (const auto res = prepare_run(opts); !res)
return Err(res.error());
scm_args = {"mu", "-l", mu_scm_path};
// do env stuff _before_ starting guile / threads.
if (opts.scm.script_path)
prepare_script(opts, scm_args);
else
prepare_shell(opts, scm_args);
// in the non-blocking case, we start guile in a
// background thread; otherwise it will block.
if (!blocking) {
auto worker = std::thread([&](){
#ifdef HAVE_PTHREAD_SETNAME_NP
pthread_setname_np(pthread_self(), "mu-scm");
#endif /*HAVE_PTHREAD_SETNAME_NP*/
run_scm(store, opts);
});
worker.detach();
} else
run_scm(store, opts);
return Ok();
}
#ifdef BUILD_TESTS #ifdef BUILD_TESTS
@ -199,7 +253,6 @@ Mu::Scm::run(const Mu::Scm::Config& conf)
#include <mu-store.hh> #include <mu-store.hh>
#include "utils/mu-test-utils.hh" #include "utils/mu-test-utils.hh"
static void static void
test_scm_script() test_scm_script()
{ {
@ -233,13 +286,8 @@ test_scm_script()
Mu::Options opts{}; Mu::Options opts{};
opts.scm.script_path = join_paths(MU_SCM_SRCDIR, "mu-scm-test.scm"); opts.scm.script_path = join_paths(MU_SCM_SRCDIR, "mu-scm-test.scm");
Mu::Scm::Config scm_conf {
/*.store =*/ *store,
/*.options =*/ opts
};
{ {
const auto res = Mu::Scm::run(scm_conf); const auto res = Mu::Scm::run(*store, opts, false /*blocks*/);
assert_valid_result(res); assert_valid_result(res);
} }
} }

View File

@ -39,28 +39,24 @@
* *
*/ */
namespace Mu::Scm { namespace Mu::Scm {
/** /**
* Configuration object * Start a guile REPL or program
* *
*/ * Initialize the Scm sub-system, then start a REPL or run a script,
struct Config {
const Mu::Store& store;
const Options& options;
};
/**
* Start a guile shell
*
* Initialize the Scm sub-system, then start a shell or run a script,
* based on the configuration. * based on the configuration.
* *
* @param conf a Config object * Unless 'blocking' is false or there is some pre-guile error, this
* method never returns. If blocking is false, it runs in the
* background.
*
* @param store a Store object
* @param opts options
* @param blocking whether to block (or run in the background)
* *
* @return Ok() or some error * @return Ok() or some error
*/ */
Result<void> run(const Config& conf); Result<void> run(const Store& store, const Options& opts,
bool blocking=true);
/** /**
* Helpers * Helpers

View File

@ -107,6 +107,7 @@ Indices
* Starting the REPL:: * Starting the REPL::
* Listening on a Unix Domain Socket:: * Listening on a Unix Domain Socket::
* Hooking up with GNU/Emacs and Geiser:: * Hooking up with GNU/Emacs and Geiser::
* Hooking up with Mu4e::
@end menu @end menu
This chapter walks you through the installation and basic setup. This chapter walks you through the installation and basic setup.
@ -183,19 +184,19 @@ REPL over a Unix domain socket, using the @t{--listen} flag.
When you start @command{mu scm} with the @t{--listen} flag, it prints a When you start @command{mu scm} with the @t{--listen} flag, it prints a
(randomized) UNIX domain socket name and blocks after that; for instance: (randomized) UNIX domain socket name and blocks after that; for instance:
@example @example
$mu scm --listen $ mu scm --listen
UNIX-CONNECT:/run/user/1000/mu-scm-socket-6ef6222e /run/user/1000/mu-scm-15269.sock
@end example @end example
You can connect to this with exernal tools, for instance with @command{socat}: You can connect to this with external tools, for instance with @command{socat}:
@example @example
socat - UNIX-CONNECT:/run/user/1000/mu-scm-socket-6ef6222e $ socat - UNIX-CONNECT:/run/user/1000/mu-scm-15269.sock
GNU Guile 3.0.9 GNU Guile 3.0.9
Copyright (C) 1995-2023 Free Software Foundation, Inc. Copyright (C) 1995-2023 Free Software Foundation, Inc.
Guile comes with ABSOLUTELY NO WARRANTY; for details type `,show w'. Guile comes with ABSOLUTELY NO WARRANTY; for details type `,show w'. This
This program is free software, and you are welcome to redistribute it program is free software, and you are welcome to redistribute it under certain
under certain conditions; type `,show c' for details. conditions; type `,show c' for details.
Enter `,help' for help. Enter `,help' for help.
scheme@@(guile-user)> scheme@@(guile-user)>
@ -216,7 +217,7 @@ domain socket, as discussed in @xref{Listening on a Unix Domain Socket}.
Assuming you have installed the @t{guile-geiser} package, the following snippet Assuming you have installed the @t{guile-geiser} package, the following snippet
makes that easy: makes that easy:
@lisp @lisp
(require 'guiser-guile) (require 'geiser-guile)
(defvar mu-scm-listen-command "mu scm --listen" (defvar mu-scm-listen-command "mu scm --listen"
"mu command to start an scm repl listening on a socket.") "mu command to start an scm repl listening on a socket.")
@ -229,13 +230,32 @@ Connect to mu's scm (guile) interface through Geiser."
:name "*mu-scm-repl*" :name "*mu-scm-repl*"
:command `("sh" "-c" ,mu-scm-listen-command) :command `("sh" "-c" ,mu-scm-listen-command)
:filter (lambda (_proc chunk) :filter (lambda (_proc chunk)
(when (string-match "^UNIX-CONNECT:\\(.*\\)$" chunk) (when (stringng-match "^\\(mu-scm.*\\.sock\\)$" chunk)
(geiser-connect-local 'guile (match-string 1 chunk)))))) (geiser-connect-local 'guile (match-string 1 chunk))))))
@end lisp @end lisp
After evaluating this, you can use @command{M-x mu-scm-geiser-connect} to start After evaluating this, you can use @command{M-x mu-scm-geiser-connect} to start
the REPL, with all the Geiser bells & whistles. the REPL, with all the Geiser bells & whistles.
@node Hooking up with Mu4e
@section Hooking up with Mu4e
@cindex Mu4e
If you use @t{mu4e}, connecting to the SCM server is even easier than with
``plain'' Emacs.
First tell @t{mu4e} to starts it server with with @t{--listen} parameter:
@lisp
(setq mu4e-mu-scm-server t)
@end lisp
After that, (re)start @t{mu4e}.
Now should be able to connect to the REPL using @t{M-x mu4e-mu-scm-repl}. Like
@ref{Hooking up with GNU/Emacs and Geiser}, this depends on the @t{geiser-guile}
package.
The SCM instance uses the same database/store instance that @t{mu4e} uses.
@node Shell @node Shell
@chapter Shell @chapter Shell