Replace loguru with a custom logger

This commit is contained in:
Fangrui Song 2018-05-27 12:24:56 -07:00
parent a6094ef714
commit cf0d16fa0c
40 changed files with 174 additions and 187 deletions

3
.gitmodules vendored
View File

@ -4,6 +4,3 @@
[submodule "third_party/doctest"]
path = third_party/doctest
url = https://github.com/onqtam/doctest
[submodule "third_party/loguru"]
path = third_party/loguru
url = https://github.com/emilk/loguru

View File

@ -201,14 +201,15 @@ target_sources(ccls PRIVATE
src/filesystem.cc
src/fuzzy_match.cc
src/main.cc
src/import_pipeline.cc
src/include_complete.cc
src/method.cc
src/language.cc
src/lex_utils.cc
src/log.cc
src/lsp.cc
src/match.cc
src/message_handler.cc
src/pipeline.cc
src/platform_posix.cc
src/platform_win.cc
src/port.cc

View File

@ -5,8 +5,6 @@
#include "lsp.h"
#include "platform.h"
#include <loguru/loguru.hpp>
#include <algorithm>
#include <unordered_map>

View File

@ -1,14 +1,15 @@
#include "clang_complete.h"
#include "clang_utils.h"
#include "filesystem.hh"
#include "log.hh"
#include "platform.h"
#include "timer.h"
#include "filesystem.hh"
#include <llvm/ADT/Twine.h>
#include <llvm/Support/Threading.h>
using namespace llvm;
#include <loguru.hpp>
#include <algorithm>
#include <thread>
@ -660,15 +661,15 @@ ClangCompleteManager::ClangCompleteManager(Project* project,
preloaded_sessions_(kMaxPreloadedSessions),
completion_sessions_(kMaxCompletionSessions) {
std::thread([&]() {
SetThreadName("comp-query");
set_thread_name("comp-query");
CompletionQueryMain(this);
}).detach();
std::thread([&]() {
SetThreadName("comp-preload");
set_thread_name("comp-preload");
CompletionPreloadMain(this);
}).detach();
std::thread([&]() {
SetThreadName("diag-query");
set_thread_name("diag-query");
DiagnosticQueryMain(this);
}).detach();
}
@ -803,6 +804,7 @@ void ClangCompleteManager::FlushSession(const std::string& filename) {
}
void ClangCompleteManager::FlushAllSessions() {
LOG_S(INFO) << "flush all clang complete sessions";
std::lock_guard<std::mutex> lock(sessions_lock_);
preloaded_sessions_.Clear();

View File

@ -2,13 +2,12 @@
#include "clang_cursor.h"
#include "clang_utils.h"
#include "log.hh"
#include "platform.h"
#include "serializer.h"
#include "timer.h"
#include "type_printer.h"
#include <loguru.hpp>
#include <assert.h>
#include <inttypes.h>
#include <limits.h>

View File

@ -1,11 +1,10 @@
#include "clang_translation_unit.h"
#include "clang_utils.h"
#include "log.hh"
#include "platform.h"
#include "utils.h"
#include <loguru.hpp>
namespace {
void EmitDiagnostics(std::string path,

View File

@ -2,11 +2,10 @@
#include "clang_utils.h"
#include "indexer.h"
#include "log.hh"
#include "platform.h"
#include "utils.h"
#include <loguru.hpp>
namespace {
std::optional<std::string> GetFileContents(
@ -108,8 +107,8 @@ IndexFile* FileConsumer::TryConsumeFile(
if (clang_getFileUniqueID(file, &file_id) != 0) {
std::string file_name = FileName(file);
if (!file_name.empty()) {
// LOG_S(ERROR) << "Could not get unique file id for " << file_name
// << " when parsing " << parse_file_;
LOG_S(ERROR) << "Could not get unique file id for " << file_name
<< " when parsing " << parse_file_;
}
return nullptr;
}

View File

@ -6,6 +6,10 @@
#include "project.h"
#include "timer.h"
#include <llvm/ADT/Twine.h>
#include <llvm/Support/Threading.h>
using namespace llvm;
#include <thread>
namespace {
@ -103,7 +107,7 @@ void IncludeComplete::Rescan() {
is_scanning = true;
std::thread([this]() {
SetThreadName("scan_includes");
set_thread_name("scan_includes");
Timer timer;
for (const std::string& dir : project_->quote_include_directories)

62
src/log.cc Normal file
View File

@ -0,0 +1,62 @@
#include "log.hh"
#include <llvm/ADT/SmallString.h>
#include <llvm/Support/Threading.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <iomanip>
#include <mutex>
namespace ccls::log {
static std::mutex mtx;
FILE* file;
Verbosity verbosity;
Message::Message(Verbosity verbosity, const char* file, int line)
: verbosity_(verbosity) {
using namespace llvm;
time_t tim = time(NULL);
struct tm t;
{
std::lock_guard<std::mutex> lock(mtx);
t = *localtime(&tim);
}
char buf[16];
snprintf(buf, sizeof buf, "%02d:%02d:%02d ", t.tm_hour, t.tm_min, t.tm_sec);
stream_ << buf;
{
SmallString<32> Name;
get_thread_name(Name);
stream_ << std::left << std::setw(13) << Name.c_str();
}
{
const char* p = strrchr(file, '/');
if (p)
file = p + 1;
stream_ << std::right << std::setw(15) << file << ':' << std::left
<< std::setw(3) << line;
}
stream_ << ' ';
// clang-format off
switch (verbosity_) {
case Verbosity_FATAL: stream_ << 'F'; break;
case Verbosity_ERROR: stream_ << 'E'; break;
case Verbosity_WARNING: stream_ << 'W'; break;
case Verbosity_INFO: stream_ << 'I'; break;
default: stream_ << "V(" << int(verbosity_) << ')';
}
// clang-format on
stream_ << ' ';
}
Message::~Message() {
if (!file) return;
std::lock_guard<std::mutex> lock(mtx);
stream_ << '\n';
fputs(stream_.str().c_str(), file);
if (verbosity_ == Verbosity_FATAL)
abort();
}
}

40
src/log.hh Normal file
View File

@ -0,0 +1,40 @@
#pragma once
#include <stdio.h>
#include <sstream>
namespace ccls::log {
extern FILE* file;
struct Voidify {
void operator&(const std::ostream&) {}
};
enum Verbosity {
Verbosity_FATAL = -3,
Verbosity_ERROR = -2,
Verbosity_WARNING = -1,
Verbosity_INFO = 0,
};
extern Verbosity verbosity;
struct Message {
std::stringstream stream_;
int verbosity_;
Message(Verbosity verbosity, const char* file, int line);
~Message();
};
}
#define LOG_IF(v, cond) \
!(cond) ? void(0) \
: ccls::log::Voidify() & \
ccls::log::Message(v, __FILE__, __LINE__).stream_
#define LOG_S(v) \
LOG_IF(ccls::log::Verbosity_##v, \
ccls::log::Verbosity_##v <= ccls::log::verbosity)
#define LOG_IF_S(v, cond) \
LOG_IF(ccls::log::Verbosity_##v, \
(cond) && ccls::log::Verbosity_##v <= ccls::log::verbosity)
#define CHECK_S(cond) LOG_IF(FATAL, !(cond)) << "check failed: " #cond " "

View File

@ -1,10 +1,9 @@
#include "lsp.h"
#include "log.hh"
#include "serializers/json.h"
#include <doctest/doctest.h>
#include <rapidjson/writer.h>
#include <loguru.hpp>
#include <stdio.h>
@ -68,45 +67,6 @@ std::optional<std::string> ReadJsonRpcContentFrom(
return content;
}
std::function<std::optional<char>()> MakeContentReader(std::string* content,
bool can_be_empty) {
return [content, can_be_empty]() -> std::optional<char> {
if (!can_be_empty)
REQUIRE(!content->empty());
if (content->empty())
return std::nullopt;
char c = (*content)[0];
content->erase(content->begin());
return c;
};
}
TEST_SUITE("FindIncludeLine") {
TEST_CASE("ReadContentFromSource") {
auto parse_correct = [](std::string content) -> std::string {
auto reader = MakeContentReader(&content, false /*can_be_empty*/);
auto got = ReadJsonRpcContentFrom(reader);
REQUIRE(got);
return got.value();
};
auto parse_incorrect = [](std::string content) -> std::optional<std::string> {
auto reader = MakeContentReader(&content, true /*can_be_empty*/);
return ReadJsonRpcContentFrom(reader);
};
REQUIRE(parse_correct("Content-Length: 0\r\n\r\n") == "");
REQUIRE(parse_correct("Content-Length: 1\r\n\r\na") == "a");
REQUIRE(parse_correct("Content-Length: 4\r\n\r\nabcd") == "abcd");
REQUIRE(parse_incorrect("ggg") == std::optional<std::string>());
REQUIRE(parse_incorrect("Content-Length: 0\r\n") ==
std::optional<std::string>());
REQUIRE(parse_incorrect("Content-Length: 5\r\n\r\nab") ==
std::optional<std::string>());
}
}
std::optional<char> ReadCharFromStdinBlocking() {
// We do not use std::cin because it does not read bytes once stuck in
// cin.bad(). We can call cin.clear() but C++ iostream has other annoyance

View File

@ -304,14 +304,6 @@ struct lsWorkspaceEdit {
};
MAKE_REFLECT_STRUCT(lsWorkspaceEdit, documentChanges);
struct lsFormattingOptions {
// Size of a tab in spaces.
int tabSize;
// Prefer spaces over tabs.
bool insertSpaces;
};
MAKE_REFLECT_STRUCT(lsFormattingOptions, tabSize, insertSpaces);
// MarkedString can be used to render human readable text. It is either a
// markdown string or a code-block that provides a language and a code snippet.
// The language identifier is sematically equal to the std::optional language

View File

@ -1,4 +1,5 @@
#include "import_pipeline.h"
#include "log.hh"
#include "pipeline.hh"
#include "platform.h"
#include "serializer.h"
#include "serializers/json.h"
@ -7,12 +8,12 @@
#include <llvm/Support/CommandLine.h>
#include <llvm/Support/Process.h>
#include <llvm/Support/Signals.h>
using namespace llvm;
using namespace llvm::cl;
#include <doctest/doctest.h>
#include <rapidjson/error/en.h>
#include <loguru.hpp>
#include <stdio.h>
#include <stdlib.h>
@ -34,10 +35,15 @@ opt<std::string> opt_log_file_append("log-file-append", desc("log"), value_desc(
list<std::string> opt_extra(Positional, ZeroOrMore, desc("extra"));
void CloseLog() {
fclose(ccls::log::file);
}
} // namespace
int main(int argc, char** argv) {
TraceMe();
sys::PrintStackTraceOnErrorSignal(argv[0]);
ParseCommandLineOptions(argc, argv,
"C/C++/Objective-C language server\n\n"
@ -50,13 +56,6 @@ int main(int argc, char** argv) {
return 0;
}
loguru::g_stderr_verbosity = opt_verbose - 1;
loguru::g_preamble_date = false;
loguru::g_preamble_time = false;
loguru::g_preamble_verbose = false;
loguru::g_flush_interval_ms = 0;
loguru::init(argc, argv);
MultiQueueWaiter querydb_waiter, indexer_waiter, stdout_waiter;
QueueManager::Init(&querydb_waiter, &indexer_waiter, &stdout_waiter);
@ -72,10 +71,19 @@ int main(int argc, char** argv) {
bool language_server = true;
if (opt_log_file.size())
loguru::add_file(opt_log_file.c_str(), loguru::Truncate, opt_verbose);
if (opt_log_file_append.size())
loguru::add_file(opt_log_file_append.c_str(), loguru::Append, opt_verbose);
if (opt_log_file.size() || opt_log_file_append.size()) {
ccls::log::file = opt_log_file.size()
? fopen(opt_log_file.c_str(), "wb")
: fopen(opt_log_file_append.c_str(), "ab");
if (!ccls::log::file) {
fprintf(
stderr, "failed to open %s\n",
(opt_log_file.size() ? opt_log_file : opt_log_file_append).c_str());
return 2;
}
setbuf(ccls::log::file, NULL);
atexit(CloseLog);
}
if (opt_test_unit) {
language_server = false;

View File

@ -1,12 +1,11 @@
#include "message_handler.h"
#include "lex_utils.h"
#include "log.hh"
#include "project.h"
#include "query_utils.h"
#include "queue_manager.h"
#include <loguru.hpp>
#include <algorithm>
MAKE_HASHABLE(SymbolIdx, t.usr, t.kind);
@ -156,14 +155,7 @@ bool FindFileOrFail(QueryDatabase* db,
if (indexing)
LOG_S(INFO) << "\"" << absolute_path << "\" is being indexed.";
else
LOG_S(INFO) << "Unable to find file \"" << absolute_path << "\"";
/*
LOG_S(INFO) << "Files (size=" << db->usr_to_file.size() << "): "
<< StringJoinMap(db->usr_to_file,
[](const std::pair<NormalizedPath, QueryFileId>& entry) {
return entry.first.path;
});
*/
LOG_S(INFO) << "unable to find file \"" << absolute_path << "\"";
if (id) {
Out_Error out;

View File

@ -2,8 +2,6 @@
#include "query_utils.h"
#include "queue_manager.h"
#include <loguru.hpp>
#include <unordered_set>
namespace {

View File

@ -1,14 +1,13 @@
#include "cache_manager.h"
#include "import_pipeline.h"
#include "match.h"
#include "message_handler.h"
#include "pipeline.hh"
#include "platform.h"
#include "project.h"
#include "queue_manager.h"
#include "timer.h"
#include "working_files.h"
#include <loguru.hpp>
#include <queue>
#include <unordered_set>

View File

@ -1,7 +1,5 @@
#include "message_handler.h"
#include <loguru.hpp>
namespace {
struct In_Exit : public NotificationInMessage {
MethodType GetMethodType() const override { return kMethodType_Exit; }
@ -13,7 +11,6 @@ struct Handler_Exit : MessageHandler {
MethodType GetMethodType() const override { return kMethodType_Exit; }
void Run(std::unique_ptr<InMessage> request) override {
LOG_S(INFO) << "Exiting; got exit message";
exit(0);
}
};

View File

@ -1,8 +1,10 @@
#include "cache_manager.h"
#include "diagnostics_engine.h"
#include "import_pipeline.h"
#include "filesystem.hh"
#include "include_complete.h"
#include "log.hh"
#include "message_handler.h"
#include "pipeline.hh"
#include "platform.h"
#include "project.h"
#include "queue_manager.h"
@ -10,11 +12,10 @@
#include "timer.h"
#include "working_files.h"
#include "filesystem.hh"
#include <llvm/ADT/Twine.h>
#include <llvm/Support/Threading.h>
using namespace llvm;
#include <loguru.hpp>
#include <iostream>
#include <stdexcept>
#include <thread>
@ -508,12 +509,12 @@ struct Handler_Initialize : BaseMessageHandler<In_InitializeRequest> {
if (g_config->index.threads == 0)
g_config->index.threads = std::thread::hardware_concurrency();
LOG_S(INFO) << "Starting " << g_config->index.threads << " indexers";
LOG_S(INFO) << "start " << g_config->index.threads << " indexers";
for (int i = 0; i < g_config->index.threads; i++) {
std::thread([=]() {
g_thread_id = i + 1;
std::string name = "indexer" + std::to_string(i);
SetThreadName(name.c_str());
set_thread_name(name.c_str());
Indexer_Main(diag_engine, vfs, project, working_files, waiter);
}).detach();
}

View File

@ -8,8 +8,6 @@
#include "lex_utils.h"
#include <loguru.hpp>
#include <regex>
namespace {

View File

@ -5,8 +5,6 @@
#include "working_files.h"
#include "queue_manager.h"
#include <loguru/loguru.hpp>
namespace {
MethodType kMethodType = "textDocument/didChange";

View File

@ -7,8 +7,6 @@
#include "timer.h"
#include "working_files.h"
#include <loguru.hpp>
namespace {
MethodType kMethodType = "textDocument/didOpen";
@ -69,7 +67,6 @@ struct Handler_TextDocumentDidOpen
true /* priority */);
clang_complete->FlushSession(entry.filename);
LOG_S(INFO) << "Flushed clang complete sessions for " << entry.filename;
}
}
};

View File

@ -4,8 +4,6 @@
#include "project.h"
#include "queue_manager.h"
#include <loguru/loguru.hpp>
namespace {
MethodType kMethodType = "textDocument/didSave";

View File

@ -6,8 +6,6 @@
#include "timer.h"
#include "working_files.h"
#include <loguru.hpp>
namespace {
MethodType kMethodType = "workspace/didChangeConfiguration";
@ -38,7 +36,6 @@ struct Handler_WorkspaceDidChangeConfiguration
"[perf] Dispatched workspace/didChangeConfiguration index requests");
clang_complete->FlushAllSessions();
LOG_S(INFO) << "Flushed all clang complete sessions";
}
};
REGISTER_MESSAGE_HANDLER(Handler_WorkspaceDidChangeConfiguration);

View File

@ -5,8 +5,6 @@
#include "queue_manager.h"
#include "working_files.h"
#include <loguru/loguru.hpp>
namespace {
MethodType kMethodType = "workspace/didChangeWatchedFiles";

View File

@ -4,8 +4,6 @@
#include "query_utils.h"
#include "queue_manager.h"
#include <loguru.hpp>
#include <ctype.h>
#include <limits.h>
#include <algorithm>
@ -70,9 +68,6 @@ struct Handler_WorkspaceSymbol : BaseMessageHandler<In_WorkspaceSymbol> {
Out_WorkspaceSymbol out;
out.id = request->id;
LOG_S(INFO) << "[querydb] Considering " << db->symbols.size()
<< " candidates for query " << request->params.query;
std::string query = request->params.query;
// {symbol info, matching detailed_name or short_name, index}
@ -129,8 +124,6 @@ struct Handler_WorkspaceSymbol : BaseMessageHandler<In_WorkspaceSymbol> {
out.result.push_back(std::get<0>(entry));
}
LOG_S(INFO) << "[querydb] Found " << out.result.size()
<< " results for query " << query;
QueueManager::WriteStdout(kMethodType, out);
}
};

View File

@ -1,10 +1,11 @@
#include "import_pipeline.h"
#include "pipeline.hh"
#include "cache_manager.h"
#include "clang_complete.h"
#include "config.h"
#include "diagnostics_engine.h"
#include "include_complete.h"
#include "log.hh"
#include "lsp.h"
#include "message_handler.h"
#include "platform.h"
@ -13,8 +14,9 @@
#include "queue_manager.h"
#include "timer.h"
#include <doctest/doctest.h>
#include <loguru.hpp>
#include <llvm/ADT/Twine.h>
#include <llvm/Support/Threading.h>
using namespace llvm;
#include <chrono>
#include <thread>
@ -275,7 +277,7 @@ void QueryDb_OnIndexed(QueueManager* queue,
void LaunchStdinLoop(std::unordered_map<MethodType, Timer>* request_times) {
std::thread([request_times]() {
SetThreadName("stdin");
set_thread_name("stdin");
auto* queue = QueueManager::instance();
while (true) {
std::unique_ptr<InMessage> message;
@ -316,7 +318,7 @@ void LaunchStdinLoop(std::unordered_map<MethodType, Timer>* request_times) {
void LaunchStdoutThread(std::unordered_map<MethodType, Timer>* request_times,
MultiQueueWaiter* waiter) {
std::thread([=]() {
SetThreadName("stdout");
set_thread_name("stdout");
auto* queue = QueueManager::instance();
while (true) {

View File

@ -8,8 +8,6 @@
std::string NormalizePath(const std::string& path);
void SetThreadName(const char* name);
// Free any unused memory and return it to the system.
void FreeUnusedMemory();

View File

@ -1,13 +1,8 @@
#include <llvm/ADT/Twine.h>
#include <llvm/Support/Threading.h>
#if defined(__unix__) || defined(__APPLE__)
#include "platform.h"
#include "utils.h"
#include <loguru.hpp>
#include <assert.h>
#include <limits.h>
#include <stdio.h>
@ -160,8 +155,3 @@ std::string GetExternalCommandOutput(const std::vector<std::string>& command,
}
#endif
void SetThreadName(const char* name) {
loguru::set_thread_name(name);
llvm::set_thread_name(name);
}

View File

@ -4,6 +4,7 @@
#include "clang_utils.h"
#include "filesystem.hh"
#include "language.h"
#include "log.hh"
#include "match.h"
#include "platform.h"
#include "queue_manager.h"
@ -25,7 +26,6 @@ using namespace llvm::opt;
#include <clang-c/CXCompilationDatabase.h>
#include <doctest/doctest.h>
#include <rapidjson/writer.h>
#include <loguru.hpp>
#if defined(__unix__) || defined(__APPLE__)
#include <unistd.h>

View File

@ -5,7 +5,6 @@
#include "serializers/json.h"
#include <doctest/doctest.h>
#include <loguru.hpp>
#include <cassert>
#include <cstdint>

View File

@ -2,9 +2,7 @@
#include "queue_manager.h"
#include <loguru.hpp>
#include <climits>
#include <limits.h>
#include <unordered_set>
namespace {

View File

@ -1,13 +1,12 @@
#include "serializer.h"
#include "filesystem.hh"
#include "log.hh"
#include "serializers/binary.h"
#include "serializers/json.h"
#include "indexer.h"
#include <loguru.hpp>
#include <stdexcept>
using namespace llvm;
@ -414,7 +413,7 @@ std::unique_ptr<IndexFile> Deserialize(
file = std::make_unique<IndexFile>(path, file_content);
Reflect(reader, *file);
} catch (std::invalid_argument& e) {
LOG_S(INFO) << "Failed to deserialize '" << path
LOG_S(INFO) << "failed to deserialize '" << path
<< "': " << e.what();
return nullptr;
}

View File

@ -11,7 +11,6 @@
#include <rapidjson/prettywriter.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
#include <loguru/loguru.hpp>
#include <stdio.h>
#include <stdlib.h>

View File

@ -1,5 +1,2 @@
#define DOCTEST_CONFIG_IMPLEMENT
#include <doctest/doctest.h>
#define LOGURU_IMPLEMENTATION 1
#include <loguru.hpp>

View File

@ -1,6 +1,6 @@
#include "timer.h"
#include <loguru.hpp>
#include "log.hh"
Timer::Timer() {
Reset();

View File

@ -1,7 +1,5 @@
#include "type_printer.h"
#include <loguru.hpp>
namespace {
int GetNameInsertingPosition(const std::string& type_desc,
@ -82,12 +80,8 @@ std::tuple<std::string, int16_t, int16_t, int16_t> GetFunctionSignature(
std::string type_desc_with_names(type_desc.begin(), type_desc.begin() + i);
type_desc_with_names.append(func_name);
for (auto& arg : args) {
if (arg.first < 0) {
LOG_S(ERROR)
<< "When adding argument names to '" << type_desc
<< "', failed to detect positions to insert argument names";
if (arg.first < 0)
break;
}
if (arg.second.empty())
continue;
// TODO Use inside-out syntax. Note, clang/lib/AST/TypePrinter.cpp does

View File

@ -2,11 +2,10 @@
#include "filesystem.hh"
using namespace llvm;
#include "log.hh"
#include "platform.h"
#include <doctest/doctest.h>
#include <siphash.h>
#include <loguru/loguru.hpp>
#include <assert.h>
#include <ctype.h>

View File

@ -1,10 +1,10 @@
#include "working_files.h"
#include "lex_utils.h"
#include "log.hh"
#include "position.h"
#include <doctest/doctest.h>
#include <loguru.hpp>
#include <algorithm>
#include <climits>
@ -320,23 +320,9 @@ void WorkingFile::ComputeLineMapping() {
std::optional<int> WorkingFile::GetBufferPosFromIndexPos(int line,
int* column,
bool is_end) {
// The implementation is simple but works pretty well for most cases. We
// lookup the line contents in the indexed file contents, and try to find the
// most similar line in the current buffer file.
//
// Previously, this was implemented by tracking edits and by running myers
// diff algorithm. They were complex implementations that did not work as
// well.
// Note: |index_line| and |buffer_line| are 1-based.
// TODO: reenable this assert once we are using the real indexed file.
// assert(index_line >= 1 && index_line <= index_lines.size());
if (line < 0 || line >= (int)index_lines.size()) {
loguru::Text stack = loguru::stacktrace();
LOG_S(WARNING) << "Bad index_line (got " << line << ", expected [0, "
<< index_lines.size() << ")) in " << filename
<< stack.c_str();
LOG_S(WARNING) << "bad index_line (got " << line << ", expected [0, "
<< index_lines.size() << ")) in " << filename;
return std::nullopt;
}

1
third_party/loguru vendored

@ -1 +0,0 @@
Subproject commit 6bf94c5f2bec437e871402d0a27e8a3094b261d5