Files
XCDesktop/remote/src/core/App.js
ssdfasd 6d5520dfa5 feat(remote): 文件传输改用Electron IPC通道
- 主进程新增4个IPC handler处理远程文件操作
- 前端通过IPC调用而非浏览器fetch访问远程API
- Remote服务新增3003端口专门处理文件传输
- 上传使用文件路径方案,下载使用保存对话框方案
2026-03-10 00:34:02 +08:00

558 lines
17 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('openCodeService', (c) => {
const OpenCodeService = require('../services/opencode/OpenCodeService');
const config = c.resolve('config');
const opencodeConfig = config.getSection('opencode') || {};
return new OpenCodeService({
enabled: opencodeConfig.enabled !== false,
port: opencodeConfig.port || 3002
});
});
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,
fileTransferPort: serverConfig.fileTransferPort || 3003,
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 openCodeService = this.container.resolve('openCodeService');
openCodeService.start();
logger.info('OpenCode 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');
// 简单的 CORS 中间件
httpServer.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') {
return res.sendStatus(200);
}
next();
});
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(async (req, res, next) => {
// 放行 CORS 预检请求
if (req.method === 'OPTIONS') {
return 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) {
logger.info('HTTP auth: token valid from cookie', { path: req.path, ip: req.socket?.remoteAddress });
res.locals.authenticated = true;
return next();
} else {
logger.info('HTTP auth: token invalid from cookie', { path: req.path, ip: req.socket?.remoteAddress });
}
}
// 检查 URL 参数中的 password
const urlPassword = req.query.password;
if (urlPassword) {
logger.info('HTTP auth: checking password from URL', {
path: req.path,
ip: req.socket?.remoteAddress,
passwordLength: urlPassword.length
});
const isValid = await authService.authenticate(urlPassword);
if (isValid) {
const newToken = tokenManager.generateToken({ userId: 'default-user' });
res.cookie('auth', newToken, { httpOnly: true, maxAge: 24 * 60 * 60 * 1000 });
logger.info('HTTP auth: password valid, token generated', { path: req.path, ip: req.socket?.remoteAddress });
res.locals.authenticated = true;
return next();
} else {
logger.warn('HTTP auth: password invalid', { path: req.path, ip: req.socket?.remoteAddress });
}
}
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');
this.wss.on('connection', async (ws, req) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const fullUrl = req.url;
let isAuthenticated = false;
let authMethod = '';
// 检查 URL 参数中的 password
const urlPassword = url.searchParams.get('password');
if (urlPassword) {
logger.info('WebSocket auth: checking password from URL', {
ip: req.socket?.remoteAddress,
urlLength: fullUrl.length,
passwordLength: urlPassword.length,
passwordPrefix: urlPassword.substring(0, 2) + '***'
});
const isValid = await authService.authenticate(urlPassword);
if (isValid) {
isAuthenticated = true;
authMethod = 'password_url';
logger.info('WebSocket auth: password from URL valid', { ip: req.socket?.remoteAddress });
} else {
logger.warn('WebSocket auth: password from URL invalid', { ip: req.socket?.remoteAddress });
}
}
// 检查 Cookie 中的 token
if (!isAuthenticated && req.cookies && req.cookies.auth) {
logger.info('WebSocket auth: checking token from cookie', { ip: req.socket?.remoteAddress });
const decoded = tokenManager.verifyToken(req.cookies.auth);
if (decoded) {
isAuthenticated = true;
authMethod = 'cookie_token';
logger.info('WebSocket auth: token from cookie valid', { ip: req.socket?.remoteAddress });
} else {
logger.warn('WebSocket auth: token from cookie invalid', { ip: req.socket?.remoteAddress });
}
}
// 未认证,拒绝连接
if (!isAuthenticated) {
logger.warn('WebSocket authentication failed', {
ip: req.socket?.remoteAddress,
hasPassword: !!urlPassword,
hasCookie: !!(req.cookies && req.cookies.auth),
fullUrl: fullUrl.substring(0, 200)
});
ws.close(1008, 'Authentication required');
return;
}
logger.debug('WebSocket authenticated', { ip: req.socket?.remoteAddress, authMethod });
// 保存认证状态
ws.isAuthenticated = true;
// 处理输入消息
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 openCodeService = this.container.resolve('openCodeService');
openCodeService.stop();
logger.info('OpenCode 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;