455 lines
13 KiB
JavaScript
455 lines
13 KiB
JavaScript
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;
|