auto-deploy-demo/server/services/processManager.js

287 lines
6.8 KiB
JavaScript
Raw Normal View History

2026-02-23 06:31:59 +00:00
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 = '') => {
2026-02-23 06:31:59 +00:00
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');
}
});
}
2026-02-23 06:31:59 +00:00
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 || '';
2026-02-23 06:31:59 +00:00
let processInfo = null;
switch (projectType) {
case 'static':
const { server } = await startStaticServer(projectDir, port, projectPath);
2026-02-23 06:31:59 +00:00
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);
2026-02-23 06:31:59 +00:00
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
};