const { spawn } = require('child_process'); const path = require('path'); const fs = require('fs'); const http = require('http'); const express = require('express'); const net = require('net'); const config = require('../config'); const PORT_RANGE_START = config.projectPortStart; const PORT_RANGE_END = config.projectPortEnd; const runningProcesses = new Map(); const checkPortAvailable = (port) => { return new Promise((resolve) => { const server = net.createServer(); server.once('error', () => { resolve(false); }); server.once('listening', () => { server.close(); resolve(true); }); server.listen(port); }); }; const getUsedPortsFromRunningProcesses = () => { const ports = new Set(); for (const [, info] of runningProcesses.entries()) { if (info.port) { ports.add(info.port); } } return ports; }; const findAvailablePort = async () => { const systemUsedPorts = getUsedPortsFromRunningProcesses(); for (let port = PORT_RANGE_START; port <= PORT_RANGE_END; port++) { if (!systemUsedPorts.has(port)) { const available = await checkPortAvailable(port); if (available) { return port; } } } throw new Error(`No available ports in range ${PORT_RANGE_START}-${PORT_RANGE_END}`); }; const detectProjectType = (projectDir) => { if (fs.existsSync(path.join(projectDir, 'package.json'))) { const pkg = JSON.parse(fs.readFileSync(path.join(projectDir, 'package.json'), 'utf8')); if (pkg.scripts && pkg.scripts.start) { return 'node'; } if (fs.existsSync(path.join(projectDir, 'vite.config.js')) || fs.existsSync(path.join(projectDir, 'vite.config.ts'))) { return 'vite'; } if (fs.existsSync(path.join(projectDir, 'vue.config.js'))) { return 'vue'; } return 'node'; } const files = fs.readdirSync(projectDir); if (files.some(f => f.endsWith('.html'))) { return 'static'; } return 'static'; }; const startStaticServer = (projectDir, port, projectPath = '') => { return new Promise((resolve, reject) => { const app = express(); if (projectPath) { app.use(projectPath, express.static(projectDir)); app.get(`${projectPath}/*`, (req, res) => { const indexPath = path.join(projectDir, 'index.html'); if (fs.existsSync(indexPath)) { res.sendFile(indexPath); } else { res.status(404).send('index.html not found'); } }); } else { app.use(express.static(projectDir)); app.get('*', (req, res) => { const indexPath = path.join(projectDir, 'index.html'); if (fs.existsSync(indexPath)) { res.sendFile(indexPath); } else { res.status(404).send('index.html not found'); } }); } const server = app.listen(port, () => { resolve({ server, port }); }); server.on('error', (err) => { if (err.code === 'EADDRINUSE') { reject(new Error(`Port ${port} is already in use`)); } else { reject(err); } }); }); }; const startProject = async (project) => { const projectDir = path.join(__dirname, '../../projects', project.id); if (!fs.existsSync(projectDir)) { throw new Error('Project directory not found'); } const port = await findAvailablePort(); const projectType = detectProjectType(projectDir); const projectPath = project.path || ''; let processInfo = null; switch (projectType) { case 'static': const { server } = await startStaticServer(projectDir, port, projectPath); processInfo = { type: 'static', server, port }; break; case 'node': const nodeProcess = spawn('npm', ['start'], { cwd: projectDir, env: { ...process.env, PORT: port.toString() }, shell: true }); processInfo = { type: 'node', process: nodeProcess, port }; nodeProcess.on('exit', () => { runningProcesses.delete(project.id); }); break; case 'vite': case 'vue': const viteProcess = spawn('npx', ['vite', '--port', port.toString(), '--host'], { cwd: projectDir, shell: true }); processInfo = { type: 'vite', process: viteProcess, port }; viteProcess.on('exit', () => { runningProcesses.delete(project.id); }); break; } runningProcesses.set(project.id, processInfo); const url = config.getProjectUrl(port, projectPath); return { port, url }; }; const stopProject = (project) => { const processInfo = runningProcesses.get(project.id); if (!processInfo) { return false; } const port = processInfo.port; if (processInfo.type === 'static' && processInfo.server) { processInfo.server.close(); } else if (processInfo.process) { processInfo.process.kill('SIGTERM'); } runningProcesses.delete(project.id); return { stopped: true, port }; }; const forceStopByPort = (port) => { for (const [projectId, info] of runningProcesses.entries()) { if (info.port === port) { if (info.type === 'static' && info.server) { info.server.close(); } else if (info.process) { info.process.kill('SIGTERM'); } runningProcesses.delete(projectId); return true; } } return false; }; const isPortInUse = async (port) => { for (const [, info] of runningProcesses.entries()) { if (info.port === port) { return true; } } return !(await checkPortAvailable(port)); }; const releaseAllPorts = () => { for (const [projectId, info] of runningProcesses.entries()) { if (info.type === 'static' && info.server) { try { info.server.close(); } catch (e) { } } else if (info.process) { try { info.process.kill('SIGTERM'); } catch (e) { } } } runningProcesses.clear(); }; const getPortStatus = async () => { const status = []; for (let port = PORT_RANGE_START; port <= PORT_RANGE_END; port++) { const runningProcess = Array.from(runningProcesses.entries()) .find(([, info]) => info.port === port); const systemAvailable = await checkPortAvailable(port); status.push({ port, inUseBySystem: !systemAvailable, inUseByProject: runningProcess ? { projectId: runningProcess[0], type: runningProcess[1].type } : null }); } return status; }; const getRunningProcesses = () => { return Array.from(runningProcesses.entries()).map(([id, info]) => ({ projectId: id, type: info.type, port: info.port })); }; module.exports = { startProject, stopProject, getRunningProcesses, forceStopByPort, isPortInUse, releaseAllPorts, getPortStatus, checkPortAvailable, findAvailablePort, PORT_RANGE_START, PORT_RANGE_END };