aboutsummaryrefslogtreecommitdiff
path: root/src/zenutil/invocationhistory.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/zenutil/invocationhistory.cpp')
-rw-r--r--src/zenutil/invocationhistory.cpp308
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