Move server-main to /src

01c6544e22a3584c78bf9aa72bcee415bb2d910e

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

2 files changed, +388 -380Ignore whitespace
server.js+2 -380
@@ -1,393 +1,15 @@
1#!/usr/bin/env node1#!/usr/bin/env node
2
3// native node modules
4import path from 'node:path';
5import util from 'node:util';
6import net from 'node:net';
7import dns from 'node:dns';
8import process from 'node:process';
9
10import cors from 'cors';
11import { csrfSync } from 'csrf-sync';
12import express from 'express';
13import compression from 'compression';
14import cookieSession from 'cookie-session';
15import multer from 'multer';
16import responseTime from 'response-time';
17import helmet from 'helmet';
18import bodyParser from 'body-parser';
19import open from 'open';
20
21// local library imports
22import './src/fetch-patch.js';
23import { CommandLineParser } from './src/command-line.js';2import { CommandLineParser } from './src/command-line.js';
24import { serverDirectory } from './src/server-directory.js';3import { serverDirectory } from './src/server-directory.js';
254
26console.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.
31if (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
36// config.yaml will be set when parsing command line arguments5// config.yaml will be set when parsing command line arguments
37const cliArgs = new CommandLineParser().parse(process.argv);6const cliArgs = new CommandLineParser().parse(process.argv);
38globalThis.DATA_ROOT = cliArgs.dataRoot;7globalThis.DATA_ROOT = cliArgs.dataRoot;
39globalThis.COMMAND_LINE_ARGS = cliArgs;8globalThis.COMMAND_LINE_ARGS = cliArgs;
40process.chdir(serverDirectory);9process.chdir(serverDirectory);
4110
42const { serverEvents, EVENT_NAMES } = await import('./src/server-events.js');
43const { loadPlugins } = await import('./src/plugin-loader.js');
44const {
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
61const { default: getWebpackServeMiddleware } = await import('./src/middleware/webpack-serve.js');
62const { default: basicAuthMiddleware } = await import('./src/middleware/basicAuth.js');
63const { default: getWhitelistMiddleware } = await import('./src/middleware/whitelist.js');
64const { default: accessLoggerMiddleware, getAccessLogPath, migrateAccessLog } = await import('./src/middleware/accessLogWriter.js');
65const { default: multerMonkeyPatch } = await import('./src/middleware/multerMonkeyPatch.js');
66const { default: initRequestProxy } = await import('./src/request-proxy.js');
67const { default: getCacheBusterMiddleware } = await import('./src/middleware/cacheBuster.js');
68const { default: corsProxyMiddleware } = await import('./src/middleware/corsProxy.js');
69const {
70 getVersion,
71 color,
72 removeColorFormatting,
73 getSeparator,
74 safeReadFileSync,
75 setupLogLevel,
76 setWindowTitle,
77} = await import('./src/util.js');
78const { UPLOADS_DIRECTORY } = await import('./src/constants.js');
79const { ensureThumbnailCache } = await import('./src/endpoints/thumbnails.js');
80
81// Routers
82const { router : usersPublicRouter } = await import('./src/endpoints/users-public.js');
83const { init : statsInit, onExit : statsOnExit } = await import('./src/endpoints/stats.js');
84const { checkForNewContent } = await import('./src/endpoints/content-manager.js');
85const { init : settingsInit } = await import('./src/endpoints/settings.js');
86const { redirectDeprecatedEndpoints, ServerStartup, setupPrivateEndpoints } = await import('./src/server-startup.js');
87const { diskCache } = await import('./src/endpoints/characters.js');
88
89// Unrestrict console logs display limit
90util.inspect.defaultOptions.maxArrayLength = null;
91util.inspect.defaultOptions.maxStringLength = null;
92util.inspect.defaultOptions.depth = 4;
93
94if (!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
99try {11try {
100 if (cliArgs.dnsPreferIPv6) {12 await import('./src/server-main.js');
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) {13} catch (error) {
108 console.warn('Failed to set DNS resolution order. Possibly unsupported in this Node version.');14 console.error('A critical error has occurred while starting the server:', error);
109}15}
110
111const app = express();
112app.use(helmet({
113 contentSecurityPolicy: false,
114}));
115app.use(compression());
116app.use(responseTime());
117
118app.use(bodyParser.json({ limit: '200mb' }));
119app.use(bodyParser.urlencoded({ extended: true, limit: '200mb' }));
120
121// CORS Settings //
122const CORS = cors({
123 origin: 'null',
124 methods: ['OPTIONS'],
125});
126
127app.use(CORS);
128
129if (cliArgs.listen && cliArgs.basicAuthMode) {
130 app.use(basicAuthMiddleware);
131}
132
133if (cliArgs.whitelistMode) {
134 const whitelistMiddleware = await getWhitelistMiddleware();
135 app.use(whitelistMiddleware);
136}
137
138if (cliArgs.listen) {
139 app.use(accessLoggerMiddleware());
140}
141
142if (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
152app.use(cookieSession({
153 name: getCookieSessionName(),
154 sameSite: 'lax',
155 httpOnly: true,
156 maxAge: getSessionCookieAge(),
157 secret: getCookieSecret(globalThis.DATA_ROOT),
158}));
159
160app.use(setUserDataMiddleware);
161
162// CSRF Protection //
163if (!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
207app.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)
218app.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
229app.get('/login', loginPageMiddleware);
230
231// Host frontend assets
232const webpackMiddleware = getWebpackServeMiddleware();
233app.use(webpackMiddleware);
234app.use(express.static(path.join(serverDirectory, 'public'), {}));
235
236// Public API
237app.use('/api/users', usersPublicRouter);
238
239// Everything below this line requires authentication
240app.use(requireLoginMiddleware);
241app.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
250const uploadsPath = path.join(cliArgs.dataRoot, UPLOADS_DIRECTORY);
251app.use(multer({ dest: uploadsPath, limits: { fieldSize: 10 * 1024 * 1024 } }).single('avatar'));
252app.use(multerMonkeyPatch);
253
254app.get('/version', async function (_, response) {
255 const data = await getVersion();
256 response.send(data);
257});
258
259redirectDeprecatedEndpoints(app);
260setupPrivateEndpoints(app);
261
262/**
263 * Tasks that need to be run before the server starts listening.
264 * @returns {Promise<void>}
265 */
266async 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 */
328async function postSetupTasks(result) {
329 const autorunHostname = await cliArgs.getAutorunHostname(result);
330 const autorunUrl = cliArgs.getAutorunUrl(autorunHostname);
331
332 if (cliArgs.autorun) {
333 try {
334 console.log('Launching in a browser...');
335 await open(autorunUrl.toString());
336 } catch (error) {
337 console.error('Failed to launch the browser. Open the URL manually.');
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 );
349 }
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 */
377function 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
385initUserStorage(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
2import path from 'node:path';
3import util from 'node:util';
4import net from 'node:net';
5import dns from 'node:dns';
6import process from 'node:process';
7
8import cors from 'cors';
9import { csrfSync } from 'csrf-sync';
10import express from 'express';
11import compression from 'compression';
12import cookieSession from 'cookie-session';
13import multer from 'multer';
14import responseTime from 'response-time';
15import helmet from 'helmet';
16import bodyParser from 'body-parser';
17import open from 'open';
18
19// local library imports
20import './fetch-patch.js';
21import { serverDirectory } from './server-directory.js';
22
23console.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.
28if (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
33import { serverEvents, EVENT_NAMES } from './server-events.js';
34import { loadPlugins } from './plugin-loader.js';
35import {
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
52import getWebpackServeMiddleware from './middleware/webpack-serve.js';
53import basicAuthMiddleware from './middleware/basicAuth.js';
54import getWhitelistMiddleware from './middleware/whitelist.js';
55import accessLoggerMiddleware, { getAccessLogPath, migrateAccessLog } from './middleware/accessLogWriter.js';
56import multerMonkeyPatch from './middleware/multerMonkeyPatch.js';
57import initRequestProxy from './request-proxy.js';
58import getCacheBusterMiddleware from './middleware/cacheBuster.js';
59import corsProxyMiddleware from './middleware/corsProxy.js';
60import {
61 getVersion,
62 color,
63 removeColorFormatting,
64 getSeparator,
65 safeReadFileSync,
66 setupLogLevel,
67 setWindowTitle,
68} from './util.js';
69import { UPLOADS_DIRECTORY } from './constants.js';
70import { ensureThumbnailCache } from './endpoints/thumbnails.js';
71
72// Routers
73import { router as usersPublicRouter } from './endpoints/users-public.js';
74import { init as statsInit, onExit as statsOnExit } from './endpoints/stats.js';
75import { checkForNewContent } from './endpoints/content-manager.js';
76import { init as settingsInit } from './endpoints/settings.js';
77import { redirectDeprecatedEndpoints, ServerStartup, setupPrivateEndpoints } from './server-startup.js';
78import { diskCache } from './endpoints/characters.js';
79
80// Unrestrict console logs display limit
81util.inspect.defaultOptions.maxArrayLength = null;
82util.inspect.defaultOptions.maxStringLength = null;
83util.inspect.defaultOptions.depth = 4;
84
85const cliArgs = globalThis.COMMAND_LINE_ARGS;
86
87if (!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
92try {
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
104const app = express();
105app.use(helmet({
106 contentSecurityPolicy: false,
107}));
108app.use(compression());
109app.use(responseTime());
110
111app.use(bodyParser.json({ limit: '200mb' }));
112app.use(bodyParser.urlencoded({ extended: true, limit: '200mb' }));
113
114// CORS Settings //
115const CORS = cors({
116 origin: 'null',
117 methods: ['OPTIONS'],
118});
119
120app.use(CORS);
121
122if (cliArgs.listen && cliArgs.basicAuthMode) {
123 app.use(basicAuthMiddleware);
124}
125
126if (cliArgs.whitelistMode) {
127 const whitelistMiddleware = await getWhitelistMiddleware();
128 app.use(whitelistMiddleware);
129}
130
131if (cliArgs.listen) {
132 app.use(accessLoggerMiddleware());
133}
134
135if (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
145app.use(cookieSession({
146 name: getCookieSessionName(),
147 sameSite: 'lax',
148 httpOnly: true,
149 maxAge: getSessionCookieAge(),
150 secret: getCookieSecret(globalThis.DATA_ROOT),
151}));
152
153app.use(setUserDataMiddleware);
154
155// CSRF Protection //
156if (!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
200app.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)
211app.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
222app.get('/login', loginPageMiddleware);
223
224// Host frontend assets
225const webpackMiddleware = getWebpackServeMiddleware();
226app.use(webpackMiddleware);
227app.use(express.static(path.join(serverDirectory, 'public'), {}));
228
229// Public API
230app.use('/api/users', usersPublicRouter);
231
232// Everything below this line requires authentication
233app.use(requireLoginMiddleware);
234app.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
243const uploadsPath = path.join(cliArgs.dataRoot, UPLOADS_DIRECTORY);
244app.use(multer({ dest: uploadsPath, limits: { fieldSize: 10 * 1024 * 1024 } }).single('avatar'));
245app.use(multerMonkeyPatch);
246
247app.get('/version', async function (_, response) {
248 const data = await getVersion();
249 response.send(data);
250});
251
252redirectDeprecatedEndpoints(app);
253setupPrivateEndpoints(app);
254
255/**
256 * Tasks that need to be run before the server starts listening.
257 * @returns {Promise<void>}
258 */
259async 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 */
321async 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 */
370function 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
378initUserStorage(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);