diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js index a04640d993..b8ed48427f 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -5,6 +5,136 @@ 'use strict'; +class TaskDom { + constructor(tty_dom, id, promise) { + this.tty_dom = tty_dom; + this.id = id; + this.promise = promise; + this.started = Date.now(); + this.state = { state: 'running' }; + this.render(); + this.interval = setInterval(() => { + this.render(); + }, 500); + promise.then(() => { + this.state = { state: 'done' }; + clearInterval(this.interval); + this.render(); + }).catch((e) => { + this.state = { state: 'failed', message: e.message.toString() }; + clearInterval(this.interval); + this.render(); + }); + } + render() { + switch (this.state.state) { + case 'running': + if (this.persistentDom === null) { + this.persistentDom = this.formatRunning(); + this.tty_dom.appendChild(this.persistentDom); + } + else { + this.persistentDom.innerHTML = this.formatRunning().innerHTML; + } + break; + case 'done': + if (this.persistentDom === null) { + this.persistentDom = this.formatDone(); + this.tty_dom.appendChild(this.persistentDom); + } + else { + this.persistentDom.innerHTML = this.formatDone().innerHTML; + } + break; + case 'failed': + if (this.persistentDom === null) { + this.persistentDom = this.formatFailed(this.state.message); + this.tty_dom.appendChild(this.persistentDom); + } + else { + this.persistentDom.innerHTML = this.formatFailed(this.state.message).innerHTML; + } + break; + } + } + formatRunning() { + const shiftArray = (arr, n) => { + return arr.slice(n).concat(arr.slice(0, n)); + }; + const elapsed_secs = Math.floor((Date.now() - this.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 ${this.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; + } + formatDone() { + const elapsed_secs = Math.floor((Date.now() - this.started) / 1000); + const spanStatus = document.createElement('span'); + spanStatus.innerText = ' OK '; + spanStatus.className = 'tty-status-ok'; + const spanMessage = document.createElement('span'); + spanMessage.innerText = `Finished ${this.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; + } + formatFailed(message) { + const elapsed_secs = Math.floor((Date.now() - this.started) / 1000); + const spanStatus = document.createElement('span'); + spanStatus.innerText = 'FAILED'; + spanStatus.className = 'tty-status-failed'; + const spanMessage = document.createElement('span'); + spanMessage.innerText = `Failed ${this.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; + } +} +class Systemd { + constructor() { + this.tty_dom = document.querySelector('#tty'); + console.log('Systemd started'); + } + async start(id, promise) { + const task = new TaskDom(this.tty_dom, id, promise); + await task.promise; + } + async startSync(id, func) { + const task = new TaskDom(this.tty_dom, id, new Promise((resolve, reject) => { + try { + resolve(func()); + } + catch (e) { + reject(e); + } + })); + return await task.promise; + } + 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 +146,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 +176,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 +184,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 +202,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 +225,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 +236,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 +275,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 +303,8 @@ await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve)); } + systemd.emergency_mode(); + let errorsElement = document.getElementById('errors'); if (!errorsElement) { @@ -160,7 +314,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..80f5950273 --- /dev/null +++ b/packages/backend/src/server/web/systemd.ts @@ -0,0 +1,161 @@ +class TaskDom{ + private started = Date.now(); + private state: { + state: 'running' + } | { + state: 'done' + } | { + state: 'failed' + message: string + } = { state: 'running' }; + + private interval = setInterval(() => { + this.render(); + }, 500); + + private persistentDom : HTMLDivElement | null = null; + + constructor( + private tty_dom: HTMLDivElement, + private id: string, + public promise: Promise) { + + promise.then(() => { + this.state = { state: 'done' }; + clearInterval(this.interval); + this.render(); + }).catch((e) => { + this.state = { state: 'failed', message: e.message.toString() }; + clearInterval(this.interval); + this.render(); + }); + } + + private render() { + switch (this.state.state) { + case 'running': + if (this.persistentDom === null) { + this.persistentDom = this.formatRunning(); + this.tty_dom.appendChild(this.persistentDom); + } else { + this.persistentDom.innerHTML = this.formatRunning().innerHTML; + } + break; + case 'done': + if (this.persistentDom === null) { + this.persistentDom = this.formatDone(); + this.tty_dom.appendChild(this.persistentDom); + } else { + this.persistentDom.innerHTML = this.formatDone().innerHTML; + } + break; + case 'failed': + if (this.persistentDom === null) { + this.persistentDom = this.formatFailed(this.state.message); + this.tty_dom.appendChild(this.persistentDom); + } else { + this.persistentDom.innerHTML = this.formatFailed(this.state.message).innerHTML; + } + break; + } + } + + private formatRunning(): HTMLDivElement { + const shiftArray = (arr: T[], n: number): T[] => { + return arr.slice(n).concat(arr.slice(0, n)); + }; + + const elapsed_secs = Math.floor((Date.now() - this.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 ${this.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; + } + + private formatDone(): HTMLDivElement { + const elapsed_secs = Math.floor((Date.now() - this.started) / 1000); + + const spanStatus = document.createElement('span'); + spanStatus.innerText = ' OK '; + spanStatus.className = 'tty-status-ok'; + + const spanMessage = document.createElement('span'); + spanMessage.innerText = `Finished ${this.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; + } + + private formatFailed(message: string): HTMLDivElement { + const elapsed_secs = Math.floor((Date.now() - this.started) / 1000); + + const spanStatus = document.createElement('span'); + spanStatus.innerText = 'FAILED'; + spanStatus.className = 'tty-status-failed'; + + const spanMessage = document.createElement('span'); + spanMessage.innerText = `Failed ${this.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; + } +} + +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 { + const task = new TaskDom(this.tty_dom, id, promise); + await task.promise; + } + + async startSync(id: string, func: () => T): Promise { + const task = new TaskDom(this.tty_dom, id, new Promise((resolve, reject) => { + try { + resolve(func()); + } catch (e) { + reject(e); + } + })); + + return await task.promise; + } + + 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