libutil: add a log line splitter

we'll need this in other places too soon.

Change-Id: I8ddd57aefa4cbd1f8f7232f35e30a204ace77e58
This commit is contained in:
eldritch horrors
2025-10-16 12:31:08 +00:00
parent daff1b148f
commit a85115bd33
3 changed files with 63 additions and 26 deletions
+16 -26
View File
@@ -1229,27 +1229,20 @@ Goal::WorkResult DerivationGoal::tooMuchLogs()
kj::Promise<Result<std::optional<Goal::WorkResult>>>
DerivationGoal::handleBuilderOutput(AsyncInputStream & in) noexcept
try {
std::string currentLogLine;
size_t currentLogLinePos = 0; // to handle carriage return
LogLineSplitter splitter;
auto flushLine = [&] {
KJ_DEFER({
currentLogLine = "";
currentLogLinePos = 0;
});
if (const auto state = handleJSONLogMessage(
currentLogLine, *act, builderActivities, "the derivation builder"
))
auto flushLine = [&](const std::string & line) {
if (const auto state =
handleJSONLogMessage(line, *act, builderActivities, "the derivation builder"))
{
return *state;
} else {
logTail.push_back(currentLogLine);
logTail.push_back(line);
if (logTail.size() > settings.logLines) {
logTail.pop_front();
}
return act->result(resBuildLogLine, currentLogLine);
return act->result(resBuildLogLine, line);
}
};
@@ -1274,8 +1267,8 @@ try {
lastChildActivity = AIO().provider.getTimer().now();
if (data.empty()) {
if (!currentLogLine.empty()) {
if (flushLine() == Logger::BufferState::NeedsFlush) {
if (auto left = splitter.finish(); !left.empty()) {
if (flushLine(left) == Logger::BufferState::NeedsFlush) {
TRY_AWAIT(act->getLogger().flush());
}
}
@@ -1287,20 +1280,17 @@ try {
co_return tooMuchLogs();
}
for (auto c : data)
if (c == '\r')
currentLogLinePos = 0;
else if (c == '\n') {
if (flushLine() == Logger::BufferState::NeedsFlush) {
if (logSink) {
(*logSink)(data);
}
while (!data.empty()) {
if (auto line = splitter.feed(data)) {
if (flushLine(*line) == Logger::BufferState::NeedsFlush) {
TRY_AWAIT(act->getLogger().flush());
}
} else {
if (currentLogLinePos >= currentLogLine.size())
currentLogLine.resize(currentLogLinePos + 1);
currentLogLine[currentLogLinePos++] = c;
}
if (logSink) (*logSink)(data);
}
}
} catch (...) {
co_return result::current_exception();
+24
View File
@@ -423,4 +423,28 @@ void logFatal(std::string const & s)
syslog(LOG_CRIT, "%s", requireCString(s).asCStr());
}
std::optional<std::string> LogLineSplitter::feed(std::string_view & input)
{
for (auto [idx, c] : enumerate(input)) {
if (c == '\r') {
pos = 0;
} else if (c == '\n') {
input = input.substr(idx + 1);
return finish();
} else {
if (pos >= line.size()) {
line.resize(pos + 1);
}
line[pos++] = c;
}
}
input = {};
return std::nullopt;
}
std::string LogLineSplitter::finish()
{
pos = 0;
return std::move(line);
}
}
+23
View File
@@ -435,4 +435,27 @@ std::optional<Logger::BufferState> handleJSONLogMessage(
std::map<ActivityId, Activity> & activities,
std::string_view source
);
/**
* Split a log stream into lines, processing carriage returns (`\r`) as a terminal would.
*/
class LogLineSplitter
{
std::string line;
size_t pos = 0;
public:
/**
* Feeds some input to the splitter and returns the first full line or `nullopt` if
* there is no complete line in the buffer yet. If any input remains `input` is set
* to the unconsumed data and `feed` should be called again until `input` is empty.
* If this function returns `nullopt` it guarantees that `input` is fully consumed.
*/
std::optional<std::string> feed(std::string_view & input);
/**
* Clear the line buffer and return its current contents.
*/
std::string finish();
};
}