From bff9f9453c655ca2009e962dae56a240ef2067db Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Sun, 24 Nov 2024 13:51:29 -0600 Subject: [PATCH] test a new initialization Signed-off-by: eternal-flame-AD --- packages/backend/src/server/web/boot.js | 363 +++++++++--------- packages/backend/src/server/web/style.css | 26 ++ packages/backend/src/server/web/systemd.ts | 151 ++++++++ .../src/server/web/views/base-embed.pug | 1 + .../backend/src/server/web/views/base.pug | 5 +- 5 files changed, 362 insertions(+), 184 deletions(-) create mode 100644 packages/backend/src/server/web/systemd.ts diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js index a04640d993..73f7baf666 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -5,6 +5,135 @@ 'use strict'; +class Systemd { + constructor(version, cmdline) { + this.tty_dom = document.querySelector('#tty'); + const welcome = document.createElement('div'); + welcome.className = 'tty-line'; + welcome.innerText = `YumechiNoKuni ${version} running in Web mode. (+mproxy, +metrics, +csp) cmdline: ${cmdline}`; + } + async start(id, promise) { + let state = { state: 'running' }; + let persistentDom = null; + const started = Date.now(); + const formatRunning = () => { + const shiftArray = (arr, n) => { + return arr.slice(n).concat(arr.slice(0, n)); + }; + const elapsed_secs = Math.floor((Date.now() - started) / 1000); + const stars = shiftArray(['*', '*', '*', ' ', ' ', ' '], elapsed_secs % 6); + const spanStatus = document.createElement('span'); + spanStatus.innerText = stars.join(''); + spanStatus.className = 'tty-status-running'; + const spanMessage = document.createElement('span'); + spanMessage.innerText = `A start job is running for ${id} (${elapsed_secs}s / no limit)`; + const div = document.createElement('div'); + div.className = 'tty-line'; + div.innerHTML = '['; + div.appendChild(spanStatus); + div.innerHTML += '] '; + div.appendChild(spanMessage); + return div; + }; + const formatDone = () => { + const elapsed_secs = (Date.now() - started) / 1000; + const spanStatus = document.createElement('span'); + spanStatus.innerText = ' OK '; + spanStatus.className = 'tty-status-ok'; + const spanMessage = document.createElement('span'); + spanMessage.innerText = `Finished ${id} in ${elapsed_secs.toFixed(3)}s`; + const div = document.createElement('div'); + div.className = 'tty-line'; + div.innerHTML = '['; + div.appendChild(spanStatus); + div.innerHTML += '] '; + div.appendChild(spanMessage); + return div; + }; + const formatFailed = (message) => { + const elapsed_secs = (Date.now() - started) / 1000; + const spanStatus = document.createElement('span'); + spanStatus.innerText = 'FAILED'; + spanStatus.className = 'tty-status-failed'; + const spanMessage = document.createElement('span'); + spanMessage.innerText = `Failed ${id} in ${elapsed_secs.toFixed(3)}s: ${message}`; + const div = document.createElement('div'); + div.className = 'tty-line'; + div.innerHTML = '['; + div.appendChild(spanStatus); + div.innerHTML += '] '; + div.appendChild(spanMessage); + return div; + }; + const render = () => { + switch (state.state) { + case 'running': + if (persistentDom === null) { + persistentDom = formatRunning(); + this.tty_dom.appendChild(persistentDom); + } + else { + persistentDom.innerHTML = formatRunning().innerHTML; + } + break; + case 'done': + if (persistentDom === null) { + persistentDom = formatDone(); + this.tty_dom.appendChild(persistentDom); + } + else { + persistentDom.innerHTML = formatDone().innerHTML; + } + break; + case 'failed': + if (persistentDom === null) { + persistentDom = formatFailed(state.message); + this.tty_dom.appendChild(persistentDom); + } + else { + persistentDom.innerHTML = formatFailed(state.message).innerHTML; + } + break; + } + }; + render(); + const interval = setInterval(render, 500); + try { + let res = await promise; + state = { state: 'done' }; + return res; + } + catch (e) { + if (e instanceof Error) { + state = { state: 'failed', message: e.message }; + } + else { + state = { state: 'failed', message: 'Unknown error' }; + } + throw e; + } + finally { + clearInterval(interval); + render(); + } + } + async startSync(id, func) { + return this.start(id, (async () => { + return func(); + })()); + } + emergency_mode(code, details) {`` + const divPrev = document.createElement('div'); + divPrev.className = 'tty-line'; + divPrev.innerText = 'Critical error occurred [' + code + '] : ' + details.message ? details.message : details; + this.tty_dom.appendChild(divPrev); + const div = document.createElement('div'); + div.className = 'tty-line'; + div.innerText = 'You are in emergency mode. Type Ctrl-Shift-I to view system logs. Clearing local storage by going to /flush and browser settings may help.'; + this.tty_dom.appendChild(div); + } +} + // ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので (async () => { window.onerror = (e) => { @@ -16,10 +145,24 @@ renderError('SOMETHING_HAPPENED_IN_PROMISE', e); }; + const cmdline = new URLSearchParams(location.search).get('cmdline') || ''; + const cmdlineArray = cmdline.split(',').map(x => x.trim()); + if (cmdlineArray.includes('nosplash')) { + document.querySelector('#splashIcon').classList.add('hidden'); + document.querySelector('#splashSpinner').classList.add('hidden'); + } + + const systemd = new Systemd(VERSION, cmdline); + + if (cmdlineArray.includes('leak')) { + await systemd.start('Promise Leak Service', new Promise(() => { })); + } + let forceError = localStorage.getItem('forceError'); if (forceError != null) { - renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.'); - return; + await systemd.startSync('Force Error Service', () => { + throw new Error('This error is forced by having forceError in local storage.'); + }); } //#region Detect language & fetch translations @@ -37,7 +180,7 @@ } } - const metaRes = await window.fetch('/api/meta', { + const metaRes = await systemd.start('Fetch /api/meta',window.fetch('/api/meta', { method: 'POST', body: JSON.stringify({}), credentials: 'omit', @@ -45,12 +188,12 @@ headers: { 'Content-Type': 'application/json', }, - }); + })); if (metaRes.status !== 200) { renderError('META_FETCH'); return; } - const meta = await metaRes.json(); + const meta = await systemd.start('Parse /api/meta', metaRes.json()); const v = meta.version; if (v == null) { renderError('META_FETCH_V'); @@ -63,7 +206,7 @@ lang = 'en-US'; } - const localRes = await window.fetch(`/assets/locales/${lang}.${v}.json`); + const localRes = await systemd.start('Fetch Locale files', window.fetch(`/assets/locales/${lang}.${v}.json`)); if (localRes.status === 200) { localStorage.setItem('lang', lang); localStorage.setItem('locale', await localRes.text()); @@ -77,19 +220,25 @@ //#region Script async function importAppScript() { - await import(`/vite/${CLIENT_ENTRY}`) + await systemd.start('Load App Script', import(`/vite/${CLIENT_ENTRY}`)) .catch(async e => { console.error(e); renderError('APP_IMPORT', e); }); } + if (cmdlineArray.includes('fail')) { + await systemd.startSync('Force Error Service', () => { + throw new Error('This error is forced by having fail in command line.'); + }); + } + // タイミングによっては、この時点でDOMの構築が済んでいる場合とそうでない場合とがある if (document.readyState !== 'loading') { - importAppScript(); + systemd.start('import App Script', importAppScript()); } else { window.addEventListener('DOMContentLoaded', () => { - importAppScript(); + systemd.start('import App Script', importAppScript()); }); } //#endregion @@ -97,19 +246,21 @@ //#region Theme const theme = localStorage.getItem('theme'); if (theme) { - for (const [k, v] of Object.entries(JSON.parse(theme))) { - document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString()); + await systemd.startSync('Apply theme', () => { + for (const [k, v] of Object.entries(JSON.parse(theme))) { + document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString()); - // HTMLの theme-color 適用 - if (k === 'htmlThemeColor') { - for (const tag of document.head.children) { - if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') { - tag.setAttribute('content', v); - break; + // HTMLの theme-color 適用 + if (k === 'htmlThemeColor') { + for (const tag of document.head.children) { + if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') { + tag.setAttribute('content', v); + break; + } } } } - } + }); } const colorScheme = localStorage.getItem('colorScheme'); if (colorScheme) { @@ -134,15 +285,19 @@ const customCss = localStorage.getItem('customCss'); if (customCss && customCss.length > 0) { - const style = document.createElement('style'); - style.innerHTML = customCss; - document.head.appendChild(style); + await systemd.startSync('Apply custom CSS', () => { + const style = document.createElement('style'); + style.innerHTML = customCss; + document.head.appendChild(style); + }); } async function addStyle(styleText) { - let css = document.createElement('style'); - css.appendChild(document.createTextNode(styleText)); - document.head.appendChild(css); + await systemd.startSync('Apply custom Style', () => { + let css = document.createElement('style'); + css.appendChild(document.createTextNode(styleText)); + document.head.appendChild(css); + }); } async function renderError(code, details) { @@ -151,164 +306,6 @@ await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve)); } - let errorsElement = document.getElementById('errors'); - - if (!errorsElement) { - document.body.innerHTML = ` - - - - - -

