// Copyright Epic Games, Inc. All Rights Reserved. #include #include #include #include #include #include #include #include #include ZEN_THIRD_PARTY_INCLUDES_START #include ZEN_THIRD_PARTY_INCLUDES_END #include namespace zen { namespace { constexpr size_t kMaxRecords = 100; constexpr std::string_view kHistoryFileName = "invocations.jsonl"; // Safety cap. With 100 records at typical ~500-1000 bytes each the file // normally sits around 50-100 KB. If it has grown past this threshold // (external corruption, runaway producer, another tool writing garbage) // we refuse to read it and start fresh with just the new record. Keeps // LogInvocation from slowing startup on a pathological file. constexpr uintmax_t kMaxReadSize = 1 * 1024 * 1024; // 1 MB bool ExecutionHistoryDisabled(int argc, char** argv) { for (int I = 1; I < argc; ++I) { if (argv[I] == nullptr) { continue; } std::string_view A = argv[I]; if (A == "--enable-execution-history=false" || A == "--enable-execution-history=0" || A == "--enable-execution-history=no") { return true; } } return false; } std::filesystem::path ResolveHistoryDir() { #if ZEN_PLATFORM_WINDOWS std::optional LocalAppData = GetEnvVariable("LOCALAPPDATA"); if (LocalAppData && !LocalAppData->empty()) { return std::filesystem::path(*LocalAppData) / "Epic" / "Zen" / "History"; } #endif std::filesystem::path SystemRoot = PickDefaultSystemRootDirectory(); if (SystemRoot.empty()) { return {}; } return SystemRoot / "History"; } std::string BuildJsonRecord(const HistoryRecord& Rec) { json11::Json::object Obj{ {"id", Rec.Id}, {"ts", Rec.Ts}, {"exe", Rec.Exe}, {"pid", static_cast(Rec.Pid)}, {"cwd", Rec.Cwd}, {"path", Rec.Path}, {"cmdline", Rec.CmdLine}, }; if (!Rec.Mode.empty()) { Obj.emplace("mode", Rec.Mode); } return json11::Json(Obj).dump(); } bool ParseJsonRecord(std::string_view Line, HistoryRecord& OutRec) { std::string Err; json11::Json J = json11::Json::parse(std::string(Line), Err); if (!Err.empty() || !J.is_object()) { return false; } OutRec.Id = J["id"].string_value(); OutRec.Ts = J["ts"].string_value(); OutRec.Exe = J["exe"].string_value(); OutRec.Mode = J["mode"].string_value(); OutRec.Cwd = J["cwd"].string_value(); OutRec.Path = J["path"].string_value(); OutRec.CmdLine = J["cmdline"].string_value(); OutRec.Pid = static_cast(J["pid"].int_value()); return true; } std::vector ReadHistoryLines(const std::filesystem::path& Path) { std::vector Lines; std::error_code SizeEc; const std::uintmax_t FileSize = std::filesystem::file_size(Path, SizeEc); if (SizeEc || FileSize > kMaxReadSize) { return Lines; } FileContents Contents = ReadFile(Path); if (!Contents) { return Lines; } IoBuffer Flat = Contents.Flatten(); const char* Data = static_cast(Flat.GetData()); const size_t Size = Flat.GetSize(); size_t Start = 0; for (size_t I = 0; I < Size; ++I) { if (Data[I] == '\n') { if (I > Start) { size_t LineEnd = I; if (LineEnd > Start && Data[LineEnd - 1] == '\r') { --LineEnd; } if (LineEnd > Start) { Lines.emplace_back(Data + Start, LineEnd - Start); } } Start = I + 1; } } if (Start < Size) { Lines.emplace_back(Data + Start, Size - Start); } return Lines; } } // namespace std::filesystem::path GetInvocationHistoryPath() noexcept { try { std::filesystem::path Dir = ResolveHistoryDir(); if (Dir.empty()) { return {}; } return Dir / kHistoryFileName; } catch (...) { return {}; } } void LogInvocation(std::string_view Exe, std::string_view Mode, int argc, char** argv, std::initializer_list ExcludeSubcommands) noexcept { try { if (ExecutionHistoryDisabled(argc, argv)) { return; } if (argc >= 2 && argv[1] != nullptr) { std::string_view A1 = argv[1]; for (std::string_view Excluded : ExcludeSubcommands) { if (A1 == Excluded) { return; } } } std::filesystem::path Dir = ResolveHistoryDir(); if (Dir.empty()) { return; } std::error_code Ec; CreateDirectories(Dir, Ec); if (Ec) { return; } std::filesystem::path Path = Dir / kHistoryFileName; HistoryRecord Rec; Rec.Id = Oid::NewOid().ToString(); Rec.Ts = DateTime::Now().ToIso8601(); Rec.Exe = std::string(Exe); Rec.Mode = std::string(Mode); std::error_code CwdEc; Rec.Cwd = std::filesystem::current_path(CwdEc).string(); Rec.Path = GetRunningExecutablePath().string(); Rec.Pid = static_cast(GetCurrentProcessId()); std::string Raw = GetRawCommandLine(); if (Raw.empty()) { std::vector Args; Args.reserve(argc); for (int I = 0; I < argc; ++I) { if (argv[I] != nullptr) { Args.emplace_back(argv[I]); } } Raw = BuildCommandLine(Args); } ScrubSensitiveValues(Raw); Rec.CmdLine = std::move(Raw); std::vector Lines = ReadHistoryLines(Path); if (Lines.size() >= kMaxRecords) { Lines.erase(Lines.begin(), Lines.begin() + (Lines.size() - (kMaxRecords - 1))); } Lines.push_back(BuildJsonRecord(Rec)); std::string NewContents; size_t TotalSize = 0; for (const std::string& L : Lines) { TotalSize += L.size() + 1; } NewContents.reserve(TotalSize); for (const std::string& L : Lines) { NewContents.append(L); NewContents.push_back('\n'); } std::error_code WriteEc; TemporaryFile::SafeWriteFile(Path, MemoryView(NewContents.data(), NewContents.size()), WriteEc); } catch (...) { } } std::vector ReadInvocationHistory(size_t MaxRecords) { std::vector Records; try { std::filesystem::path Path = GetInvocationHistoryPath(); if (Path.empty()) { return Records; } std::vector Lines = ReadHistoryLines(Path); if (Lines.size() > MaxRecords) { Lines.erase(Lines.begin(), Lines.begin() + (Lines.size() - MaxRecords)); } Records.reserve(Lines.size()); for (const std::string& L : Lines) { HistoryRecord Rec; if (ParseJsonRecord(L, Rec)) { Records.push_back(std::move(Rec)); } } } catch (...) { } return Records; } void invocationhistory_forcelink() { } } // namespace zen