Files
XCDesktop/remote/src/core/App.js
2026-03-08 01:34:54 +08:00

455 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const Container = require('./Container');
const EventBus = require('./EventBus');
const EventTypes = require('./events');
const { ErrorHandler } = require('./ErrorHandler');
const logger = require('../utils/logger');
const MessageTypes = require('../server/messageTypes');
const SHUTDOWN_TIMEOUT = 30000;
class App {
constructor() {
this.container = null;
this.eventBus = null;
this.started = false;
this.shuttingDown = false;
this.lastClipboardText = '';
}
async bootstrap() {
this.container = new Container();
this.eventBus = new EventBus();
this.container.register('eventBus', () => this.eventBus);
this._registerConfig();
this._registerLogger();
this._registerErrorHandler();
this._registerAuthServices();
this._registerStreamServices();
this._registerInputServices();
this._registerClipboardServices();
this._registerFileServices();
this._registerNetworkServices();
this._registerServer();
logger.info('Application bootstrap completed');
return this;
}
_registerConfig() {
this.container.register('config', (c) => {
return require('../config');
});
}
_registerLogger() {
this.container.register('logger', (c) => {
return require('../utils/logger');
});
}
_registerEventBus() {
this.container.register('eventBus', (c) => {
return this.eventBus;
});
}
_registerErrorHandler() {
this.container.register('errorHandler', (c) => {
return new ErrorHandler();
});
}
_registerAuthServices() {
this.container.register('authService', (c) => {
const AuthService = require('../services/auth/AuthService');
return new AuthService();
});
this.container.register('tokenManager', (c) => {
const TokenManager = require('../services/auth/TokenManager');
return new TokenManager();
});
}
_registerStreamServices() {
this.container.register('ffmpegEncoder', (c) => {
const FFmpegEncoder = require('../services/stream/FFmpegEncoder');
return new FFmpegEncoder();
});
}
_registerInputServices() {
this.container.register('inputService', (c) => {
const PowerShellInput = require('../services/input/PowerShellInput');
return new PowerShellInput();
});
this.container.register('inputHandler', (c) => {
const InputHandler = require('../server/InputHandler');
const inputService = c.resolve('inputService');
return new InputHandler(inputService);
});
}
_registerClipboardServices() {
this.container.register('clipboardService', (c) => {
const { clipboardService } = require('../services/clipboard');
return clipboardService;
});
}
_registerFileServices() {
this.container.register('fileService', (c) => {
const { fileService } = require('../services/file');
return fileService;
});
}
_registerNetworkServices() {
this.container.register('frpService', (c) => {
const FRPService = require('../services/network/FRPService');
const config = c.resolve('config');
const frpConfig = config.getSection('frp') || {};
return new FRPService({
enabled: frpConfig.enabled !== false
});
});
this.container.register('giteaService', (c) => {
const GiteaService = require('../services/network/GiteaService');
const config = c.resolve('config');
const giteaConfig = config.getSection('gitea') || {};
return new GiteaService({
enabled: giteaConfig.enabled !== false
});
});
}
_registerServer() {
this.container.register('httpServer', (c) => {
const Server = require('../server/Server');
const config = c.resolve('config');
const serverConfig = config.getSection('server') || {};
return new Server({
port: serverConfig.port || 3000,
host: serverConfig.host || '0.0.0.0'
});
});
this.container.register('wsServer', (c) => {
const WebSocketServer = require('../server/WebSocketServer');
return new WebSocketServer();
});
this.container.register('streamBroadcaster', (c) => {
const StreamBroadcaster = require('../server/StreamBroadcaster');
const wsServer = c.resolve('wsServer');
return new StreamBroadcaster(wsServer);
});
}
async start() {
if (this.started) {
logger.warn('Application already started');
return;
}
const errorHandler = this.container.resolve('errorHandler');
errorHandler.initialize();
this._setupRoutes();
const httpServer = this.container.resolve('httpServer');
const address = await httpServer.start();
logger.info('HTTP server started', { address });
const wsServer = this.container.resolve('wsServer');
wsServer.start(httpServer.getHTTPServer());
logger.info('WebSocket server started');
this._setupWebSocketHandlers();
const ffmpegEncoder = this.container.resolve('ffmpegEncoder');
ffmpegEncoder.start();
logger.info('FFmpeg encoder started');
const screenRes = ffmpegEncoder.getScreenResolution();
wsServer.setScreenResolution(screenRes.width, screenRes.height);
const streamBroadcaster = this.container.resolve('streamBroadcaster');
streamBroadcaster.setEncoder(ffmpegEncoder);
logger.info('Stream broadcaster attached to encoder');
const inputService = this.container.resolve('inputService');
await inputService.start();
logger.info('Input service started');
const frpService = this.container.resolve('frpService');
frpService.start();
logger.info('FRP service started');
const giteaService = this.container.resolve('giteaService');
giteaService.start();
logger.info('Gitea service started');
// 启动剪贴板监控,主动同步到主控
this._startClipboardWatcher();
this.started = true;
await this.eventBus.emit(EventTypes.APP_START, {
timestamp: new Date().toISOString(),
address
});
logger.info('Application started successfully');
this._setupGracefulShutdown();
}
_setupRoutes() {
const httpServer = this.container.resolve('httpServer');
const config = this.container.resolve('config');
const authService = this.container.resolve('authService');
const tokenManager = this.container.resolve('tokenManager');
const inputHandler = this.container.resolve('inputHandler');
const paths = require('../utils/paths');
const express = require('express');
const cookieParser = require('cookie-parser');
const authMiddleware = require('../middlewares/auth');
const routes = require('../routes');
httpServer.use(cookieParser());
httpServer.use(express.json());
httpServer.use(express.urlencoded({ extended: true }));
httpServer.app.post('/login', async (req, res) => {
const { password } = req.body;
const isValid = await authService.authenticate(password);
if (isValid) {
const token = tokenManager.generateToken({ userId: 'default-user' });
res.cookie('auth', token, { httpOnly: true, maxAge: 24 * 60 * 60 * 1000 });
return res.redirect('/');
}
httpServer.renderLoginPage(res, '密码错误');
});
httpServer.use((req, res, next) => {
if (!authService.hasPassword()) {
res.locals.authenticated = true;
return next();
}
const token = req.cookies && req.cookies.auth;
if (token) {
const decoded = tokenManager.verifyToken(token);
if (decoded) {
res.locals.authenticated = true;
return next();
}
}
if (req.path === '/' || req.path === '/index.html') {
return httpServer.renderLoginPage(res);
}
res.status(401).json({ error: 'Authentication required' });
});
httpServer.static(paths.getPublicPath());
httpServer.use('/api', authMiddleware);
httpServer.use('/api', routes);
httpServer.app.get('/api/config', (req, res) => {
try {
res.json(config.getAll());
} catch (error) {
logger.error('Error getting config', { error: error.message });
res.status(500).json({ error: 'Failed to get config' });
}
});
const wsServer = this.container.resolve('wsServer');
const originalSetup = wsServer.setupConnectionHandler.bind(wsServer);
wsServer.setupConnectionHandler = function() {
originalSetup();
const originalHandlers = this.wss.listeners('connection');
this.wss.removeAllListeners('connection');
const securityConfig = require('../utils/config').getSecurityConfig();
const password = securityConfig.password;
// 未认证的连接也允许,用于剪贴板同步
this.wss.on('connection', (ws, req) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const isAuthenticated = url.searchParams.get('password') === password;
// 保存认证状态
ws.isAuthenticated = isAuthenticated;
// 处理输入消息(不检查认证)
ws.on('message', (data) => {
try {
const message = JSON.parse(data);
inputHandler.handleMessage(message, ws);
} catch (error) {
logger.debug('Failed to parse WebSocket message', { error: error.message });
}
});
// 调用原始 handlers用于已认证的连接
originalHandlers.forEach(handler => {
handler(ws, req);
});
});
};
}
_setupWebSocketHandlers() {
// WebSocket handlers set up in _ aresetupRoutes()
// to avoid duplicate message handling
}
_startClipboardWatcher() {
const clipboardService = this.container.resolve('clipboardService');
const wsServer = this.container.resolve('wsServer');
clipboardService.read().then(content => {
if (content.type === 'text') {
this.lastClipboardText = content.data || '';
}
}).catch(() => {});
setInterval(async () => {
try {
const content = await clipboardService.read();
if (content.type === 'text' && content.data !== this.lastClipboardText) {
this.lastClipboardText = content.data || '';
const message = JSON.stringify({
type: MessageTypes.CLIPBOARD_DATA,
contentType: content.type,
data: content.data,
size: content.size
});
wsServer.clients.forEach((client) => {
if (client.readyState === 1) { // WebSocket.OPEN
client.send(message);
}
});
logger.info('Clipboard changed, synced to client');
}
} catch (error) {
// ignore
}
}, 1000);
}
_setupGracefulShutdown() {
const shutdown = async (signal) => {
if (this.shuttingDown) {
return;
}
this.shuttingDown = true;
logger.info(`Received ${signal}, starting graceful shutdown`);
await this.stop();
process.exit(0);
};
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('exit', () => {
logger.info('Process exiting');
});
}
async stop() {
if (!this.started) {
return;
}
logger.info('Stopping application');
const shutdownPromise = async () => {
const frpService = this.container.resolve('frpService');
frpService.stop();
logger.info('FRP service stopped');
const giteaService = this.container.resolve('giteaService');
giteaService.stop();
logger.info('Gitea service stopped');
const streamBroadcaster = this.container.resolve('streamBroadcaster');
streamBroadcaster.stop();
logger.info('Stream broadcaster stopped');
const ffmpegEncoder = this.container.resolve('ffmpegEncoder');
ffmpegEncoder.stop();
logger.info('FFmpeg encoder stopped');
const inputHandler = this.container.resolve('inputHandler');
inputHandler.stop();
logger.info('Input handler stopped');
const inputService = this.container.resolve('inputService');
await inputService.stop();
logger.info('Input service stopped');
const wsServer = this.container.resolve('wsServer');
wsServer.stop();
logger.info('WebSocket server stopped');
const httpServer = this.container.resolve('httpServer');
await httpServer.stop();
logger.info('HTTP server stopped');
await this.eventBus.emit(EventTypes.APP_STOP, {
timestamp: new Date().toISOString()
});
this.started = false;
logger.info('Application stopped successfully');
};
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error('Shutdown timeout exceeded'));
}, SHUTDOWN_TIMEOUT);
});
try {
await Promise.race([shutdownPromise(), timeoutPromise]);
} catch (error) {
logger.error('Error during shutdown', { error: error.message });
this.started = false;
}
}
getService(name) {
if (!this.container) {
throw new Error('Application not bootstrapped');
}
return this.container.resolve(name);
}
getEventBus() {
return this.eventBus;
}
isStarted() {
return this.started;
}
}
module.exports = App;