You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
367 lines
7.8 KiB
367 lines
7.8 KiB
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); |
|
} |
|
}
|
|
|