Failed to load
読み込みに失敗しました

- -

The following actions may solve the problem. / 以下を行うと解決する可能性があります。

-

Update your os and browser / ブラウザおよびOSを最新バージョンに更新する

-

Disable an adblocker / アドブロッカーを無効にする

-

Clear the browser cache / ブラウザのキャッシュをクリアする

-

(Tor Browser) Set dom.webaudio.enabled to true / dom.webaudio.enabledをtrueに設定する

-
- Other options / その他のオプション - - - -
- - - -
- - - -
-
-
- `; - errorsElement = document.getElementById('errors'); - } - const detailsElement = document.createElement('details'); - detailsElement.id = 'errorInfo'; - detailsElement.innerHTML = ` -
- - ERROR CODE: ${code} - - ${details.toString()} ${JSON.stringify(details)}`; - errorsElement.appendChild(detailsElement); - addStyle(` - * { - font-family: BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif; - } - - #misskey_app, - #splash { - display: none !important; - } - - body, - html { - background-color: #222; - color: #dfddcc; - justify-content: center; - margin: auto; - padding: 10px; - text-align: center; - } - - button { - border-radius: 999px; - padding: 0px 12px 0px 12px; - border: none; - cursor: pointer; - margin-bottom: 12px; - } - - .button-big { - background: linear-gradient(90deg, rgb(134, 179, 0), rgb(74, 179, 0)); - line-height: 50px; - } - - .button-big:hover { - background: rgb(153, 204, 0); - } - - .button-small { - background: #444; - line-height: 40px; - } - - .button-small:hover { - background: #555; - } - - .button-label-big { - color: #222; - font-weight: bold; - font-size: 1.2em; - padding: 12px; - } - - .button-label-small { - color: rgb(153, 204, 0); - font-size: 16px; - padding: 12px; - } - - a { - color: rgb(134, 179, 0); - text-decoration: none; - } - - p, - li { - font-size: 16px; - } - - .icon-warning { - color: #dec340; - height: 4rem; - padding-top: 2rem; - } - - h1 { - font-size: 1.5em; - margin: 1em; - } - - code { - font-family: Fira, FiraCode, monospace; - } - - #errorInfo { - background: #333; - margin-bottom: 2rem; - padding: 0.5rem 1rem; - width: 40rem; - border-radius: 10px; - justify-content: center; - margin: auto; - } - - #errorInfo summary { - cursor: pointer; - } - - #errorInfo summary > * { - display: inline; - } - - @media screen and (max-width: 500px) { - #errorInfo { - width: 50%; - } - }`); + systemd.emergency_mode(code, details); } })(); diff --git a/packages/backend/src/server/web/style.css b/packages/backend/src/server/web/style.css index 81f1838a4a..923de63225 100644 --- a/packages/backend/src/server/web/style.css +++ b/packages/backend/src/server/web/style.css @@ -9,6 +9,32 @@ html { color: var(--MI_THEME-fg); } +.hidden { + display: none !important; +} + +#tty { + font-family: monospace; + z-index: 10001; + opacity: 1; +} + +#tty > .tty-line { + display: block; +} + +#tty > .tty-line .tty-status-ok { + color: green; +} + +#tty > .tty-line .tty-status-failed { + color: darkred; +} + +#tty > .tty-line .tty-status-running { + color: red; +} + #splash { position: fixed; z-index: 10000; diff --git a/packages/backend/src/server/web/systemd.ts b/packages/backend/src/server/web/systemd.ts new file mode 100644 index 0000000000..d5cb01097d --- /dev/null +++ b/packages/backend/src/server/web/systemd.ts @@ -0,0 +1,151 @@ +export class Systemd { + private tty_dom: HTMLDivElement; + constructor() { + this.tty_dom = document.querySelector('#tty') as HTMLDivElement; + + console.log('Systemd started'); + } + + async start(id: string, promise: Promise): Promise { + + let state: { + state: 'running' + } | { + state: 'done' + } | { + state: 'failed' + message: string + } = { state: 'running' }; + + let persistentDom : HTMLDivElement | null = null; + + const started = Date.now(); + + const formatRunning = () => { + const shiftArray = (arr: T[], n: number): T[] => { + return arr.slice(n).concat(arr.slice(0, n)); + }; + + const elapsed_secs = Math.floor((Date.now() - started) / 1000); + const stars = shiftArray(['*', '*', '*', ' ', ' ', ' '], elapsed_secs % 6); + + const spanStatus = document.createElement('span'); + + spanStatus.innerText = stars.join(''); + spanStatus.className = 'tty-status-running'; + + const spanMessage = document.createElement('span'); + spanMessage.innerText = `A start job is running for ${id} (${elapsed_secs}s / no limit)`; + + const div = document.createElement('div'); + div.className = 'tty-line'; + div.innerHTML = '['; + div.appendChild(spanStatus); + div.innerHTML += '] '; + div.appendChild(spanMessage); + + return div; + } + + const formatDone = () => { + const elapsed_secs = Math.floor((Date.now() - started) / 1000); + + const spanStatus = document.createElement('span'); + spanStatus.innerText = ' OK '; + spanStatus.className = 'tty-status-ok'; + + const spanMessage = document.createElement('span'); + spanMessage.innerText = `Finished ${id} in ${elapsed_secs}s`; + + const div = document.createElement('div'); + div.className = 'tty-line'; + div.innerHTML = '['; + div.appendChild(spanStatus); + div.innerHTML += '] '; + div.appendChild(spanMessage); + + return div; + } + + const formatFailed = (message: string) => { + const elapsed_secs = Math.floor((Date.now() - started) / 1000); + + const spanStatus = document.createElement('span'); + spanStatus.innerText = 'FAILED'; + spanStatus.className = 'tty-status-failed'; + + const spanMessage = document.createElement('span'); + spanMessage.innerText = `Failed ${id} in ${elapsed_secs}s: ${message}`; + + const div = document.createElement('div'); + div.className = 'tty-line'; + div.innerHTML = '['; + div.appendChild(spanStatus); + div.innerHTML += '] '; + div.appendChild(spanMessage); + + return div; + } + + const render = () => { + switch (state.state) { + case 'running': + if (persistentDom === null) { + persistentDom = formatRunning(); + this.tty_dom.appendChild(persistentDom); + } else { + persistentDom.innerHTML = formatRunning().innerHTML; + } + break; + case 'done': + if (persistentDom === null) { + persistentDom = formatDone(); + this.tty_dom.appendChild(persistentDom); + } else { + persistentDom.innerHTML = formatDone().innerHTML; + } + break; + case 'failed': + if (persistentDom === null) { + persistentDom = formatFailed(state.message); + this.tty_dom.appendChild(persistentDom); + } else { + persistentDom.innerHTML = formatFailed(state.message).innerHTML; + } + break; + } + }; + + render(); + const interval = setInterval(render, 500); + + try { + let res = await promise; + state = { state: 'done' }; + return res; + } catch (e) { + if (e instanceof Error) { + state = { state: 'failed', message: e.message }; + } else { + state = { state: 'failed', message: 'Unknown error' }; + } + throw e; + } finally { + clearInterval(interval); + render(); + } + } + + async startSync(id: string, func: () => T): Promise { + return this.start(id, (async () => { + return func(); + })()); + } + + public emergency_mode() { + const div = document.createElement('div'); + div.className = 'tty-line'; + div.innerText = 'You are in emergency mode. Type Ctrl-Shift-I to view logs.'; + this.tty_dom.appendChild(div); + } +} \ No newline at end of file diff --git a/packages/backend/src/server/web/views/base-embed.pug b/packages/backend/src/server/web/views/base-embed.pug index bc1352ace4..c434f33747 100644 --- a/packages/backend/src/server/web/views/base-embed.pug +++ b/packages/backend/src/server/web/views/base-embed.pug @@ -56,6 +56,7 @@ html(class='embed') br | Please turn on your JavaScript div#splash + div#tty img#splashIcon(src= icon || '/static-assets/splash.png') div#splashSpinner Loading... diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug index 8fbfdacabd..405d050d90 100644 --- a/packages/backend/src/server/web/views/base.pug +++ b/packages/backend/src/server/web/views/base.pug @@ -65,7 +65,6 @@ html script(type='application/json' id='misskey_clientCtx' data-generated-at=now) != clientCtx - script(integrity=bootJS.integrity) !{bootJS.content} body noscript: p @@ -73,7 +72,11 @@ html br | Please turn on your JavaScript div#splash + div#tty img#splashIcon(src= icon || '/static-assets/splash.png') div#splashSpinner Loading... + + script(integrity=bootJS.integrity) !{bootJS.content} + block content