272 lines
6.3 KiB
JavaScript
272 lines
6.3 KiB
JavaScript
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) => {
|
|
return new Promise((resolve, reject) => {
|
|
const app = express();
|
|
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);
|
|
|
|
let processInfo = null;
|
|
|
|
switch (projectType) {
|
|
case 'static':
|
|
const { server } = await startStaticServer(projectDir, port);
|
|
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);
|
|
|
|
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
|
|
};
|