Move server-main to /src

01c6544e22a3584c78bf9aa72bcee415bb2d910e

Cohee <18619528+Cohee1207@users.noreply.github.com>

2 files changed, +388 -380Showing whitespace changes
server.js+2 -380
@@ -1,393 +1,15 @@
11#!/usr/bin/env node
2-
3-// native node modules
4-import path from 'node:path';
5-import util from 'node:util';
6-import net from 'node:net';
7-import dns from 'node:dns';
8-import process from 'node:process';
9-
10-import cors from 'cors';
11-import { csrfSync } from 'csrf-sync';
12-import express from 'express';
13-import compression from 'compression';
14-import cookieSession from 'cookie-session';
15-import multer from 'multer';
16-import responseTime from 'response-time';
17-import helmet from 'helmet';
18-import bodyParser from 'body-parser';
19-import open from 'open';
20-
21-// local library imports
22-import './src/fetch-patch.js';
232import { CommandLineParser } from './src/command-line.js';
243import { serverDirectory } from './src/server-directory.js';
254
26-console.log(`Node version: ${process.version}. Running in ${process.env.NODE_ENV} environment. Server directory: ${serverDirectory}`);
27-
28-// Work around a node v20.0.0, v20.1.0, and v20.2.0 bug. The issue was fixed in v20.3.0.
29-// https://github.com/nodejs/node/issues/47822#issuecomment-1564708870
30-// Safe to remove once support for Node v20 is dropped.
31-if (process.versions && process.versions.node && process.versions.node.match(/20\.[0-2]\.0/)) {
32- // @ts-ignore
33- if (net.setDefaultAutoSelectFamily) net.setDefaultAutoSelectFamily(false);
34-}
35-
365// config.yaml will be set when parsing command line arguments
376const cliArgs = new CommandLineParser().parse(process.argv);
387globalThis.DATA_ROOT = cliArgs.dataRoot;
398globalThis.COMMAND_LINE_ARGS = cliArgs;
409process.chdir(serverDirectory);
4110
42-const { serverEvents, EVENT_NAMES } = await import('./src/server-events.js');
43-const { loadPlugins } = await import('./src/plugin-loader.js');
44-const {
45- initUserStorage,
46- getCookieSecret,
47- getCookieSessionName,
48- ensurePublicDirectoriesExist,
49- getUserDirectoriesList,
50- migrateSystemPrompts,
51- migrateUserData,
52- requireLoginMiddleware,
53- setUserDataMiddleware,
54- shouldRedirectToLogin,
55- cleanUploads,
56- getSessionCookieAge,
57- verifySecuritySettings,
58- loginPageMiddleware,
59-} = await import('./src/users.js');
60-
61-const { default: getWebpackServeMiddleware } = await import('./src/middleware/webpack-serve.js');
62-const { default: basicAuthMiddleware } = await import('./src/middleware/basicAuth.js');
63-const { default: getWhitelistMiddleware } = await import('./src/middleware/whitelist.js');
64-const { default: accessLoggerMiddleware, getAccessLogPath, migrateAccessLog } = await import('./src/middleware/accessLogWriter.js');
65-const { default: multerMonkeyPatch } = await import('./src/middleware/multerMonkeyPatch.js');
66-const { default: initRequestProxy } = await import('./src/request-proxy.js');
67-const { default: getCacheBusterMiddleware } = await import('./src/middleware/cacheBuster.js');
68-const { default: corsProxyMiddleware } = await import('./src/middleware/corsProxy.js');
69-const {
70- getVersion,
71- color,
72- removeColorFormatting,
73- getSeparator,
74- safeReadFileSync,
75- setupLogLevel,
76- setWindowTitle,
77-} = await import('./src/util.js');
78-const { UPLOADS_DIRECTORY } = await import('./src/constants.js');
79-const { ensureThumbnailCache } = await import('./src/endpoints/thumbnails.js');
80-
81-// Routers
82-const { router : usersPublicRouter } = await import('./src/endpoints/users-public.js');
83-const { init : statsInit, onExit : statsOnExit } = await import('./src/endpoints/stats.js');
84-const { checkForNewContent } = await import('./src/endpoints/content-manager.js');
85-const { init : settingsInit } = await import('./src/endpoints/settings.js');
86-const { redirectDeprecatedEndpoints, ServerStartup, setupPrivateEndpoints } = await import('./src/server-startup.js');
87-const { diskCache } = await import('./src/endpoints/characters.js');
88-
89-// Unrestrict console logs display limit
90-util.inspect.defaultOptions.maxArrayLength = null;
91-util.inspect.defaultOptions.maxStringLength = null;
92-util.inspect.defaultOptions.depth = 4;
93-
94-if (!cliArgs.enableIPv6 && !cliArgs.enableIPv4) {
95- console.error('error: You can\'t disable all internet protocols: at least IPv6 or IPv4 must be enabled.');
96- process.exit(1);
97-}
98-
99-try {
100- if (cliArgs.dnsPreferIPv6) {
101- dns.setDefaultResultOrder('ipv6first');
102- console.log('Preferring IPv6 for DNS resolution');
103- } else {
104- dns.setDefaultResultOrder('ipv4first');
105- console.log('Preferring IPv4 for DNS resolution');
106- }
107-} catch (error) {
108- console.warn('Failed to set DNS resolution order. Possibly unsupported in this Node version.');
109-}
110-
111-const app = express();
112-app.use(helmet({
113- contentSecurityPolicy: false,
114-}));
115-app.use(compression());
116-app.use(responseTime());
117-
118-app.use(bodyParser.json({ limit: '200mb' }));
119-app.use(bodyParser.urlencoded({ extended: true, limit: '200mb' }));
120-
121-// CORS Settings //
122-const CORS = cors({
123- origin: 'null',
124- methods: ['OPTIONS'],
125-});
126-
127-app.use(CORS);
128-
129-if (cliArgs.listen && cliArgs.basicAuthMode) {
130- app.use(basicAuthMiddleware);
131-}
132-
133-if (cliArgs.whitelistMode) {
134- const whitelistMiddleware = await getWhitelistMiddleware();
135- app.use(whitelistMiddleware);
136-}
137-
138-if (cliArgs.listen) {
139- app.use(accessLoggerMiddleware());
140-}
141-
142-if (cliArgs.enableCorsProxy) {
143- app.use('/proxy/:url(*)', corsProxyMiddleware);
144-} else {
145- app.use('/proxy/:url(*)', async (_, res) => {
146- const message = 'CORS proxy is disabled. Enable it in config.yaml or use the --corsProxy flag.';
147- console.log(message);
148- res.status(404).send(message);
149- });
150-}
151-
152-app.use(cookieSession({
153- name: getCookieSessionName(),
154- sameSite: 'lax',
155- httpOnly: true,
156- maxAge: getSessionCookieAge(),
157- secret: getCookieSecret(globalThis.DATA_ROOT),
158-}));
159-
160-app.use(setUserDataMiddleware);
161-
162-// CSRF Protection //
163-if (!cliArgs.disableCsrf) {
164- const csrfSyncProtection = csrfSync({
165- getTokenFromState: (req) => {
166- if (!req.session) {
167- console.error('(CSRF error) getTokenFromState: Session object not initialized');
168- return;
169- }
170- return req.session.csrfToken;
171- },
172- getTokenFromRequest: (req) => {
173- return req.headers['x-csrf-token']?.toString();
174- },
175- storeTokenInState: (req, token) => {
176- if (!req.session) {
177- console.error('(CSRF error) storeTokenInState: Session object not initialized');
178- return;
179- }
180- req.session.csrfToken = token;
181- },
182- size: 32,
183- });
184-
185- app.get('/csrf-token', (req, res) => {
186- res.json({
187- 'token': csrfSyncProtection.generateToken(req),
188- });
189- });
190-
191- // Customize the error message
192- csrfSyncProtection.invalidCsrfTokenError.message = color.red('Invalid CSRF token. Please refresh the page and try again.');
193- csrfSyncProtection.invalidCsrfTokenError.stack = undefined;
194-
195- app.use(csrfSyncProtection.csrfSynchronisedProtection);
196-} else {
197- console.warn('\nCSRF protection is disabled. This will make your server vulnerable to CSRF attacks.\n');
198- app.get('/csrf-token', (req, res) => {
199- res.json({
200- 'token': 'disabled',
201- });
202- });
203-}
204-
205-// Static files
206-// Host index page
207-app.get('/', getCacheBusterMiddleware(), (request, response) => {
208- if (shouldRedirectToLogin(request)) {
209- const query = request.url.split('?')[1];
210- const redirectUrl = query ? `/login?${query}` : '/login';
211- return response.redirect(redirectUrl);
212- }
213-
214- return response.sendFile('index.html', { root: path.join(serverDirectory, 'public') });
215-});
216-
217-// Callback endpoint for OAuth PKCE flows (e.g. OpenRouter)
218-app.get('/callback/:source?', (request, response) => {
219- const source = request.params.source;
220- const query = request.url.split('?')[1];
221- const searchParams = new URLSearchParams();
222- source && searchParams.set('source', source);
223- query && searchParams.set('query', query);
224- const path = `/?${searchParams.toString()}`;
225- return response.redirect(307, path);
226-});
227-
228-// Host login page
229-app.get('/login', loginPageMiddleware);
230-
231-// Host frontend assets
232-const webpackMiddleware = getWebpackServeMiddleware();
233-app.use(webpackMiddleware);
234-app.use(express.static(path.join(serverDirectory, 'public'), {}));
235-
236-// Public API
237-app.use('/api/users', usersPublicRouter);
238-
239-// Everything below this line requires authentication
240-app.use(requireLoginMiddleware);
241-app.get('/api/ping', (request, response) => {
242- if (request.query.extend && request.session) {
243- request.session.touch = Date.now();
244- }
245-
246- response.sendStatus(204);
247-});
248-
249-// File uploads
250-const uploadsPath = path.join(cliArgs.dataRoot, UPLOADS_DIRECTORY);
251-app.use(multer({ dest: uploadsPath, limits: { fieldSize: 10 * 1024 * 1024 } }).single('avatar'));
252-app.use(multerMonkeyPatch);
253-
254-app.get('/version', async function (_, response) {
255- const data = await getVersion();
256- response.send(data);
257-});
258-
259-redirectDeprecatedEndpoints(app);
260-setupPrivateEndpoints(app);
261-
262-/**
263- * Tasks that need to be run before the server starts listening.
264- * @returns {Promise<void>}
265- */
266-async function preSetupTasks() {
267- const version = await getVersion();
268-
269- // Print formatted header
270- console.log();
271- console.log(`SillyTavern ${version.pkgVersion}`);
272- if (version.gitBranch) {
273- console.log(`Running '${version.gitBranch}' (${version.gitRevision}) - ${version.commitDate}`);
274- if (!version.isLatest && ['staging', 'release'].includes(version.gitBranch)) {
275- console.log('INFO: Currently not on the latest commit.');
276- console.log(' Run \'git pull\' to update. If you have any merge conflicts, run \'git reset --hard\' and \'git pull\' to reset your branch.');
277- }
278- }
279- console.log();
280-
281- const directories = await getUserDirectoriesList();
282- await checkForNewContent(directories);
283- await ensureThumbnailCache(directories);
284- await diskCache.verify(directories);
285- cleanUploads();
286- migrateAccessLog();
287-
288- await settingsInit();
289- await statsInit();
290-
291- const pluginsDirectory = path.join(serverDirectory, 'plugins');
292- const cleanupPlugins = await loadPlugins(app, pluginsDirectory);
293- const consoleTitle = process.title;
294-
295- let isExiting = false;
296- const exitProcess = async () => {
297- if (isExiting) return;
298- isExiting = true;
299- await statsOnExit();
300- if (typeof cleanupPlugins === 'function') {
301- await cleanupPlugins();
302- }
303- diskCache.dispose();
304- setWindowTitle(consoleTitle);
305- process.exit();
306- };
307-
308- // Set up event listeners for a graceful shutdown
309- process.on('SIGINT', exitProcess);
310- process.on('SIGTERM', exitProcess);
311- process.on('uncaughtException', (err) => {
312- console.error('Uncaught exception:', err);
313- exitProcess();
314- });
315-
316- // Add request proxy.
317- initRequestProxy({ enabled: cliArgs.requestProxyEnabled, url: cliArgs.requestProxyUrl, bypass: cliArgs.requestProxyBypass });
318-
319- // Wait for frontend libs to compile
320- await webpackMiddleware.runWebpackCompiler();
321-}
322-
323-/**
324- * Tasks that need to be run after the server starts listening.
325- * @param {import('./src/server-startup.js').ServerStartupResult} result The result of the server startup
326- * @returns {Promise<void>}
327- */
328-async function postSetupTasks(result) {
329- const autorunHostname = await cliArgs.getAutorunHostname(result);
330- const autorunUrl = cliArgs.getAutorunUrl(autorunHostname);
331-
332- if (cliArgs.autorun) {
33311try {
334- console.log('Launching in a browser...');
12+ await import('./src/server-main.js');
335- await open(autorunUrl.toString());
33613} catch (error) {
33714 console.error('FailedA tocritical launcherror thehas browser.occurred Openwhile thestarting URLthe manually.server:', error);
338- }
339- }
340-
341- setWindowTitle('SillyTavern WebServer');
342-
343- let logListen = 'SillyTavern is listening on';
344-
345- if (result.useIPv6 && !result.v6Failed) {
346- logListen += color.green(
347- ' IPv6: ' + cliArgs.getIPv6ListenUrl().host,
348- );
34915}
350-
351- if (result.useIPv4 && !result.v4Failed) {
352- logListen += color.green(
353- ' IPv4: ' + cliArgs.getIPv4ListenUrl().host,
354- );
355- }
356-
357- const goToLog = 'Go to: ' + color.blue(autorunUrl) + ' to open SillyTavern';
358- const plainGoToLog = removeColorFormatting(goToLog);
359-
360- console.log(logListen);
361- if (cliArgs.listen) {
362- console.log();
363- console.log('To limit connections to internal localhost only ([::1] or 127.0.0.1), change the setting in config.yaml to "listen: false".');
364- console.log('Check the "access.log" file in the data directory to inspect incoming connections:', color.green(getAccessLogPath()));
365- }
366- console.log('\n' + getSeparator(plainGoToLog.length) + '\n');
367- console.log(goToLog);
368- console.log('\n' + getSeparator(plainGoToLog.length) + '\n');
369-
370- setupLogLevel();
371- serverEvents.emit(EVENT_NAMES.SERVER_STARTED, { url: autorunUrl });
372-}
373-
374-/**
375- * Registers a not-found error response if a not-found error page exists. Should only be called after all other middlewares have been registered.
376- */
377-function apply404Middleware() {
378- const notFoundWebpage = safeReadFileSync(path.join(serverDirectory, 'public/error/url-not-found.html')) ?? '';
379- app.use((req, res) => {
380- res.status(404).send(notFoundWebpage);
381- });
382-}
383-
384-// User storage module needs to be initialized before starting the server
385-initUserStorage(globalThis.DATA_ROOT)
386- .then(ensurePublicDirectoriesExist)
387- .then(migrateUserData)
388- .then(migrateSystemPrompts)
389- .then(verifySecuritySettings)
390- .then(preSetupTasks)
391- .then(apply404Middleware)
392- .then(() => new ServerStartup(app, cliArgs).start())
393- .then(postSetupTasks);
src/server-main.js+386 -0
@@ -0,0 +1,386 @@
1+// native node modules
2+import path from 'node:path';
3+import util from 'node:util';
4+import net from 'node:net';
5+import dns from 'node:dns';
6+import process from 'node:process';
7+
8+import cors from 'cors';
9+import { csrfSync } from 'csrf-sync';
10+import express from 'express';
11+import compression from 'compression';
12+import cookieSession from 'cookie-session';
13+import multer from 'multer';
14+import responseTime from 'response-time';
15+import helmet from 'helmet';
16+import bodyParser from 'body-parser';
17+import open from 'open';
18+
19+// local library imports
20+import './fetch-patch.js';
21+import { serverDirectory } from './server-directory.js';
22+
23+console.log(`Node version: ${process.version}. Running in ${process.env.NODE_ENV} environment. Server directory: ${serverDirectory}`);
24+
25+// Work around a node v20.0.0, v20.1.0, and v20.2.0 bug. The issue was fixed in v20.3.0.
26+// https://github.com/nodejs/node/issues/47822#issuecomment-1564708870
27+// Safe to remove once support for Node v20 is dropped.
28+if (process.versions && process.versions.node && process.versions.node.match(/20\.[0-2]\.0/)) {
29+ // @ts-ignore
30+ if (net.setDefaultAutoSelectFamily) net.setDefaultAutoSelectFamily(false);
31+}
32+
33+import { serverEvents, EVENT_NAMES } from './server-events.js';
34+import { loadPlugins } from './plugin-loader.js';
35+import {
36+ initUserStorage,
37+ getCookieSecret,
38+ getCookieSessionName,
39+ ensurePublicDirectoriesExist,
40+ getUserDirectoriesList,
41+ migrateSystemPrompts,
42+ migrateUserData,
43+ requireLoginMiddleware,
44+ setUserDataMiddleware,
45+ shouldRedirectToLogin,
46+ cleanUploads,
47+ getSessionCookieAge,
48+ verifySecuritySettings,
49+ loginPageMiddleware,
50+} from './users.js';
51+
52+import getWebpackServeMiddleware from './middleware/webpack-serve.js';
53+import basicAuthMiddleware from './middleware/basicAuth.js';
54+import getWhitelistMiddleware from './middleware/whitelist.js';
55+import accessLoggerMiddleware, { getAccessLogPath, migrateAccessLog } from './middleware/accessLogWriter.js';
56+import multerMonkeyPatch from './middleware/multerMonkeyPatch.js';
57+import initRequestProxy from './request-proxy.js';
58+import getCacheBusterMiddleware from './middleware/cacheBuster.js';
59+import corsProxyMiddleware from './middleware/corsProxy.js';
60+import {
61+ getVersion,
62+ color,
63+ removeColorFormatting,
64+ getSeparator,
65+ safeReadFileSync,
66+ setupLogLevel,
67+ setWindowTitle,
68+} from './util.js';
69+import { UPLOADS_DIRECTORY } from './constants.js';
70+import { ensureThumbnailCache } from './endpoints/thumbnails.js';
71+
72+// Routers
73+import { router as usersPublicRouter } from './endpoints/users-public.js';
74+import { init as statsInit, onExit as statsOnExit } from './endpoints/stats.js';
75+import { checkForNewContent } from './endpoints/content-manager.js';
76+import { init as settingsInit } from './endpoints/settings.js';
77+import { redirectDeprecatedEndpoints, ServerStartup, setupPrivateEndpoints } from './server-startup.js';
78+import { diskCache } from './endpoints/characters.js';
79+
80+// Unrestrict console logs display limit
81+util.inspect.defaultOptions.maxArrayLength = null;
82+util.inspect.defaultOptions.maxStringLength = null;
83+util.inspect.defaultOptions.depth = 4;
84+
85+const cliArgs = globalThis.COMMAND_LINE_ARGS;
86+
87+if (!cliArgs.enableIPv6 && !cliArgs.enableIPv4) {
88+ console.error('error: You can\'t disable all internet protocols: at least IPv6 or IPv4 must be enabled.');
89+ process.exit(1);
90+}
91+
92+try {
93+ if (cliArgs.dnsPreferIPv6) {
94+ dns.setDefaultResultOrder('ipv6first');
95+ console.log('Preferring IPv6 for DNS resolution');
96+ } else {
97+ dns.setDefaultResultOrder('ipv4first');
98+ console.log('Preferring IPv4 for DNS resolution');
99+ }
100+} catch (error) {
101+ console.warn('Failed to set DNS resolution order. Possibly unsupported in this Node version.');
102+}
103+
104+const app = express();
105+app.use(helmet({
106+ contentSecurityPolicy: false,
107+}));
108+app.use(compression());
109+app.use(responseTime());
110+
111+app.use(bodyParser.json({ limit: '200mb' }));
112+app.use(bodyParser.urlencoded({ extended: true, limit: '200mb' }));
113+
114+// CORS Settings //
115+const CORS = cors({
116+ origin: 'null',
117+ methods: ['OPTIONS'],
118+});
119+
120+app.use(CORS);
121+
122+if (cliArgs.listen && cliArgs.basicAuthMode) {
123+ app.use(basicAuthMiddleware);
124+}
125+
126+if (cliArgs.whitelistMode) {
127+ const whitelistMiddleware = await getWhitelistMiddleware();
128+ app.use(whitelistMiddleware);
129+}
130+
131+if (cliArgs.listen) {
132+ app.use(accessLoggerMiddleware());
133+}
134+
135+if (cliArgs.enableCorsProxy) {
136+ app.use('/proxy/:url(*)', corsProxyMiddleware);
137+} else {
138+ app.use('/proxy/:url(*)', async (_, res) => {
139+ const message = 'CORS proxy is disabled. Enable it in config.yaml or use the --corsProxy flag.';
140+ console.log(message);
141+ res.status(404).send(message);
142+ });
143+}
144+
145+app.use(cookieSession({
146+ name: getCookieSessionName(),
147+ sameSite: 'lax',
148+ httpOnly: true,
149+ maxAge: getSessionCookieAge(),
150+ secret: getCookieSecret(globalThis.DATA_ROOT),
151+}));
152+
153+app.use(setUserDataMiddleware);
154+
155+// CSRF Protection //
156+if (!cliArgs.disableCsrf) {
157+ const csrfSyncProtection = csrfSync({
158+ getTokenFromState: (req) => {
159+ if (!req.session) {
160+ console.error('(CSRF error) getTokenFromState: Session object not initialized');
161+ return;
162+ }
163+ return req.session.csrfToken;
164+ },
165+ getTokenFromRequest: (req) => {
166+ return req.headers['x-csrf-token']?.toString();
167+ },
168+ storeTokenInState: (req, token) => {
169+ if (!req.session) {
170+ console.error('(CSRF error) storeTokenInState: Session object not initialized');
171+ return;
172+ }
173+ req.session.csrfToken = token;
174+ },
175+ size: 32,
176+ });
177+
178+ app.get('/csrf-token', (req, res) => {
179+ res.json({
180+ 'token': csrfSyncProtection.generateToken(req),
181+ });
182+ });
183+
184+ // Customize the error message
185+ csrfSyncProtection.invalidCsrfTokenError.message = color.red('Invalid CSRF token. Please refresh the page and try again.');
186+ csrfSyncProtection.invalidCsrfTokenError.stack = undefined;
187+
188+ app.use(csrfSyncProtection.csrfSynchronisedProtection);
189+} else {
190+ console.warn('\nCSRF protection is disabled. This will make your server vulnerable to CSRF attacks.\n');
191+ app.get('/csrf-token', (req, res) => {
192+ res.json({
193+ 'token': 'disabled',
194+ });
195+ });
196+}
197+
198+// Static files
199+// Host index page
200+app.get('/', getCacheBusterMiddleware(), (request, response) => {
201+ if (shouldRedirectToLogin(request)) {
202+ const query = request.url.split('?')[1];
203+ const redirectUrl = query ? `/login?${query}` : '/login';
204+ return response.redirect(redirectUrl);
205+ }
206+
207+ return response.sendFile('index.html', { root: path.join(serverDirectory, 'public') });
208+});
209+
210+// Callback endpoint for OAuth PKCE flows (e.g. OpenRouter)
211+app.get('/callback/:source?', (request, response) => {
212+ const source = request.params.source;
213+ const query = request.url.split('?')[1];
214+ const searchParams = new URLSearchParams();
215+ source && searchParams.set('source', source);
216+ query && searchParams.set('query', query);
217+ const path = `/?${searchParams.toString()}`;
218+ return response.redirect(307, path);
219+});
220+
221+// Host login page
222+app.get('/login', loginPageMiddleware);
223+
224+// Host frontend assets
225+const webpackMiddleware = getWebpackServeMiddleware();
226+app.use(webpackMiddleware);
227+app.use(express.static(path.join(serverDirectory, 'public'), {}));
228+
229+// Public API
230+app.use('/api/users', usersPublicRouter);
231+
232+// Everything below this line requires authentication
233+app.use(requireLoginMiddleware);
234+app.get('/api/ping', (request, response) => {
235+ if (request.query.extend && request.session) {
236+ request.session.touch = Date.now();
237+ }
238+
239+ response.sendStatus(204);
240+});
241+
242+// File uploads
243+const uploadsPath = path.join(cliArgs.dataRoot, UPLOADS_DIRECTORY);
244+app.use(multer({ dest: uploadsPath, limits: { fieldSize: 10 * 1024 * 1024 } }).single('avatar'));
245+app.use(multerMonkeyPatch);
246+
247+app.get('/version', async function (_, response) {
248+ const data = await getVersion();
249+ response.send(data);
250+});
251+
252+redirectDeprecatedEndpoints(app);
253+setupPrivateEndpoints(app);
254+
255+/**
256+ * Tasks that need to be run before the server starts listening.
257+ * @returns {Promise<void>}
258+ */
259+async function preSetupTasks() {
260+ const version = await getVersion();
261+
262+ // Print formatted header
263+ console.log();
264+ console.log(`SillyTavern ${version.pkgVersion}`);
265+ if (version.gitBranch) {
266+ console.log(`Running '${version.gitBranch}' (${version.gitRevision}) - ${version.commitDate}`);
267+ if (!version.isLatest && ['staging', 'release'].includes(version.gitBranch)) {
268+ console.log('INFO: Currently not on the latest commit.');
269+ console.log(' Run \'git pull\' to update. If you have any merge conflicts, run \'git reset --hard\' and \'git pull\' to reset your branch.');
270+ }
271+ }
272+ console.log();
273+
274+ const directories = await getUserDirectoriesList();
275+ await checkForNewContent(directories);
276+ await ensureThumbnailCache(directories);
277+ await diskCache.verify(directories);
278+ cleanUploads();
279+ migrateAccessLog();
280+
281+ await settingsInit();
282+ await statsInit();
283+
284+ const pluginsDirectory = path.join(serverDirectory, 'plugins');
285+ const cleanupPlugins = await loadPlugins(app, pluginsDirectory);
286+ const consoleTitle = process.title;
287+
288+ let isExiting = false;
289+ const exitProcess = async () => {
290+ if (isExiting) return;
291+ isExiting = true;
292+ await statsOnExit();
293+ if (typeof cleanupPlugins === 'function') {
294+ await cleanupPlugins();
295+ }
296+ diskCache.dispose();
297+ setWindowTitle(consoleTitle);
298+ process.exit();
299+ };
300+
301+ // Set up event listeners for a graceful shutdown
302+ process.on('SIGINT', exitProcess);
303+ process.on('SIGTERM', exitProcess);
304+ process.on('uncaughtException', (err) => {
305+ console.error('Uncaught exception:', err);
306+ exitProcess();
307+ });
308+
309+ // Add request proxy.
310+ initRequestProxy({ enabled: cliArgs.requestProxyEnabled, url: cliArgs.requestProxyUrl, bypass: cliArgs.requestProxyBypass });
311+
312+ // Wait for frontend libs to compile
313+ await webpackMiddleware.runWebpackCompiler();
314+}
315+
316+/**
317+ * Tasks that need to be run after the server starts listening.
318+ * @param {import('./server-startup.js').ServerStartupResult} result The result of the server startup
319+ * @returns {Promise<void>}
320+ */
321+async function postSetupTasks(result) {
322+ const autorunHostname = await cliArgs.getAutorunHostname(result);
323+ const autorunUrl = cliArgs.getAutorunUrl(autorunHostname);
324+
325+ if (cliArgs.autorun) {
326+ try {
327+ console.log('Launching in a browser...');
328+ await open(autorunUrl.toString());
329+ } catch (error) {
330+ console.error('Failed to launch the browser. Open the URL manually.');
331+ }
332+ }
333+
334+ setWindowTitle('SillyTavern WebServer');
335+
336+ let logListen = 'SillyTavern is listening on';
337+
338+ if (result.useIPv6 && !result.v6Failed) {
339+ logListen += color.green(
340+ ' IPv6: ' + cliArgs.getIPv6ListenUrl().host,
341+ );
342+ }
343+
344+ if (result.useIPv4 && !result.v4Failed) {
345+ logListen += color.green(
346+ ' IPv4: ' + cliArgs.getIPv4ListenUrl().host,
347+ );
348+ }
349+
350+ const goToLog = 'Go to: ' + color.blue(autorunUrl) + ' to open SillyTavern';
351+ const plainGoToLog = removeColorFormatting(goToLog);
352+
353+ console.log(logListen);
354+ if (cliArgs.listen) {
355+ console.log();
356+ console.log('To limit connections to internal localhost only ([::1] or 127.0.0.1), change the setting in config.yaml to "listen: false".');
357+ console.log('Check the "access.log" file in the data directory to inspect incoming connections:', color.green(getAccessLogPath()));
358+ }
359+ console.log('\n' + getSeparator(plainGoToLog.length) + '\n');
360+ console.log(goToLog);
361+ console.log('\n' + getSeparator(plainGoToLog.length) + '\n');
362+
363+ setupLogLevel();
364+ serverEvents.emit(EVENT_NAMES.SERVER_STARTED, { url: autorunUrl });
365+}
366+
367+/**
368+ * Registers a not-found error response if a not-found error page exists. Should only be called after all other middlewares have been registered.
369+ */
370+function apply404Middleware() {
371+ const notFoundWebpage = safeReadFileSync(path.join(serverDirectory, 'public/error/url-not-found.html')) ?? '';
372+ app.use((req, res) => {
373+ res.status(404).send(notFoundWebpage);
374+ });
375+}
376+
377+// User storage module needs to be initialized before starting the server
378+initUserStorage(globalThis.DATA_ROOT)
379+ .then(ensurePublicDirectoriesExist)
380+ .then(migrateUserData)
381+ .then(migrateSystemPrompts)
382+ .then(verifySecuritySettings)
383+ .then(preSetupTasks)
384+ .then(apply404Middleware)
385+ .then(() => new ServerStartup(app, cliArgs).start())
386+ .then(postSetupTasks);