From a332267bbc3c2b2174581c3806c98f9ecfb549ce 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 | 191 ++++++++++++++++-- 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, 351 insertions(+), 23 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..8c214d52c7 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -5,6 +5,129 @@ 'use strict'; +class Systemd { + constructor() { + this.tty_dom = document.querySelector('#tty'); + console.log('Systemd started'); + } + 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 = 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) => { + 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, func) { + return this.start(id, (async () => { + return func(); + })()); + } + 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); + } +} + // ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので (async () => { window.onerror = (e) => { @@ -16,6 +139,15 @@ 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(); + let forceError = localStorage.getItem('forceError'); if (forceError != null) { renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.'); @@ -37,7 +169,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 +177,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 +195,7 @@ lang = 'en-US'; } - const localRes = await window.fetch(`/assets/locales/${lang}.${v}.json`); + const localRes = await systemd.start(window.fetch(`/assets/locales/${lang}.${v}.json`)); if (localRes.status === 200) { localStorage.setItem('lang', lang); localStorage.setItem('locale', await localRes.text()); @@ -86,10 +218,10 @@ // タイミングによっては、この時点で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 +229,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 +268,26 @@ 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); + }); + } + + if (cmdlineArray.includes('fail')) { + await systemd.start('User Requested Fail', new Promise((_, reject) => { + reject(new Error('Failed by command line')); + } + )); } async function renderError(code, details) { @@ -151,6 +296,8 @@ await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve)); } + systemd.emergency_mode(); + let errorsElement = document.getElementById('errors'); if (!errorsElement) { @@ -160,7 +307,7 @@ -

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

+

You are in emergency mode! Failed to load
読み込みに失敗しました

diff --git a/packages/backend/src/server/web/style.css b/packages/backend/src/server/web/style.css index 81f1838a4a..88559bf8f1 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; +} + +#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-error { + 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