diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js index a04640d9935b..863cf85d1062 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -5,6 +5,131 @@ 'use strict'; +class Systemd { + constructor(version, cmdline) { + this.tty_dom = document.querySelector('#tty'); + const welcome = document.createElement('div'); + welcome.className = 'tty-line'; + welcome.innerText = `misskey-temp ${version} running in Web mode. cmdline: ${cmdline}`; + this.tty_dom.appendChild(welcome); + } + 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 +141,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 +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('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 +216,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 +242,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()); - - // 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; + 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; + } } } } - } + }); } const colorScheme = localStorage.getItem('colorScheme'); if (colorScheme) { @@ -134,181 +281,22 @@ 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) { - // Cannot set property 'innerHTML' of null を回避 - if (document.readyState === 'loading') { - await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve)); - } - - let errorsElement = document.getElementById('errors'); - - if (!errorsElement) { - document.body.innerHTML = ` - -
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に設定する
-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 83784f48cf61..12edc052c379 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 {
+ z-index: 10001;
+ opacity: 1;
+}
+
+#tty > .tty-line {
+ font-family: 'Courier New', Courier, monospace !important;
+ 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: relative;
z-index: 10000;
@@ -42,7 +68,7 @@ html {
left: 0;
margin: auto;
display: inline-block;
- width: 28px;
+ width: 60px;
height: 28px;
transform: translateY(70px);
color: var(--MI_THEME-accent);
@@ -60,6 +86,17 @@ html {
stroke-linejoin: round;
stroke-miterlimit: 1.5;
}
+
+#splashSpinner > .spinner.bg circle {
+ fill:none;
+ stroke:currentColor;
+ stroke-width:24px;
+}
+#splashSpinner > .spinner.fg path {
+ fill:none;
+ stroke:currentColor;
+ stroke-width:24px;
+}
#splashSpinner > .spinner.bg {
opacity: 0.275;
}
diff --git a/packages/backend/src/server/web/style.embed.css b/packages/backend/src/server/web/style.embed.css
index 5e8786cc4ee8..2fb777340621 100644
--- a/packages/backend/src/server/web/style.embed.css
+++ b/packages/backend/src/server/web/style.embed.css
@@ -64,7 +64,7 @@ html.embed.noborder #splash {
left: 0;
margin: auto;
display: inline-block;
- width: 28px;
+ width: 60px;
height: 28px;
transform: translateY(70px);
color: var(--MI_THEME-accent);
@@ -82,6 +82,17 @@ html.embed.noborder #splash {
stroke-linejoin: round;
stroke-miterlimit: 1.5;
}
+
+#splashSpinner > .spinner.bg circle {
+ fill:none;
+ stroke:currentColor;
+ stroke-width:24px;
+}
+#splashSpinner > .spinner.fg path {
+ fill:none;
+ stroke:currentColor;
+ stroke-width:24px;
+}
#splashSpinner > .spinner.bg {
opacity: 0.275;
}
diff --git a/packages/backend/src/server/web/systemd.ts b/packages/backend/src/server/web/systemd.ts
new file mode 100644
index 000000000000..29e45c69d88f
--- /dev/null
+++ b/packages/backend/src/server/web/systemd.ts
@@ -0,0 +1,155 @@
+/*
+ * SPDX-FileCopyrightText: yume/yumechi-no-kun
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class Systemd {
+ private tty_dom: HTMLDivElement;
+ constructor() {
+ this.tty_dom = document.querySelector('#tty') as HTMLDivElement;
+
+ console.log('Systemd started');
+ }
+
+ async start