Download CommonLua/CrashReport.lua from sirnii/myspace: direct link, hf CLI and curl.
- Browser
- Download file 25.9 kB
-
https://huggingface.co/spaces/sirnii/myspace/resolve/main/CommonLua/CrashReport.lua
- Command line
-
hf download hf://spaces/sirnii/myspace/CommonLua/CrashReport.lua
-
curl -L -o CrashReport.lua https://huggingface.co/spaces/sirnii/myspace/resolve/main/CommonLua/CrashReport.lua
25.9 kB
| local logs_folder = "AppData/crashes" | |
| function GatherMinidumps(ignore_pattern) | |
| local err, files = AsyncListFiles(logs_folder, "*.dmp", "recursive,modified") | |
| if err then | |
| print(string.format("Crash folder enum error: %s", err)) | |
| return | |
| end | |
| if ignore_pattern then | |
| for i = #files, 1, -1 do | |
| local filepath = files[i] | |
| local _, filename = SplitPath(filepath) | |
| if string.match(filename, ignore_pattern) then | |
| table.remove(files, i) | |
| table.remove(files.modified, i) | |
| end | |
| end | |
| end | |
| return files | |
| end | |
| local function check(str, what) | |
| return string.starts_with(str, what, true) | |
| end | |
| function CrashFileParse(crash_file) | |
| local info = {} | |
| local crash_section_found, crash_section_complete | |
| local err, lines = AsyncFileToString(crash_file, nil, nil, "lines") | |
| if err then | |
| return err | |
| end | |
| PauseInfiniteLoopDetection("CrashFileParse") | |
| local crash_keys = { "Thread", "Module", "Address", "Function", "Process", "Error", "Details" } | |
| local header_keys = {"Lua revision", "Timestamp", "CPU", "GPU" } | |
| local patterns = { | |
| ["Lua revision"] = '^Lua revision:%s*(%d+)', | |
| ["Timestamp"] = "^Timestamp:%s*(%x+)", | |
| ["CPU"] = "^CPU%s*(.+)", | |
| ["GPU"] = "^GPU%s*(.+)", | |
| } | |
| local values = {} | |
| local _ | |
| local bR = string.byte("R") | |
| local b_ = string.byte("-") | |
| local bkeys, hkeys = {}, {} | |
| for i, key in ipairs(crash_keys) do | |
| bkeys[i] = string.byte(key) | |
| end | |
| for i, key in ipairs(header_keys) do | |
| hkeys[i] = string.byte(key) | |
| end | |
| for i, line in ipairs(lines) do | |
| local b = string.byte(line) | |
| for i, key in ipairs(header_keys) do | |
| if b == hkeys[i] and check(line, key) then | |
| local pattern = patterns[key] or ('^' .. key .. ':%s+(.+)$') | |
| local value = string.match(line, pattern) | |
| value = value and string.trim_spaces(value) | |
| if value then | |
| value = string.gsub(value, "[\n\r]", "") | |
| if key == "GPU" then | |
| local idx = string.find_lower(value, 'Feature Level') or string.find_lower(value, '{') | |
| if idx then | |
| value = string.sub(value, 1, idx - 1) | |
| value = string.trim_spaces(value) | |
| end | |
| elseif key == "CPU" then | |
| if string.starts_with(value, 'name', true) then | |
| value = string.sub(value, 5) | |
| value = string.trim_spaces(value) | |
| end | |
| end | |
| info[#info + 1] = key .. ": " .. value | |
| values[key] = value | |
| table.remove(header_keys, i) | |
| table.remove(hkeys, i) | |
| end | |
| break | |
| end | |
| end | |
| if crash_section_complete then | |
| -- | |
| elseif not crash_section_found then | |
| if b_ == b and check(line, "-- Exception Information") then | |
| crash_section_found = true | |
| end | |
| else | |
| if b == bR and check(line, "Registers:") or #crash_keys == 0 then | |
| crash_section_complete = true | |
| else | |
| for i, key in ipairs(crash_keys) do | |
| if b == bkeys[i] and check(line, key) then | |
| local value = string.match(line, '^' .. key .. ':%s+(.+)$') | |
| value = value and string.trim_spaces(value) | |
| if value then | |
| info[#info + 1] = key .. ": " .. value | |
| if key == "Thread" then | |
| _, value = string.match(value, '^(%d+)%s*\"(.+)\"$') | |
| elseif key == "Address" then | |
| value = string.sub(value, -4) | |
| end | |
| values[key] = value | |
| table.remove(crash_keys, i) | |
| table.remove(bkeys, i) | |
| end | |
| break | |
| end | |
| end | |
| end | |
| end | |
| if (#crash_keys == 0 or crash_section_complete) and (#header_keys == 0 or i > 1024) then | |
| break | |
| end | |
| end | |
| ResumeInfiniteLoopDetection("CrashFileParse") | |
| if not crash_section_found then | |
| return "Crash info not found" | |
| end | |
| local hash = xxhash(values.Address, values.Thread, values.Error, values.Details) | |
| local label = string.format("[Crash] @%s%s%s (%s) %s%s%s", | |
| values.Address or "", values.Function and " " or "", values.Function or "", | |
| values.Thread or "", | |
| values.Error or "", values.Details and ": " or "", values.Details or "") | |
| local revision = values["Lua revision"] | |
| local revision_num = revision and tonumber(revision) or 0 | |
| local info_str = table.concat(info, "\n") | |
| return nil, info_str, label, values, revision_num, hash | |
| end | |
| function CrashUploadToMantis(minidumps) | |
| local exception_info = {} | |
| local min_revision = config.BugReportCrashesMinRevision or 0 | |
| local unmount | |
| local function report(dump_file) | |
| local dump_dir, dump_name, dump_ext = SplitPath(dump_file) | |
| local crash_file = dump_dir .. dump_name .. ".crash" | |
| local err, info_str, label, values, revision_num, hash = CrashFileParse(crash_file) | |
| if err or not info_str or revision_num < min_revision or exception_info[hash] then | |
| return | |
| end | |
| exception_info[hash] = true | |
| if MountsByPath("memorytmp") == 0 then | |
| local err = MountPack("memorytmp", "", "create", 16*1024*1024) | |
| if err then | |
| print("MountPack error:", err) | |
| return | |
| end | |
| unmount = true | |
| end | |
| local pack_file = "memorytmp/" .. dump_name .. ".hpk" | |
| local pack_index = { | |
| { src = dump_file, dst = dump_name .. ".dmp", }, | |
| } | |
| local err, log = AsyncPack(pack_file, "", pack_index) | |
| if err then | |
| print("Pack error:", err) | |
| return | |
| end | |
| --print(os.date("%T"), ConvertToOSPath(crash_file)) | |
| local files = { crash_file, pack_file } | |
| local descr = "All crash and dump files are already attached." | |
| WaitXBugReportDlg(label, descr, files, { | |
| summary_readonly = true, | |
| no_screenshot = true, | |
| no_extra_info = true, | |
| append_description = "\n----\n" .. info_str, | |
| tags = { "Crash" }, | |
| severity = "crash", | |
| }) | |
| AsyncFileDelete(pack_file) | |
| end | |
| for _, minidump in ipairs(minidumps) do | |
| report(minidump) | |
| end | |
| if unmount then | |
| local err = UnmountByPath("memorytmp") | |
| if err then | |
| print("UnmountByPath error:", err) | |
| return | |
| end | |
| end | |
| end | |
| function MinidumpUploadAsync(url, os_path) | |
| local err, json = LuaToJSON({upload_file_minidump=os_path}) | |
| if err then | |
| print("Failed to convert minidump data to JSON", err) | |
| return | |
| end | |
| local err, info = AsyncWebRequest{ | |
| url = url, | |
| method = "POST", | |
| headers = {["Content-Type"] = "application/json"}, | |
| body = json, | |
| } | |
| err = err or info and info.error | |
| if err then | |
| print(string.format("Minidump upload fail: %s", err)) | |
| end | |
| end | |
| function GetCrashFiles(file_spec) | |
| local _, crash_files = AsyncListFiles(logs_folder, file_spec or "*.crash", "recursive,modified") | |
| crash_files = crash_files or {} | |
| local crash_date, index = table.max(crash_files.modified) | |
| return crash_files, crash_files[index], crash_date, index | |
| end | |
| function EmptyCrashFolder() | |
| return AsyncEmptyPath(logs_folder) | |
| end | |
| function CrashReportingEnabled() | |
| if not Platform.pc then return end | |
| return config.UploadMinidump or config.BugReportCrashesOnStartup | |
| end | |
| function RenameCrashPair(minidump, new_minidump) | |
| AsyncFileRename(minidump, new_minidump) | |
| local crash_file = string.gsub(minidump, ".dmp$", ".crash") | |
| local new_crash_file = string.gsub(new_minidump, ".dmp$", ".crash") | |
| AsyncFileRename(crash_file, new_crash_file) | |
| end | |
| if FirstLoad then | |
| g_bCrashReported = false | |
| end | |
| function WaitBugReportCrashesOnStartup() | |
| local _, minidump = GetCrashFiles("*.dmp") | |
| if not minidump then return end | |
| CrashUploadToMantis({minidump}) | |
| EmptyCrashFolder() | |
| end | |
| function OnMsg.EngineStarted() | |
| if not config.BugReportCrashesOnStartup or g_bCrashReported then return end | |
| g_bCrashReported = true | |
| CreateRealTimeThread(WaitBugReportCrashesOnStartup) | |
| end | |
| ---- | |
| if FirstLoad then | |
| SymbolsFolders = false | |
| GedFolderCrashesInstance = false | |
| CrashCache = false | |
| CrashFilter = false | |
| CrashResolved = false | |
| end | |
| CrashCacheVersion = 4 | |
| local base_cache_folder = "AppData/CrashCache/" | |
| local cache_file = base_cache_folder .. "CrashCache.bin" | |
| local resolved_file = ConvertToBenderProjectPath("Logs/Crashes/__Resolved.lua") | |
| CrashFolderSymbols = ConvertToBenderProjectPath("Logs/Pdbs/") | |
| CrashFolderBender = ConvertToBenderProjectPath("Logs/Crashes") | |
| CrashFolderSwarm = ConvertToBenderProjectPath("SwarmBackup/*/Storage/log-crash") | |
| CrashFolderLocal = "AppData/crashes" | |
| local defaults_groups = { | |
| SwarmBackup = ">Swarm", | |
| } | |
| local CrashInfoButtons = { | |
| {name = "LocateSymbols", func = "SymbolsFolderOpen"}, | |
| } | |
| DefineClass.CrashInfo = { | |
| __parents = {"PropertyObject"}, | |
| properties = { | |
| { category = "Actions", id = "Actions", editor = "buttons", default = "", buttons = CrashInfoButtons }, | |
| { category = "Crash", id = "ExeTimestamp", name = "Exe Timestamp", editor = "text", default = "" }, | |
| { category = "Crash", id = "SymbolsFolder", name = "Symbols Folder", editor = "text", default = "", buttons = {{name = "Open", func = "SymbolsFolderOpen"}} }, | |
| } | |
| } | |
| function CrashInfo:SymbolsFolderOpen() | |
| local bdb_folder = self.SymbolsFolder | |
| if bdb_folder ~= 0 then | |
| local os_command = string.format("cmd /c start \"\" \"%s\"", bdb_folder) | |
| os.execute(os_command) | |
| end | |
| end | |
| function CrashInfo:GetCacheFolder() | |
| local timestamp = self.ExeTimestamp | |
| if timestamp == "" then | |
| return "Missing timestamp!" | |
| end | |
| local cache_folder = base_cache_folder .. timestamp .. "/" | |
| if not io.exists(cache_folder) then | |
| local err = AsyncCreatePath(cache_folder) | |
| if err then | |
| return err | |
| end | |
| end | |
| return nil, cache_folder | |
| end | |
| function GedFolderCrashesRun(get) | |
| local crash = get.selected_object | |
| if crash then crash:OpenLogFile() end | |
| end | |
| DefineClass.FolderCrashGroup = { | |
| __parents = {"SortedBy", "GedFilter" }, | |
| properties = { | |
| { id = "name", editor = "text", default = "", read_only = true, buttons = {{name = "Export", func = "ExportToCSV"}} }, | |
| { id = "count", editor = "number", default = 0, read_only = true }, | |
| { id = "thread", name = "Show Thread", editor = "combo", default = false, items = function(self) return table.keys(self.threads, true) end }, | |
| { id = "timestamp", name = "Show Timestamp", editor = "combo", default = false, items = function(self) return table.keys(self.timestamps, true) end }, | |
| { id = "filter", name = "Show Name", editor = "combo", default = false, items = function(self) return table.keys(self.names, true) end }, | |
| { id = "cpu", name = "Show CPU", editor = "combo", default = false, items = function(self) return table.keys(self.cpus, true) end }, | |
| { id = "gpu", name = "Show GPU", editor = "combo", default = false, items = function(self) return table.keys(self.gpus, true) end }, | |
| { id = "unique", name = "Show Unique Only", editor = "bool", default = false }, | |
| { id = "resolved", name = "Show Resolved", editor = "bool", default = false }, | |
| { id = "shown_count", name = "Shown Count", editor = "number", default = 0, read_only = true }, | |
| }, | |
| shown = false, | |
| names = false, | |
| timestamps = false, | |
| threads = false, | |
| cpus = false, | |
| gpus = false, | |
| } | |
| function FolderCrashGroup:PrepareForFiltering() | |
| self.shown = {} | |
| self.shown_count = 0 | |
| end | |
| function FolderCrashGroup:FilterObject(obj) | |
| local name = obj.name | |
| if self.unique then | |
| if self.shown[name] then | |
| return | |
| end | |
| self.shown[name] = true | |
| end | |
| if not self.resolved and CrashResolved and CrashResolved[obj.hash] then | |
| return | |
| end | |
| local timestamp = self.timestamp | |
| if timestamp and timestamp ~= obj.ExeTimestamp then | |
| return | |
| end | |
| local thread = self.thread | |
| if thread and thread ~= obj.thread then | |
| return | |
| end | |
| local cpu = self.cpu | |
| if cpu and cpu ~= obj.CPU then | |
| return | |
| end | |
| local gpu = self.gpu | |
| if gpu and gpu ~= obj.GPU then | |
| return | |
| end | |
| local filter = self.filter | |
| if filter and filter ~= name and not string.find(name, filter) then | |
| return | |
| end | |
| self.shown_count = self.shown_count + 1 | |
| return true | |
| end | |
| function FolderCrashGroup:ExportToCSV() | |
| local name = string.starts_with(self.name, ">") and string.sub(self.name, 2) or self.name | |
| local path = base_cache_folder .. name .. ".csv" | |
| local err = SaveCSV(path, self, {"name", "thread", "date", "CPU", "GPU", "ExeTimestamp"}, {"name", "thread", "date", "CPU", "GPU", "Exe"}) | |
| if err then | |
| print(err, "while exporting", path) | |
| else | |
| print("Exported to", path) | |
| OpenTextFileWithEditorOfChoice(path) | |
| end | |
| end | |
| function FolderCrashGroup:GetSortItems() | |
| return {"name", "timestamp", "thread", "date", "CPU", "GPU", "occurrences"} | |
| end | |
| function FolderCrashGroup:Cmp(c1, c2, sort_by) | |
| local n1, n2 = c1.name, c2.name | |
| local ts1, ts2 = c1.ExeTimestamp, c2.ExeTimestamp | |
| local d1, d2 = c1.DmpTimestamp, c2.DmpTimestamp | |
| local CPU1, CPU2 = c1.CPU, c2.CPU | |
| local GPU1, GPU2 = c1.GPU, c2.GPU | |
| local o1, o2 = c1.occurrences, c2.occurrences | |
| if sort_by == "occurrences" then | |
| if o1 ~= o2 then | |
| return o1 > o2 | |
| end | |
| elseif sort_by == "date" then | |
| if d1 ~= d2 then | |
| return d1 < d2 | |
| end | |
| elseif sort_by == "thread" then | |
| local t1, t2 = c1.thread, c2.thread | |
| if t1 ~= t2 then | |
| return t1 < t2 | |
| end | |
| elseif sort_by == "timestamp" then | |
| if ts1 ~= ts2 then | |
| return ts1 < ts2 | |
| end | |
| elseif sort_by == "CPU" then | |
| if CPU1 ~= CPU2 then | |
| return CPU1 < CPU2 | |
| end | |
| elseif sort_by == "GPU" then | |
| if GPU1 ~= GPU2 then | |
| return GPU1 < GPU2 | |
| end | |
| end | |
| if n1 ~= n2 then | |
| return n1 < n2 | |
| end | |
| if ts1 ~= ts2 then | |
| return ts1 < ts2 | |
| end | |
| if d1 ~= d2 then | |
| return d1 < d2 | |
| end | |
| if o1 ~= o2 then | |
| return o1 > o2 | |
| end | |
| if GPU1 ~= GPU2 then | |
| return GPU1 < GPU2 | |
| end | |
| if CPU1 ~= CPU2 then | |
| return CPU1 < CPU2 | |
| end | |
| end | |
| function FolderCrashGroup:GetEditorView() | |
| return string.format("%s <color 128 128 128>%d</color>", self.name, self.count) | |
| end | |
| local FolderCrashButtons = { | |
| {name = "DebugInVS", func = "DebugDump"}, | |
| {name = "LocateSymbols", func = "SymbolsFolderOpen"}, | |
| {name = "OpenLog", func = "OpenLogFile"}, | |
| {name = "Resolve", func = "ResolveCrash"}, | |
| } | |
| DefineClass.FolderCrash = { | |
| __parents = {"CrashInfo"}, | |
| properties = { | |
| { category = "Actions", id = "Actions", editor = "buttons", default = "", buttons = FolderCrashButtons }, | |
| { category = "Actions", id = "Resolved", editor = "bool", default = false, read_only = true }, | |
| { category = "Crash", id = "ModuleName", editor = "text", default = "" }, | |
| { category = "Crash", id = "LocalModuleName", editor = "text", default = "", help = "Use it to change the symbols name locally, if the expected PDB name do not match" }, | |
| { category = "Crash", id = "name", name = "Summary", editor = "text", default = "" }, | |
| { category = "Crash", id = "occurrences", name = "Occurrences", editor = "number", default = 0, }, | |
| { category = "Crash", id = "date", name = "Dmp Date", editor = "text", default = "" }, | |
| { category = "Crash", id = "DmpTimestamp", name = "Dmp Timestamp", editor = "number", default = 0 }, | |
| { category = "Crash", id = "thread", editor = "text", default = "" }, | |
| { category = "Crash", id = "CPU", editor = "text", default = "" }, | |
| { category = "Crash", id = "GPU", editor = "text", default = "" }, | |
| { category = "Crash", id = "full_path", name = "Log Path", editor = "text", default = "", buttons = {{name = "Open", func = "OpenLogFile"}}, }, | |
| { category = "Crash", id = "crash_info", name = "Full Info", editor = "text", default = "", max_lines = 30, lines = 10, }, | |
| { category = "Crash", id = "dump_file", name = "text", editor = "text", default = "", no_edit = true, }, | |
| { category = "Crash", id = "group", name = "text", editor = "text", default = "", no_edit = true, }, | |
| { category = "Crash", id = "values", editor = "prop_table", default = false, no_edit = true, }, | |
| { category = "Crash", id = "hash", editor = "number", default = false, no_edit = true, }, | |
| }, | |
| StoreAsTable = true, | |
| CustomModuleName = false, | |
| } | |
| function WaitSaveCrashResolved() | |
| local code = pstr("return ", 1024) | |
| TableToLuaCode(CrashResolved, nil, code) | |
| local err = AsyncStringToFile(resolved_file, code) | |
| if err then | |
| print("once", "Failed to save the resolved crashes to", resolved_file, ":", err) | |
| end | |
| end | |
| function FolderCrash:GetModuleNameRaw() | |
| local module_file = self.values and self.values.Module | |
| if (module_file or "") == "" then | |
| return "" | |
| end | |
| local module_dir, module_name, module_ext = SplitPath(module_file) | |
| return module_name | |
| end | |
| function FolderCrash:GetModuleName() | |
| if (self.CustomModuleName or "") ~= "" then | |
| return self.CustomModuleName | |
| end | |
| return self:GetModuleNameRaw() | |
| end | |
| function FolderCrash:SetModuleName(module_name) | |
| self.CustomModuleName = (module_name or "") ~= "" and module_name ~= self:GetModuleNameRaw() and module_name or nil | |
| end | |
| function FolderCrash:GetResolved() | |
| return CrashResolved and CrashResolved[self.hash] | |
| end | |
| function FolderCrash:ResolveCrash(root, prop_id, ged) | |
| if self:GetResolved() then | |
| print(self.name, "is already resolved") | |
| return | |
| end | |
| if ged:WaitQuestion("Resolve", string.format("Mark crash \"%s\" as resolved?", self.name), "Yes", "No") ~= "ok" then | |
| return | |
| end | |
| CrashResolved = CrashResolved or {} | |
| CrashResolved[self.hash] = self.name .. " " .. self.ExeTimestamp | |
| DelayedCall(0, WaitSaveCrashResolved) | |
| end | |
| function FolderCrash:GetEditorView() | |
| local resolved = self:GetResolved() | |
| local color_start = resolved and "RESOLVED <color 128 128 128>" or "" | |
| local color_end = resolved and "</color>" or "" | |
| return string.format("<style GedMultiLine>%s%s%s <color 64 128 196>%s</color> <color 64 196 128>%s</color> <color 196 128 64>%s</color></style>", | |
| color_start, self.name, color_end, self.ExeTimestamp, self.CPU, self.GPU) | |
| end | |
| function FolderCrash:OpenLogFile() | |
| local full_path = self.full_path or "" | |
| if full_path ~= "" then | |
| OpenTextFileWithEditorOfChoice(full_path) | |
| end | |
| end | |
| function CopySymbols(cache_folder, src_folder, module_name, local_name) | |
| if (module_name or "") == "" then | |
| return "Invalid param!" | |
| end | |
| if (local_name or "") == "" then | |
| local_name = module_name | |
| end | |
| local pdbfile = cache_folder .. local_name .. ".pdb" | |
| if io.exists(pdbfile) then | |
| print("Using locally cached", pdbfile) | |
| return | |
| end | |
| if src_folder == "" then | |
| return "Symbols folder not found!" | |
| end | |
| local err, files = AsyncListFiles(src_folder, module_name .. ".*") | |
| if err then | |
| return print_format("Failed to list", src_folder, ":", err) | |
| end | |
| for _, file in ipairs(files) do | |
| local file_dir, file_name, file_ext = SplitPath(file) | |
| local dest = cache_folder .. local_name .. file_ext | |
| print("Copying", file, "to", dest) | |
| local err = AsyncCopyFile(file, dest, "raw") | |
| if err then | |
| return print_format("Failed to copy", file, ":", err) | |
| end | |
| end | |
| if not io.exists(pdbfile) then | |
| return print_format("No symbols found at", src_folder) | |
| end | |
| end | |
| function FolderCrash:DebugDump() | |
| if not Platform.pc then | |
| print("Supported on PC only!") | |
| return | |
| end | |
| local err | |
| local module_name = self:GetModuleName() or "" | |
| if module_name == "" or string.lower(module_name) == "unknown" then | |
| print("Invalid module name!") | |
| return | |
| end | |
| local err, cache_folder = self:GetCacheFolder() | |
| if err then | |
| print("Failed to create working directory:", err) | |
| return | |
| end | |
| local orig_dump_file = self.dump_file | |
| local orig_dump_dir, dump_name, dump_ext = SplitPath(orig_dump_file) | |
| local dump_file = cache_folder .. dump_name .. dump_ext | |
| if not io.exists(dump_file) then | |
| if not io.exists(orig_dump_file) then | |
| print("No dump pack found!") | |
| return | |
| end | |
| local err = AsyncCopyFile(orig_dump_file, dump_file, "raw") | |
| if err then | |
| print("Failed to copy", orig_dump_file, ":", err) | |
| return | |
| end | |
| end | |
| local err = CopySymbols(cache_folder, self.SymbolsFolder, module_name, self.LocalModuleName) | |
| if err then | |
| print("Copy symbols error:", err) | |
| return | |
| end | |
| local os_path = ConvertToOSPath(dump_file) | |
| local os_command = string.format("cmd /c start \"\" \"%s\"", os_path) | |
| os.execute(os_command) | |
| end | |
| function FetchSymbolsFolders() | |
| local err | |
| local st = GetPreciseTicks() | |
| err, SymbolsFolders = AsyncListFiles(CrashFolderSymbols, "*", "folders") | |
| if err then | |
| print("Failed to fetch symbols folders from Bender:", err) | |
| SymbolsFolders = {} | |
| end | |
| print(#SymbolsFolders, "symbol folders found in", GetPreciseTicks() - st, "ms at", CrashFolderSymbols) | |
| end | |
| function ResolveSymbolsFolder(timestamp) | |
| if (timestamp or "") == "" then | |
| return | |
| end | |
| assert(SymbolsFolders) | |
| for _, folder in ipairs(SymbolsFolders) do | |
| if string.ends_with(folder, timestamp, true) then | |
| return folder | |
| end | |
| end | |
| end | |
| function OpenCrashFolderBrowser(location, timestamp) | |
| CreateRealTimeThread(WaitOpenCrashFolderBrowser, location, timestamp) | |
| end | |
| function WaitOpenCrashFolderBrowser(location, timestamp) | |
| print("Opening crash folder browser at", location) | |
| FetchSymbolsFolders() | |
| if not CrashCache then | |
| local err, str = AsyncFileToString(cache_file) | |
| if not err then | |
| CrashCache = dostring(str) | |
| end | |
| if not CrashCache or CrashCache.version ~= CrashCacheVersion then | |
| CrashCache = { version = CrashCacheVersion } | |
| end | |
| end | |
| if not CrashResolved then | |
| local err, str = AsyncFileToString(resolved_file) | |
| if not err then | |
| CrashResolved = dostring(str) | |
| end | |
| if not CrashResolved then | |
| CrashResolved = {} | |
| end | |
| end | |
| local to_read, to_delete = {}, {} | |
| local to_delete_count = 0 | |
| local groups = {} | |
| local total_count = 0 | |
| local function AddCrashTo(crash, crash_name, group_name) | |
| local group = groups[group_name] | |
| if not group then | |
| group = FolderCrashGroup:new{ name = group_name } | |
| groups[group_name] = group | |
| groups[#groups + 1] = group | |
| end | |
| group[#group + 1] = crash | |
| end | |
| local skipped = 0 | |
| local function AddCrash(crash, group_name) | |
| if timestamp and timestamp ~= crash.ExeTimestamp then | |
| skipped = skipped + 1 | |
| return | |
| end | |
| AddCrashTo(crash, crash.name, crash.group) | |
| AddCrashTo(crash, crash.name, ">All") | |
| total_count = total_count + 1 | |
| end | |
| local created = 0 | |
| local read = 0 | |
| local function ReadCrash(info) | |
| read = read + 1 | |
| local crashfile, folder = info[1], info[2] | |
| local file_dir, file_name, file_ext = SplitPath(crashfile) | |
| local dump_file = file_dir .. file_name .. ".dmp" | |
| local err, info, label, values, revision_num, hash, DmpTimestamp | |
| err, DmpTimestamp = AsyncGetFileAttribute(dump_file, "timestamp") | |
| if err then | |
| print(err, "while getting timestamp of", dump_file) | |
| else | |
| err, info, label, values, revision_num, hash = CrashFileParse(crashfile) | |
| if err then | |
| print(err, "error while reading", crashfile) | |
| end | |
| end | |
| if err then | |
| to_delete_count = to_delete_count + 1 | |
| to_delete[#to_delete + 1] = crashfile | |
| to_delete[#to_delete + 1] = dump_file | |
| return | |
| end | |
| local group_name = string.sub(file_dir, #folder + 2) | |
| if group_name == "" then | |
| group_name = ">Ungrouped" | |
| for pattern, name in pairs(defaults_groups) do | |
| if file_dir:find(pattern) then | |
| group_name = name | |
| break | |
| end | |
| end | |
| else | |
| group_name = group_name:sub(1, -2) | |
| group_name = group_name:gsub("\\", "/") | |
| end | |
| local crash = FolderCrash:new{ | |
| dump_file = dump_file, | |
| group = group_name, | |
| folder = file_dir, | |
| name = label, | |
| full_path = crashfile, | |
| crash_info = info, | |
| date = os.date("%y/%m/%d %H:%M:%S", DmpTimestamp), | |
| DmpTimestamp = DmpTimestamp, | |
| ExeTimestamp = values.Timestamp, | |
| SymbolsFolder = ResolveSymbolsFolder(values.Timestamp), | |
| CPU = values.CPU, | |
| GPU = values.GPU, | |
| thread = values.Thread, | |
| values = values, | |
| hash = hash, | |
| } | |
| CrashCache[crashfile] = crash | |
| AddCrash(crash) | |
| created = created + 1 | |
| if read % 100 == 0 then | |
| print(#to_read - read, "remaining...") | |
| end | |
| end | |
| local folders | |
| if type(location) == "string" then | |
| folders = { location } | |
| elseif type(location) == "table" then | |
| folders = location | |
| else | |
| folders = { CrashFolderBender } | |
| end | |
| print("Fetching folder structure...") | |
| while true do | |
| local found | |
| for i=#folders,1,-1 do | |
| local folder = folders[i] | |
| local star_i = folder:find_lower("*") | |
| if star_i then | |
| found = true | |
| table.remove(folders, i) | |
| local base = folder:sub(1, star_i - 1) | |
| local sub = folder:sub(star_i + 1) | |
| local err, subfolders = AsyncListFiles(base, "*", "folders") | |
| if err then | |
| print("Failed to fetch issues from", base, ":", err) | |
| else | |
| for _, subfolder in ipairs(subfolders) do | |
| local f1 = subfolder .. sub | |
| if io.exists(f1) then | |
| folders[#folders + 1] = f1 | |
| end | |
| end | |
| end | |
| end | |
| end | |
| if not found then | |
| break | |
| end | |
| end | |
| for _, folder in ipairs(folders) do | |
| if folder:ends_with("/") or folder:ends_with("\\") then | |
| folder = folder:sub(1, -2) | |
| end | |
| local st = GetPreciseTicks() | |
| local err, files = AsyncListFiles(folder, "*.crash", "recursive") | |
| if err then | |
| printf("Failed to fetch issues (%s) from '%s'", err, folder) | |
| else | |
| printf("%d crashes found in '%s'", #files, folder) | |
| for i, crashfile in ipairs(files) do | |
| local group_name | |
| local cache = CrashCache[crashfile] | |
| if cache then | |
| AddCrash(cache) | |
| else | |
| to_read[#to_read + 1] = { crashfile, folder } | |
| end | |
| end | |
| end | |
| end | |
| local st = GetPreciseTicks() | |
| parallel_foreach(to_read, ReadCrash) | |
| table.sortby_field(groups, "name") | |
| for _, group in ipairs(groups) do | |
| local names, timestamps, threads, gpus, cpus = {}, {}, {}, {}, {} | |
| group.names = names | |
| group.timestamps = timestamps | |
| group.threads = threads | |
| group.gpus = gpus | |
| group.cpus = cpus | |
| for _, crash in ipairs(group) do | |
| local name = crash.name | |
| names[name] = (names[name] or 0) + 1 | |
| timestamps[crash.ExeTimestamp] = true | |
| threads[crash.thread] = true | |
| gpus[crash.GPU] = true | |
| cpus[crash.CPU] = true | |
| end | |
| for _, crash in ipairs(group) do | |
| crash.occurrences = names[crash.name] | |
| end | |
| group:Sort() | |
| group.count = #group | |
| end | |
| print("Crashes processed:", total_count, ", skipped:", skipped, ", time:", GetPreciseTicks() - st, "ms") | |
| if created > 0 then | |
| local code = pstr("return ", 1024) | |
| TableToLuaCode(CrashCache, nil, code) | |
| AsyncCreatePath(base_cache_folder) | |
| local err = AsyncStringToFile(cache_file, code, -2, 0, "zstd") | |
| if err then | |
| print("once", "Failed to save the crash cache to", cache_file, ":", err) | |
| end | |
| end | |
| local ged = OpenGedAppSingleton("GedFolderCrashes", groups) | |
| ged:SetSelection("root", { 1 }, nil, not "notify") | |
| if to_delete_count > 0 then | |
| if "ok" == WaitQuestion(terminal.desktop, "Warning", string.format("Confirm removal of %s invalid crash files?", to_delete_count)) then | |
| local err = AsyncFileDelete(to_delete) | |
| if err then | |
| print(err, "while deleting invalid crash files!") | |
| else | |
| print(to_delete_count, "invalid crash files removed.") | |
| end | |
| end | |
| end | |
| end | |