import { $el, ComfyDialog } from "./ui.js"; import { api } from "./api.js"; $el("style", { textContent: ` .comfy-logging-logs { display: grid; color: var(--fg-color); white-space: pre-wrap; } .comfy-logging-log { display: contents; } .comfy-logging-title { background: var(--tr-even-bg-color); font-weight: bold; margin-bottom: 5px; text-align: center; } .comfy-logging-log div { background: var(--row-bg); padding: 5px; } `, parent: document.body, }); // Stringify function supporting max depth and removal of circular references // https://stackoverflow.com/a/57193345 function stringify(val, depth, replacer, space, onGetObjID) { depth = isNaN(+depth) ? 1 : depth; var recursMap = new WeakMap(); function _build(val, depth, o, a, r) { // (JSON.stringify() has it's own rules, which we respect here by using it for property iteration) return !val || typeof val != "object" ? val : ((r = recursMap.has(val)), recursMap.set(val, true), (a = Array.isArray(val)), r ? (o = (onGetObjID && onGetObjID(val)) || null) : JSON.stringify(val, function (k, v) { if (a || depth > 0) { if (replacer) v = replacer(k, v); if (!k) return (a = Array.isArray(v)), (val = v); !o && (o = a ? [] : {}); o[k] = _build(v, a ? depth : depth - 1); } }), o === void 0 ? (a ? [] : {}) : o); } return JSON.stringify(_build(val, depth), null, space); } const jsonReplacer = (k, v, ui) => { if (v instanceof Array && v.length === 1) { v = v[0]; } if (v instanceof Date) { v = v.toISOString(); if (ui) { v = v.split("T")[1]; } } if (v instanceof Error) { let err = ""; if (v.name) err += v.name + "\n"; if (v.message) err += v.message + "\n"; if (v.stack) err += v.stack + "\n"; if (!err) { err = v.toString(); } v = err; } return v; }; const fileInput = $el("input", { type: "file", accept: ".json", style: { display: "none" }, parent: document.body, }); class ComfyLoggingDialog extends ComfyDialog { constructor(logging) { super(); this.logging = logging; } clear() { this.logging.clear(); this.show(); } export() { const blob = new Blob([stringify([...this.logging.entries], 20, jsonReplacer, "\t")], { type: "application/json", }); const url = URL.createObjectURL(blob); const a = $el("a", { href: url, download: `comfyui-logs-${Date.now()}.json`, style: { display: "none" }, parent: document.body, }); a.click(); setTimeout(function () { a.remove(); window.URL.revokeObjectURL(url); }, 0); } import() { fileInput.onchange = () => { const reader = new FileReader(); reader.onload = () => { fileInput.remove(); try { const obj = JSON.parse(reader.result); if (obj instanceof Array) { this.show(obj); } else { throw new Error("Invalid file selected."); } } catch (error) { alert("Unable to load logs: " + error.message); } }; reader.readAsText(fileInput.files[0]); }; fileInput.click(); } createButtons() { return [ $el("button", { type: "button", textContent: "Clear", onclick: () => this.clear(), }), $el("button", { type: "button", textContent: "Export logs...", onclick: () => this.export(), }), $el("button", { type: "button", textContent: "View exported logs...", onclick: () => this.import(), }), ...super.createButtons(), ]; } getTypeColor(type) { switch (type) { case "error": return "red"; case "warn": return "orange"; case "debug": return "dodgerblue"; } } show(entries) { if (!entries) entries = this.logging.entries; this.element.style.width = "100%"; const cols = { source: "Source", type: "Type", timestamp: "Timestamp", message: "Message", }; const keys = Object.keys(cols); const headers = Object.values(cols).map((title) => $el("div.comfy-logging-title", { textContent: title, }) ); const rows = entries.map((entry, i) => { return $el( "div.comfy-logging-log", { $: (el) => el.style.setProperty("--row-bg", `var(--tr-${i % 2 ? "even" : "odd"}-bg-color)`), }, keys.map((key) => { let v = entry[key]; let color; if (key === "type") { color = this.getTypeColor(v); } else { v = jsonReplacer(key, v, true); if (typeof v === "object") { v = stringify(v, 5, jsonReplacer, " "); } } return $el("div", { style: { color, }, textContent: v, }); }) ); }); const grid = $el( "div.comfy-logging-logs", { style: { gridTemplateColumns: `repeat(${headers.length}, 1fr)`, }, }, [...headers, ...rows] ); const els = [grid]; if (!this.logging.enabled) { els.unshift( $el("h3", { style: { textAlign: "center" }, textContent: "Logging is disabled", }) ); } super.show($el("div", els)); } } export class ComfyLogging { /** * @type Array<{ source: string, type: string, timestamp: Date, message: any }> */ entries = []; #enabled; #console = {}; get enabled() { return this.#enabled; } set enabled(value) { if (value === this.#enabled) return; if (value) { this.patchConsole(); } else { this.unpatchConsole(); } this.#enabled = value; } constructor(app) { this.app = app; this.dialog = new ComfyLoggingDialog(this); this.addSetting(); this.catchUnhandled(); this.addInitData(); } addSetting() { const settingId = "Comfy.Logging.Enabled"; const htmlSettingId = settingId.replaceAll(".", "-"); const setting = this.app.ui.settings.addSetting({ id: settingId, name: settingId, defaultValue: true, type: (name, setter, value) => { return $el("tr", [ $el("td", [ $el("label", { textContent: "Logging", for: htmlSettingId, }), ]), $el("td", [ $el("input", { id: htmlSettingId, type: "checkbox", checked: value, onchange: (event) => { setter((this.enabled = event.target.checked)); }, }), $el("button", { textContent: "View Logs", onclick: () => { this.app.ui.settings.element.close(); this.dialog.show(); }, style: { fontSize: "14px", display: "block", marginTop: "5px", }, }), ]), ]); }, }); this.enabled = setting.value; } patchConsole() { // Capture common console outputs const self = this; for (const type of ["log", "warn", "error", "debug"]) { const orig = console[type]; this.#console[type] = orig; console[type] = function () { orig.apply(console, arguments); self.addEntry("console", type, ...arguments); }; } } unpatchConsole() { // Restore original console functions for (const type of Object.keys(this.#console)) { console[type] = this.#console[type]; } this.#console = {}; } catchUnhandled() { // Capture uncaught errors window.addEventListener("error", (e) => { this.addEntry("window", "error", e.error ?? "Unknown error"); return false; }); window.addEventListener("unhandledrejection", (e) => { this.addEntry("unhandledrejection", "error", e.reason ?? "Unknown error"); }); } clear() { this.entries = []; } addEntry(source, type, ...args) { if (this.enabled) { this.entries.push({ source, type, timestamp: new Date(), message: args, }); } } log(source, ...args) { this.addEntry(source, "log", ...args); } async addInitData() { if (!this.enabled) return; const source = "ComfyUI.Logging"; this.addEntry(source, "debug", { UserAgent: navigator.userAgent }); const systemStats = await api.getSystemStats(); this.addEntry(source, "debug", systemStats); } }