diff --git a/.eslintrc.json b/.eslintrc.json index 62feb13a5..2dddb41a1 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -42,9 +42,13 @@ "globals": { // asssets "panzoom": "readonly", - // script.js + // logger.js "log": "readonly", "debug": "readonly", + "error": "readonly", + "xhrGet": "readonly", + "xhrPost": "readonly", + // script.js "gradioApp": "readonly", "executeCallbacks": "readonly", "onAfterUiUpdate": "readonly", @@ -87,7 +91,6 @@ // settings.js "registerDragDrop": "readonly", // extraNetworks.js - "requestGet": "readonly", "getENActiveTab": "readonly", "quickApplyStyle": "readonly", "quickSaveStyle": "readonly", diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dabf0d7c..4a4a0a416 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ - Flux: do not recast quants - Sampler improvements - update DPM FlowMatch samplers +- UI: + - browser->server logging - Fixes: - update `diffusers` - fix README links diff --git a/installer.py b/installer.py index 0b64c3616..8f552526a 100644 --- a/installer.py +++ b/installer.py @@ -459,7 +459,7 @@ def check_python(supported_minors=[9, 10, 11, 12], reason=None): def check_diffusers(): if args.skip_all or args.skip_requirements: return - sha = 'b5fd6f13f5434d69d919cc8cedf0b11db664cf06' + sha = '7ac6e286ee994270e737b70c904ea50049d53567' pkg = pkg_resources.working_set.by_key.get('diffusers', None) minor = int(pkg.version.split('.')[1] if pkg is not None else 0) cur = opts.get('diffusers_version', '') if minor > 0 else '' diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index 77fe125f3..622e40faf 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -3,19 +3,6 @@ let sortVal = -1; // helpers -const requestGet = (url, data, handler) => { - const xhr = new XMLHttpRequest(); - const args = Object.keys(data).map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(data[k])}`).join('&'); - xhr.open('GET', `${url}?${args}`, true); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - if (xhr.status === 200) handler(JSON.parse(xhr.responseText)); - else console.error(`Request: url=${url} status=${xhr.status} err`); - } - }; - xhr.send(JSON.stringify(data)); -}; - const getENActiveTab = () => { let tabName = ''; if (gradioApp().getElementById('tab_txt2img').style.display === 'block') tabName = 'txt2img'; @@ -98,7 +85,7 @@ function readCardTags(el, tags) { } function readCardDescription(page, item) { - requestGet('/sd_extra_networks/description', { page, item }, (data) => { + xhrGet('/sd_extra_networks/description', { page, item }, (data) => { const tabname = getENActiveTab(); const description = gradioApp().querySelector(`#${tabname}_description > label > textarea`); description.value = data?.description?.trim() || ''; diff --git a/javascript/gallery.js b/javascript/gallery.js index 1f3afd148..05e594e4c 100644 --- a/javascript/gallery.js +++ b/javascript/gallery.js @@ -94,14 +94,14 @@ async function delayFetchThumb(fn) { outstanding++; const res = await fetch(`/sdapi/v1/browser/thumb?file=${encodeURI(fn)}`, { priority: 'low' }); if (!res.ok) { - console.error(res.statusText); + error(`fetchThumb: ${res.statusText}`); outstanding--; return undefined; } const json = await res.json(); outstanding--; if (!res || !json || json.error || Object.keys(json).length === 0) { - if (json.error) console.error(json.error); + if (json.error) error(`fetchThumb: ${json.error}`); return undefined; } return json; diff --git a/javascript/loader.js b/javascript/loader.js index f3c7fe60f..8cd4811bf 100644 --- a/javascript/loader.js +++ b/javascript/loader.js @@ -20,7 +20,7 @@ async function preloadImages() { try { await Promise.all(imagePromises); } catch (error) { - console.error('Error preloading images:', error); + error(`preloadImages: ${error}`); } } @@ -43,14 +43,16 @@ async function createSplash() { const motdEl = document.getElementById('motd'); if (motdEl) motdEl.innerHTML = text.replace(/["]+/g, ''); }) - .catch((err) => console.error('getMOTD:', err)); + .catch((err) => error(`getMOTD: ${err}`)); } async function removeSplash() { const splash = document.getElementById('splash'); if (splash) splash.remove(); log('removeSplash'); - log('startupTime', Math.round(performance.now() - appStartTime) / 1000); + const t = Math.round(performance.now() - appStartTime) / 1000; + log('startupTime', t); + xhrPost('/sdapi/v1/log', { message: `ready time=${t}` }); } window.onload = createSplash; diff --git a/javascript/logMonitor.js b/javascript/logMonitor.js index e4fe99a7f..9b915e6da 100644 --- a/javascript/logMonitor.js +++ b/javascript/logMonitor.js @@ -2,6 +2,7 @@ let logMonitorEl = null; let logMonitorStatus = true; let logWarnings = 0; let logErrors = 0; +let logConnected = false; function dateToStr(ts) { const dt = new Date(1000 * ts); @@ -29,8 +30,7 @@ async function logMonitor() { row.innerHTML = `${dateToStr(l.created)}${level}${l.facility}${module}${l.msg}`; logMonitorEl.appendChild(row); } catch (e) { - // console.log('logMonitor', e); - console.error('logMonitor line', line); + error(`logMonitor: ${line}`); } }; @@ -46,6 +46,7 @@ async function logMonitor() { if (logMonitorStatus) setTimeout(logMonitor, opts.logmonitor_refresh_period); else setTimeout(logMonitor, 10 * 1000); // on failure try to reconnect every 10sec + if (!opts.logmonitor_show) return; logMonitorStatus = false; if (!logMonitorEl) { @@ -64,14 +65,20 @@ async function logMonitor() { const lines = await res.json(); if (logMonitorEl && lines?.length > 0) logMonitorEl.parentElement.parentElement.style.display = opts.logmonitor_show ? 'block' : 'none'; for (const line of lines) addLogLine(line); + if (!logConnected) { + logConnected = true; + xhrPost('/sdapi/v1/log', { debug: 'connected' }); + } } else { - addLogLine(`{ "created": ${Date.now()}, "level":"ERROR", "module":"logMonitor", "facility":"ui", "msg":"Failed to fetch log: ${res?.status} ${res?.statusText}" }`); + logConnected = false; logErrors++; + addLogLine(`{ "created": ${Date.now()}, "level":"ERROR", "module":"logMonitor", "facility":"ui", "msg":"Failed to fetch log: ${res?.status} ${res?.statusText}" }`); } cleanupLog(atBottom); } catch (err) { - addLogLine(`{ "created": ${Date.now()}, "level":"ERROR", "module":"logMonitor", "facility":"ui", "msg":"Failed to fetch log: server unreachable" }`); + logConnected = false; logErrors++; + addLogLine(`{ "created": ${Date.now()}, "level":"ERROR", "module":"logMonitor", "facility":"ui", "msg":"Failed to fetch log: server unreachable" }`); cleanupLog(atBottom); } } diff --git a/javascript/logger.js b/javascript/logger.js new file mode 100644 index 000000000..4ff1fb822 --- /dev/null +++ b/javascript/logger.js @@ -0,0 +1,67 @@ +const serverTimeout = 5000; + +const log = async (...msg) => { + const dt = new Date(); + const ts = `${dt.getHours().toString().padStart(2, '0')}:${dt.getMinutes().toString().padStart(2, '0')}:${dt.getSeconds().toString().padStart(2, '0')}.${dt.getMilliseconds().toString().padStart(3, '0')}`; + if (window.logger) window.logger.innerHTML += window.logPrettyPrint(...msg); + console.log(ts, ...msg); // eslint-disable-line no-console +}; + +const debug = async (...msg) => { + const dt = new Date(); + const ts = `${dt.getHours().toString().padStart(2, '0')}:${dt.getMinutes().toString().padStart(2, '0')}:${dt.getSeconds().toString().padStart(2, '0')}.${dt.getMilliseconds().toString().padStart(3, '0')}`; + if (window.logger) window.logger.innerHTML += window.logPrettyPrint(...msg); + console.debug(ts, ...msg); // eslint-disable-line no-console +}; + +const error = async (...msg) => { + const dt = new Date(); + const ts = `${dt.getHours().toString().padStart(2, '0')}:${dt.getMinutes().toString().padStart(2, '0')}:${dt.getSeconds().toString().padStart(2, '0')}.${dt.getMilliseconds().toString().padStart(3, '0')}`; + if (window.logger) window.logger.innerHTML += window.logPrettyPrint(...msg); + console.error(ts, ...msg); // eslint-disable-line no-console + xhrPost('/sdapi/v1/log', { error: msg.join(' ') }); // eslint-disable-line no-use-before-define +}; + +const xhrInternal = (xhrObj, data, handler = undefined, errorHandler = undefined, ignore = false) => { + const err = (msg) => { + if (!ignore) { + error(`${msg}: state=${xhrObj.readyState} status=${xhrObj.status} response=${xhrObj.responseText}`); + if (errorHandler) errorHandler(); + } + }; + + xhrObj.setRequestHeader('Content-Type', 'application/json'); + xhrObj.timeout = serverTimeout; + xhrObj.ontimeout = () => err('xhr.ontimeout'); + xhrObj.onerror = () => err('xhr.onerror'); + xhrObj.onabort = () => err('xhr.onabort'); + xhrObj.onreadystatechange = () => { + if (xhrObj.readyState === 4) { + if (xhrObj.status === 200) { + try { + const json = JSON.parse(xhrObj.responseText); + if (handler) handler(json); + } catch (e) { + error(`xhr.onreadystatechange: ${e}`); + } + } else { + err(`xhr.onreadystatechange: state=${xhrObj.readyState} status=${xhrObj.status} response=${xhrObj.responseText}`); + } + } + }; + const req = JSON.stringify(data); + xhrObj.send(req); +}; + +const xhrGet = (url, data, handler = undefined, errorHandler = undefined, ignore = false) => { + const xhr = new XMLHttpRequest(); + const args = Object.keys(data).map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(data[k])}`).join('&'); + xhr.open('GET', `${url}?${args}`, true); + xhrInternal(xhr, data, handler, errorHandler, ignore); +}; + +function xhrPost(url, data, handler = undefined, errorHandler = undefined, ignore = false) { + const xhr = new XMLHttpRequest(); + xhr.open('POST', url, true); + xhrInternal(xhr, data, handler, errorHandler, ignore); +} diff --git a/javascript/progressBar.js b/javascript/progressBar.js index 3f954e13b..9d897bc87 100644 --- a/javascript/progressBar.js +++ b/javascript/progressBar.js @@ -1,42 +1,5 @@ let lastState = {}; -function request(url, data, handler, errorHandler) { - const xhr = new XMLHttpRequest(); - xhr.open('POST', url, true); - xhr.setRequestHeader('Content-Type', 'application/json'); - xhr.timeout = 5000; - xhr.ontimeout = () => { - console.error('xhr.ontimeout', xhr); - errorHandler(); - }; - xhr.onerror = () => { - console.error('xhr.onerror', xhr); - errorHandler(); - }; - xhr.onabort = () => { - console.error('xhr.onabort', xhr); - errorHandler(); - }; - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - if (xhr.status === 200) { - try { - const json = JSON.parse(xhr.responseText); - handler(json); - } catch (err) { - console.error('xhr.onreadystatechange', xhr, err); - errorHandler(); - } - } else { - console.error('xhr.onreadystatechange', xhr); - errorHandler(); - } - } - }; - const req = JSON.stringify(data); - xhr.send(req); -} - function pad2(x) { return x < 10 ? `0${x}` : x; } @@ -151,11 +114,11 @@ function requestProgress(id_task, progressEl, galleryEl, atEnd = null, onProgres }; const onProgressErrorHandler = (err) => { - console.error('onProgressError', err); + error(`onProgressError: ${err}`); done(); }; - request('./internal/progress', { id_task, id_live_preview }, onProgressHandler, onProgressErrorHandler); + xhrPost('./internal/progress', { id_task, id_live_preview }, onProgressHandler, onProgressErrorHandler); }; start(id_task, 0); } diff --git a/javascript/script.js b/javascript/script.js index 104567dd7..250e90ba2 100644 --- a/javascript/script.js +++ b/javascript/script.js @@ -1,17 +1,3 @@ -const log = (...msg) => { - const dt = new Date(); - const ts = `${dt.getHours().toString().padStart(2, '0')}:${dt.getMinutes().toString().padStart(2, '0')}:${dt.getSeconds().toString().padStart(2, '0')}.${dt.getMilliseconds().toString().padStart(3, '0')}`; - if (window.logger) window.logger.innerHTML += window.logPrettyPrint(...msg); - console.log(ts, ...msg); // eslint-disable-line no-console -}; - -const debug = (...msg) => { - const dt = new Date(); - const ts = `${dt.getHours().toString().padStart(2, '0')}:${dt.getMinutes().toString().padStart(2, '0')}:${dt.getSeconds().toString().padStart(2, '0')}.${dt.getMilliseconds().toString().padStart(3, '0')}`; - if (window.logger) window.logger.innerHTML += window.logPrettyPrint(...msg); - console.debug(ts, ...msg); // eslint-disable-line no-console -}; - async function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); // eslint-disable-line no-promise-executor-return } @@ -82,7 +68,7 @@ function executeCallbacks(queue, arg) { try { callback(arg); } catch (e) { - console.error('error running callback', callback, ':', e); + error(`executeCallbacks: ${callback} ${e}`); } } } diff --git a/javascript/ui.js b/javascript/ui.js index 8808f1c8b..81d1c67e4 100644 --- a/javascript/ui.js +++ b/javascript/ui.js @@ -28,7 +28,7 @@ function clip_gallery_urls(gallery) { const files = gallery.map((v) => v.data); navigator.clipboard.writeText(JSON.stringify(files)).then( () => log('clipboard:', files), - (err) => console.error('clipboard:', files, err), + (err) => error(`clipboard: ${files} ${err}`), ); } @@ -493,9 +493,9 @@ function previewTheme() { el.src = `/file=html/${name}.jpg`; } }) - .catch((e) => console.error('previewTheme:', e)); + .catch((e) => error(`previewTheme: ${e}`)); }) - .catch((e) => console.error('previewTheme:', e)); + .catch((e) => error(`previewTheme: ${e}`)); } async function browseFolder() { diff --git a/modules/api/api.py b/modules/api/api.py index f8346995d..d48cbf521 100644 --- a/modules/api/api.py +++ b/modules/api/api.py @@ -35,7 +35,8 @@ class Api: # server api self.add_api_route("/sdapi/v1/motd", server.get_motd, methods=["GET"], response_model=str) - self.add_api_route("/sdapi/v1/log", server.get_log_buffer, methods=["GET"], response_model=List[str]) + self.add_api_route("/sdapi/v1/log", server.get_log, methods=["GET"], response_model=List[str]) + self.add_api_route("/sdapi/v1/log", server.post_log, methods=["POST"]) self.add_api_route("/sdapi/v1/start", self.get_session_start, methods=["GET"]) self.add_api_route("/sdapi/v1/version", server.get_version, methods=["GET"]) self.add_api_route("/sdapi/v1/status", server.get_status, methods=["GET"], response_model=models.ResStatus) diff --git a/modules/api/models.py b/modules/api/models.py index e68ebf081..39bcbe383 100644 --- a/modules/api/models.py +++ b/modules/api/models.py @@ -286,10 +286,16 @@ class ResImageInfo(BaseModel): items: dict = Field(title="Items", description="A dictionary containing all the other fields the image had") parameters: dict = Field(title="Parameters", description="A dictionary with parsed generation info fields") -class ReqLog(BaseModel): +class ReqGetLog(BaseModel): lines: int = Field(default=100, title="Lines", description="How many lines to return") clear: bool = Field(default=False, title="Clear", description="Should the log be cleared after returning the lines?") + +class ReqPostLog(BaseModel): + message: Optional[str] = Field(title="Message", description="The info message to log") + debug: Optional[str] = Field(title="Debug message", description="The debug message to log") + error: Optional[str] = Field(title="Error message", description="The error message to log") + class ReqProgress(BaseModel): skip_current_image: bool = Field(default=False, title="Skip current image", description="Skip current image serialization") diff --git a/modules/api/server.py b/modules/api/server.py index 939e19c86..dabbe634c 100644 --- a/modules/api/server.py +++ b/modules/api/server.py @@ -37,12 +37,22 @@ def get_platform(): from modules.loader import get_packages as loader_get_packages return { **installer_get_platform(), **loader_get_packages() } -def get_log_buffer(req: models.ReqLog = Depends()): +def get_log(req: models.ReqGetLog = Depends()): lines = shared.log.buffer[:req.lines] if req.lines > 0 else shared.log.buffer.copy() if req.clear: shared.log.buffer.clear() return lines +def post_log(req: models.ReqPostLog): + if req.message is not None: + shared.log.info(f'UI: {req.message}') + if req.debug is not None: + shared.log.debug(f'UI: {req.debug}') + if req.error is not None: + shared.log.error(f'UI: {req.error}') + return {} + + def get_config(): options = {} for k in shared.opts.data.keys():