Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: move the repl input code to its own file #10083

Merged
merged 4 commits into from
Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
186 changes: 186 additions & 0 deletions src/libcmd/repl-interacter.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
#include <cstdio>

#ifdef USE_READLINE
#include <readline/history.h>
#include <readline/readline.h>
#else
// editline < 1.15.2 don't wrap their API for C++ usage
// (added in https://github.com/troglobit/editline/commit/91398ceb3427b730995357e9d120539fb9bb7461).
// This results in linker errors due to to name-mangling of editline C symbols.
// For compatibility with these versions, we wrap the API here
// (wrapping multiple times on newer versions is no problem).
extern "C" {
#include <editline.h>
}
#endif

#include "signals.hh"
#include "finally.hh"
#include "repl-interacter.hh"
#include "file-system.hh"
#include "libcmd/repl.hh"

namespace nix {

namespace {
// Used to communicate to NixRepl::getLine whether a signal occurred in ::readline.
volatile sig_atomic_t g_signal_received = 0;

void sigintHandler(int signo)
{
g_signal_received = signo;
}
};

static detail::ReplCompleterMixin * curRepl; // ugly

static char * completionCallback(char * s, int * match)
{
auto possible = curRepl->completePrefix(s);
if (possible.size() == 1) {
*match = 1;
auto * res = strdup(possible.begin()->c_str() + strlen(s));
if (!res)
throw Error("allocation failure");
return res;
} else if (possible.size() > 1) {
auto checkAllHaveSameAt = [&](size_t pos) {
auto & first = *possible.begin();
for (auto & p : possible) {
if (p.size() <= pos || p[pos] != first[pos])
return false;
}
return true;
};
size_t start = strlen(s);
size_t len = 0;
while (checkAllHaveSameAt(start + len))
++len;
if (len > 0) {
*match = 1;
auto * res = strdup(std::string(*possible.begin(), start, len).c_str());
if (!res)
throw Error("allocation failure");
return res;
}
}

*match = 0;
return nullptr;
}

static int listPossibleCallback(char * s, char *** avp)
{
auto possible = curRepl->completePrefix(s);

if (possible.size() > (INT_MAX / sizeof(char *)))
throw Error("too many completions");

int ac = 0;
char ** vp = nullptr;

auto check = [&](auto * p) {
if (!p) {
if (vp) {
while (--ac >= 0)
free(vp[ac]);
free(vp);
}
throw Error("allocation failure");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is probably a bad idea/unsound to unwind through libreadline, resulting in the terminal not being reset! But I'm not fixing it in this PR as this PR is just moving code around.

}
return p;
};

vp = check((char **) malloc(possible.size() * sizeof(char *)));

for (auto & p : possible)
vp[ac++] = check(strdup(p.c_str()));

*avp = vp;

return ac;
}

ReadlineLikeInteracter::Guard ReadlineLikeInteracter::init(detail::ReplCompleterMixin * repl)
{
// Allow nix-repl specific settings in .inputrc
rl_readline_name = "nix-repl";
try {
createDirs(dirOf(historyFile));
} catch (SystemError & e) {
logWarning(e.info());
}
#ifndef USE_READLINE
el_hist_size = 1000;
#endif
read_history(historyFile.c_str());
auto oldRepl = curRepl;
curRepl = repl;
Guard restoreRepl([oldRepl] { curRepl = oldRepl; });
#ifndef USE_READLINE
rl_set_complete_func(completionCallback);
rl_set_list_possib_func(listPossibleCallback);
#endif
return restoreRepl;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This used to be a Finally in caller scope so we have to hoist it to the caller by returning it now. Nothing else is changed in this function.

}

static constexpr const char * promptForType(ReplPromptType promptType)
{
switch (promptType) {
case ReplPromptType::ReplPrompt:
return "nix-repl> ";
case ReplPromptType::ContinuationPrompt:
return " ";
}
assert(false);
}
Comment on lines +127 to +136
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the only actual new code in here


bool ReadlineLikeInteracter::getLine(std::string & input, ReplPromptType promptType)
{
struct sigaction act, old;
sigset_t savedSignalMask, set;

auto setupSignals = [&]() {
act.sa_handler = sigintHandler;
sigfillset(&act.sa_mask);
act.sa_flags = 0;
if (sigaction(SIGINT, &act, &old))
throw SysError("installing handler for SIGINT");

sigemptyset(&set);
sigaddset(&set, SIGINT);
if (sigprocmask(SIG_UNBLOCK, &set, &savedSignalMask))
throw SysError("unblocking SIGINT");
};
auto restoreSignals = [&]() {
if (sigprocmask(SIG_SETMASK, &savedSignalMask, nullptr))
throw SysError("restoring signals");

if (sigaction(SIGINT, &old, 0))
throw SysError("restoring handler for SIGINT");
};

setupSignals();
char * s = readline(promptForType(promptType));
Finally doFree([&]() { free(s); });
restoreSignals();

if (g_signal_received) {
g_signal_received = 0;
input.clear();
return true;
}

if (!s)
return false;
input += s;
input += '\n';
return true;
}

ReadlineLikeInteracter::~ReadlineLikeInteracter()
{
write_history(historyFile.c_str());
}

};
48 changes: 48 additions & 0 deletions src/libcmd/repl-interacter.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#pragma once
/// @file

#include "finally.hh"
#include "types.hh"
#include <functional>
#include <string>

namespace nix {

namespace detail {
/** Provides the completion hooks for the repl, without exposing its complete
* internals. */
struct ReplCompleterMixin {
virtual StringSet completePrefix(const std::string & prefix) = 0;
};
};

enum class ReplPromptType {
ReplPrompt,
ContinuationPrompt,
};

class ReplInteracter
{
public:
using Guard = Finally<std::function<void()>>;

virtual Guard init(detail::ReplCompleterMixin * repl) = 0;
/** Returns a boolean of whether the interacter got EOF */
virtual bool getLine(std::string & input, ReplPromptType promptType) = 0;
virtual ~ReplInteracter(){};
};

class ReadlineLikeInteracter : public virtual ReplInteracter
{
std::string historyFile;
public:
ReadlineLikeInteracter(std::string historyFile)
: historyFile(historyFile)
{
}
virtual Guard init(detail::ReplCompleterMixin * repl) override;
virtual bool getLine(std::string & input, ReplPromptType promptType) override;
virtual ~ReadlineLikeInteracter() override;
};

};