nda-installer
Version:
An application to deploy NodeJS projects from an internet browser
488 lines (429 loc) • 16.5 kB
JavaScript
;
/**
* NDA is protected by DMCA (2022).
* It's source code is licensed under GNU AGPL 3.0
*/
const processKill = require('tree-kill');
const { spawn } = require('child_process');
const pidusage = require('pidusage');
const fs = require('fs');
const path = require('path');
const os = require('os');
const { writeLog, getErrorType, deleteProjectLogs } = require('../helpers/log-writer');
const { PROJECT } = require('../helpers/constant-texts');
const { convertBufferToArray, isPortInUse, currentDateTime, isPidRunning } = require('../helpers/utilities');
const projectConfig = require('../../config/project-configs');
const PROJECTS_CONFIG_PATH = projectConfig.PROJECTS_CONFIG_PATH;
const CHILD_PROCESS_LOG_PATH = projectConfig.CHILD_PROCESS_LOG_PATH;
const CONFIG_PATH = projectConfig.CHILD_PROCESS_BASE_CONFIG_PATH;
let activeUID = {};
const _projects = async () => {
let projects = fs.readFileSync(PROJECTS_CONFIG_PATH);
try {
projects = projects && projects.length > 0 && JSON.parse(projects) ? JSON.parse(projects) : [];
let pids = [];
projects.forEach((obj) => {
if (obj.pid) {
if (Number.isInteger(obj.pid) && isPidRunning(obj.pid)) {
pids.push(obj.pid);
} else {
obj.pid = 'N/A';
}
}
});
projects.sort((a, b) => {
return new Date(b.updatedAt) - new Date(a.updatedAt);
});
if (pids.length > 0) {
let pidStats = await pidusage(pids);
if (pidStats) {
projects.forEach((obj) => {
if (Number.isInteger(obj.pid) && pidStats[obj.pid.toString()]) {
obj.cpu = parseFloat(pidStats[obj.pid.toString()].cpu).toFixed(2);
if (obj.cpu > 100) {
obj.cpu = 100.00;
}
}
});
}
}
} catch (err) {
writeLog(null, 500, err, '_2')
}
return projects;
};
const _updateProjects = async (fields, UID = null) => {
let projects = await _projects();
projects.forEach((obj) => {
if (UID && UID === obj.UID) {
for (const key in fields) {
obj[key] = fields[key];
}
} else if (!UID) {
for (const key in fields) {
obj[key] = fields[key];
}
}
});
fs.writeFileSync(PROJECTS_CONFIG_PATH, JSON.stringify(projects));
};
const _projectFieldByKey = async (key, value, fields) => {
let projects = await _projects();
let projectField = {};
if (projects && projects.length > 0) {
let project = projects.filter((obj) => {
return obj[key].toString() === value.toString();
});
if (fields && fields.length > 0 && project && project[0]) {
fields.forEach((key) => {
if (project[0][key]) {
projectField[key] = project[0][key];
}
});
} else {
projectField = project && project[0] ? project[0] : null;
}
}
return projectField;
};
const _projectInfo = async (UID) => {
let projects = await _projects();
let projectInfo = {};
if (projects && projects.length > 0) {
let project = projects.filter((obj) => {
return obj['UID'].toString() === UID.toString();
});
projectInfo = project && project[0] ? project[0] : null;
}
return projectInfo;
};
const _projectFields = (query) => {
let { name, port, envvariables, projectpath, UID, startfilepath, pkginstallopt, jobs, bootstartopt } = query;
let jobsCount = jobs && Array.isArray(jobs) && jobs.length > 0 ? jobs.length : 'N/A';
return { name, port, envvariables, projectpath, UID, startfilepath, pkginstallopt, jobsCount, bootstartopt, cpu: 'N/A', pid: 'N/A', createdAt: currentDateTime(), updatedAt: currentDateTime(), jobs };
};
const _hashFromName = (input) => {
let hash = 0, len = input.length;
for (let i = 0; i < len; i++) {
hash += input.charCodeAt(i);
}
return hash + '' + new Date().getUTCFullYear() + '' + new Date().getUTCDay() + '' + new Date().getUTCHours() + '' + new Date().getUTCMinutes();
}
const doPreChecks = (configParams, callback) => {
if (!fs.existsSync(path.resolve(configParams.projectpath, configParams.startfilepath))) {
writeLog(null, 500, `Unable to start - ${configParams.name}. Invalid project base path or start file`, '_2', configParams.UID);
callback({ code: '#0411' });
} else if (!configParams.pkginstallopt || os.type().indexOf('Darwin') > -1) {
isPortInUse(configParams.port, function (data) {
if (data && data.inUse) {
callback({ code: '#0409' });
} else {
callback(null);
}
});
} else {
writeLog(null, null, `Running npm install for "${configParams.name}".`, '_0', configParams.UID);
let childProcess = spawn(os.type().indexOf('Windows') > -1 ? 'npm.cmd' : 'npm', ['install'], { cwd: path.resolve(configParams.projectpath) });
childProcess.stdout.on('data', (data) => {
writeLog(null, null, convertBufferToArray(data), '_0', configParams.UID);
});
childProcess.stderr.on('data', (data) => {
let errMsg = convertBufferToArray(data);
let msgType = getErrorType(errMsg);
writeLog(null, null, errMsg, msgType, configParams.UID);
});
childProcess.on('exit', () => {
isPortInUse(configParams.port, function (data) {
if (data && data.inUse) {
callback({ code: '#0409' });
} else {
callback(null);
}
});
});
}
};
const handleJobs = (projectpath, jobs, envvariables, UID) => {
if (jobs && Array.isArray(jobs) && jobs.length > 0) {
let availableJobs = 0;
let jobPids = [];
for (let i = 0; i < jobs.length; i++) {
const jobKey = i + 1;
if (jobs[i]['job-status-' + jobKey]) {
availableJobs++;
writeLog(null, null, `Starting the job - ${jobs[i]['job-name-' + jobKey]}.`, '_0', UID);
jobs[i].pidSet = false;
let jobsChild = spawn(process.execPath, [path.resolve(projectpath, jobs[i]['job-path-' + jobKey])], {
env: envvariables, detached: true, cwd: projectpath
});
jobsChild.stdout.on('data', async (data) => {
writeLog(null, null, convertBufferToArray(data), '_0', UID);
if (!jobs[i].pidSet) {
jobs[i].pidSet = true;
jobPids.push({ name: jobs[i]['job-name-' + jobKey], pid: jobsChild.pid });
if (jobPids.length === availableJobs) {
await _updateProjects({ jobPids }, UID);
}
}
});
jobsChild.stderr.on('data', async (data) => {
let errMsg = convertBufferToArray(data);
let msgType = getErrorType(errMsg);
writeLog(null, 500, errMsg, msgType, UID);
if (!jobs[i].pidSet) {
jobs[i].pidSet = true;
jobPids.push({ name: jobs[i]['job-name-' + jobKey], pid: jobsChild.pid });
if (jobPids.length === availableJobs) {
await _updateProjects({ jobPids }, UID);
}
}
});
}
}
}
};
const _startProject = async (UID, callback) => {
let { envvariables, port, projectpath, startfilepath, jobs, name, pkginstallopt } = await _projectInfo(UID);
let callBackCount = 0;
let callbackSent = 0;
if (parseInt(port) > 0) {
doPreChecks({ UID, port, projectpath, name, startfilepath, pkginstallopt }, (err) => {
if (err) {
callback(err);
} else {
if (projectpath && projectpath.length > 0 && startfilepath && startfilepath.length > 0) {
let todayDate = currentDateTime().split(' ')[0];
let logFilePath = path.resolve(CHILD_PROCESS_LOG_PATH, `${UID}_${todayDate}.txt`);
if (!fs.existsSync(logFilePath)) {
fs.writeFileSync(logFilePath, '');
}
try {
envvariables = JSON.parse(envvariables);
} catch (err) {
envvariables = {};
}
for (const key in envvariables) {
envvariables[key] = envvariables[key].toString().trim();
}
if (fs.existsSync(path.resolve(projectpath, 'config'))) {
envvariables.NODE_CONFIG_DIR = path.resolve(projectpath, 'config');
}
if (port) {
envvariables.port = port;
}
writeLog(null, null, `Starting the project - ${name}.`, '_0', UID);
let child = spawn(process.execPath, [path.resolve(projectpath, startfilepath)], {
env: envvariables, detached: true, cwd: projectpath
});
let err = null;
let success = null;
let callbackType = null;
child.stdout.on('data', (data) => {
writeLog(null, null, convertBufferToArray(data), '_0', UID);
let successInterval = setInterval(async function () {
isPortInUse(port, function (data) {
if (data && (data.inUse || data.ignore)) {
activeUID = {
[UID]: true
};
success = { status: 'OK', pid: child.pid };
}
});
if (callbackSent === 0 && success) {
callbackSent++;
await _updateProjects({ lastRunAt: currentDateTime() }, UID);
clearInterval(successInterval);
callback(null, success);
handleJobs(projectpath, jobs, envvariables, UID);
} else if (callbackSent > 0) {
clearInterval(successInterval);
if (callbackType === 'error' && child.pid) {
processKill(child.pid);
}
}
}, 1000);
});
child.stderr.on('data', (data) => {
let errMsg = convertBufferToArray(data);
let msgType = getErrorType(errMsg);
if (msgType === '_2') {
err = { code: errMsg.indexOf(PROJECT.Error.PACKAGE_REQUIRED) > -1 ? '#0410' : '#0422' };
}
let callBackTimer = setInterval(async function () {
if (success && callbackSent === 0) {
clearTimeout(callBackTimer);
callbackSent++;
await _updateProjects({ lastRunAt: currentDateTime() }, UID);
callback(null, success);
callbackType = 'success';
handleJobs(projectpath, jobs, envvariables, UID);
} else if (callBackCount > 2 && callbackSent === 0 && err) {
clearTimeout(callBackTimer);
callbackSent++;
callback(err);
callbackType = 'error';
} else if (callbackSent > 0) {
clearTimeout(callBackTimer);
} else {
callBackCount++;
}
}, 2000);
if (err && err.code === '#0410') {
//errMsg += PROJECT.INFO.PACKAGE_INSTALLATION;
}
writeLog(null, 500, errMsg, msgType, UID);
});
} else if (callbackSent === 0) {
callbackSent++;
callback({ code: '#0422' });
}
}
});
} else {
callback({ code: '#0412' });
}
};
const _stopProject = async (pid) => {
let { UID, jobPids } = await _projectFieldByKey('pid', parseInt(pid), ['UID', 'jobPids']);
processKill(pid);
if (activeUID[UID]) {
delete activeUID[UID];
}
let updatedObjects = { pid: 'N/A', cpu: 'N/A' };
if (jobPids && Array.isArray(jobPids) && jobPids.length > 0) {
for (let i = 0; i < jobPids.length; i++) {
if (jobPids[i].pid && parseInt(jobPids[i].pid) > 0) {
processKill(jobPids[i].pid);
writeLog(null, null, `Job - ${jobPids[i].name} is stopped.`, '_0', UID);
}
}
updatedObjects['jobPids'] = [];
}
await _updateProjects(updatedObjects, UID);
writeLog(null, null, PROJECT.INFO.PROJECT_STOPPED, '_0', UID);
return { status: 'OK' };
};
const _deleteProject = async (UID) => {
let projects = await _projects();
let indexVal;
projects.forEach((obj, index) => {
if (obj.UID === UID) {
indexVal = index;
}
});
if (indexVal >= 0) {
projects.splice(indexVal, 1);
}
fs.writeFileSync(PROJECTS_CONFIG_PATH, JSON.stringify(projects));
deleteProjectLogs(UID);
return { status: 'OK' };
};
const stopAllProjects = async () => {
let projects = await _projects();
if (projects && projects.length > 0) {
for (let i = 0; i < projects.length; i++) {
if (projects[i].pid && parseInt(projects[i].pid) > 0) {
await _stopProject(projects[i].pid);
}
}
}
};
const _projectJobs = (query) => {
let jobKeys = {};
for (const key in query) {
if (key.toString().indexOf('job-name') > -1 || key.toString().indexOf('job-path') > -1 || key.toString().indexOf('job-status') > -1) {
if (query[key] && query[key].length > 0) {
let fieldIndex = key.split('-')[2];
if (jobKeys[fieldIndex]) {
jobKeys[fieldIndex][key] = key.toString().indexOf('job-status') > -1 ? true : query[key];
} else {
jobKeys[fieldIndex] = { [key]: query[key] };
}
}
}
}
return jobKeys;
};
const _restartRunningPids = async (runningPids, callback) => {
let startedPids = 0;
let projects = await _projects();
for (let i = 0; i < runningPids.length; i++) {
_startProject(runningPids[i], (err, response) => {
startedPids++;
if (response && response.pid) {
if (isPidRunning(response.pid)) {
projects.forEach((obj) => {
if (obj.UID === runningPids[i]) {
obj.pid = response.pid;
}
});
}
}
if (startedPids === runningPids.length) {
callback('Ok');
fs.unlinkSync(path.resolve(CONFIG_PATH, 'runningPids.txt'));
fs.writeFileSync(PROJECTS_CONFIG_PATH, JSON.stringify(projects));
}
});
}
};
const _startProjectsOnBoot = async (callback) => {
let projects = await _projects();
let bootEnabledProjects = projects && projects.length > 0 ? projects.filter((project) => {
return project.bootstartopt;
}) : [];
if (bootEnabledProjects.length > 0) {
let processedCount = bootEnabledProjects.length;
for (let i = 0; i < bootEnabledProjects.length; i++) {
_startProject(bootEnabledProjects[i].UID, (err, response) => {
if (response && response.pid) {
if (isPidRunning(response.pid)) {
projects.forEach((obj) => {
if (obj.UID === bootEnabledProjects[i].UID) {
obj.pid = response.pid;
}
});
processedCount--;
if (processedCount === 0) {
fs.writeFileSync(PROJECTS_CONFIG_PATH, JSON.stringify(projects));
callback();
}
}
} else {
processedCount--;
if (processedCount === 0) {
fs.writeFileSync(PROJECTS_CONFIG_PATH, JSON.stringify(projects));
callback();
}
}
});
}
} else {
callback();
}
};
const getClonedProjectName = async (name) => {
let clonedName = name.indexOf('_copy_') > -1 ? name.split('_copy_')[0] : name;
return clonedName + '_copy_' + Math.floor(new Date().getTime() / 1000);
};
const _cloneProject = async (UID) => {
let projects = await _projects();
let projectInfo = {};
let projectToClone = projects.filter((project) => project.UID === UID);
if (projectToClone && projectToClone[0]) {
projectInfo = _projectFields(projectToClone[0]);
projectInfo.name = await getClonedProjectName(projectInfo.name);
projectInfo.jobPids = [];
projectInfo.UID = _hashFromName(projectInfo.name);
projectInfo.port = '';
projects.push(projectInfo);
}
fs.writeFileSync(PROJECTS_CONFIG_PATH, JSON.stringify(projects));
return projectInfo;
};
module.exports = {
_projectFields, _startProject, _hashFromName, _projects,
_updateProjects, _stopProject, _deleteProject, _projectInfo, stopAllProjects,
_projectJobs, _restartRunningPids, _startProjectsOnBoot,
_cloneProject
};