diff options
Diffstat (limited to 'src/zenutil/invocationhistory.cpp')
| -rw-r--r-- | src/zenutil/invocationhistory.cpp | 308 |
1 files changed, 308 insertions, 0 deletions
diff --git a/src/zenutil/invocationhistory.cpp b/src/zenutil/invocationhistory.cpp new file mode 100644 index 000000000..a022e4cd5 --- /dev/null +++ b/src/zenutil/invocationhistory.cpp @@ -0,0 +1,308 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include <zenutil/invocationhistory.h> + +#include <zencore/basicfile.h> +#include <zencore/compactbinary.h> +#include <zencore/filesystem.h> +#include <zencore/iobuffer.h> +#include <zencore/memoryview.h> +#include <zencore/process.h> +#include <zencore/uid.h> +#include <zenutil/config/commandlineoptions.h> + +ZEN_THIRD_PARTY_INCLUDES_START +#include <json11.hpp> +ZEN_THIRD_PARTY_INCLUDES_END + +#include <system_error> + +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<std::string> 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<int>(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<uint32_t>(J["pid"].int_value()); + return true; + } + + std::vector<std::string> ReadHistoryLines(const std::filesystem::path& Path) + { + std::vector<std::string> 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<const char*>(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<std::string_view> 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<uint32_t>(GetCurrentProcessId()); + + std::string Raw = GetRawCommandLine(); + if (Raw.empty()) + { + std::vector<std::string> 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<std::string> 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<HistoryRecord> +ReadInvocationHistory(size_t MaxRecords) +{ + std::vector<HistoryRecord> Records; + try + { + std::filesystem::path Path = GetInvocationHistoryPath(); + if (Path.empty()) + { + return Records; + } + + std::vector<std::string> 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 |