overte-HifiExperiments/server-console/src/modules/hf-process.js

379 lines
12 KiB
JavaScript

'use strict'
const request = require('request');
const extend = require('extend');
const util = require('util');
const events = require('events');
const childProcess = require('child_process');
const fs = require('fs-extra');
const os = require('os');
const path = require('path');
const ProcessGroupStates = {
STOPPED: 'stopped',
STARTED: 'started',
STOPPING: 'stopping'
};
const ProcessStates = {
STOPPED: 'stopped',
STARTED: 'started',
STOPPING: 'stopping'
};
function ProcessGroup(name, processes) {
events.EventEmitter.call(this);
this.name = name;
this.state = ProcessGroupStates.STOPPED;
this.processes = [];
this.restarting = false;
for (let process of processes) {
this.addProcess(process);
}
};
util.inherits(ProcessGroup, events.EventEmitter);
ProcessGroup.prototype = extend(ProcessGroup.prototype, {
addProcess: function(process) {
this.processes.push(process);
process.on('state-update', this.onProcessStateUpdate.bind(this));
},
start: function() {
if (this.state != ProcessGroupStates.STOPPED) {
log.warn("Can't start process group that is not stopped.");
return;
}
for (let process of this.processes) {
process.start();
}
this.state = ProcessGroupStates.STARTED;
this.emit('state-update', this);
},
stop: function() {
if (this.state != ProcessGroupStates.STARTED) {
log.warn("Can't stop process group that is not started.");
return;
}
for (let process of this.processes) {
process.stop();
}
this.state = ProcessGroupStates.STOPPING;
this.emit('state-update', this);
},
restart: function() {
if (this.state == ProcessGroupStates.STOPPED) {
// start the group, we were already stopped
this.start();
} else {
// set our restart flag so the group will restart once stopped
this.restarting = true;
// call stop, that will put them in the stopping state
this.stop();
}
},
// Event handlers
onProcessStateUpdate: function(process) {
var processesStillRunning = false;
for (let process of this.processes) {
if (process.state != ProcessStates.STOPPED) {
processesStillRunning = true;
break;
}
}
if (!processesStillRunning) {
this.state = ProcessGroupStates.STOPPED;
this.emit('state-update', this);
// if we we're supposed to restart, call start now and reset the flag
if (this.restarting) {
this.start();
this.restarting = false;
}
}
this.emit('process-update', process);
}
});
var ID = 0;
function Process(name, command, commandArgs, logDirectory) {
events.EventEmitter.call(this);
this.id = ++ID;
this.name = name;
this.command = command;
this.commandArgs = commandArgs ? commandArgs : [];
this.child = null;
this.logDirectory = logDirectory;
this.logStdout = null;
this.logStderr = null;
this.detached = false;
this.restartOnCrash = false;
this.restartCount = 0;
this.firstRestartTimestamp = Date.now();
this.state = ProcessStates.STOPPED;
};
util.inherits(Process, events.EventEmitter);
Process.prototype = extend(Process.prototype, {
start: function() {
if (this.state != ProcessStates.STOPPED) {
log.warn("Can't start process that is not stopped.");
return;
}
log.debug("Starting " + this.command + " " + this.commandArgs.join(' '));
var logStdout = 'ignore',
logStderr = 'ignore';
if (this.logDirectory) {
var logDirectoryCreated = false;
try {
fs.mkdirsSync(this.logDirectory);
logDirectoryCreated = true;
} catch (e) {
if (e.code == 'EEXIST') {
logDirectoryCreated = true;
} else {
log.error("Error creating log directory");
}
}
if (logDirectoryCreated) {
// Create a temporary file with the current time
var time = (new Date).getTime();
var tmpLogStdout = path.resolve(this.logDirectory + '/' + this.name + '-' + time + '-stdout.txt');
var tmpLogStderr = path.resolve(this.logDirectory + '/' + this.name + '-' + time + '-stderr.txt');
try {
logStdout = fs.openSync(tmpLogStdout, 'ax');
} catch(e) {
log.debug("Error creating stdout log file", e);
logStdout = 'ignore';
}
try {
logStderr = fs.openSync(tmpLogStderr, 'ax');
} catch(e) {
log.debug("Error creating stderr log file", e);
logStderr = 'ignore';
}
}
}
try {
this.child = childProcess.spawn(this.command, this.commandArgs, {
detached: this.detached,
stdio: ['ignore', logStdout, logStderr]
});
log.debug("Spawned " + this.command + " with pid " + this.child.pid);
} catch (e) {
log.debug("Got error starting child process for " + this.name, e);
this.child = null;
this.updateState(ProcessStates.STOPPED);
return;
}
if (logStdout != 'ignore') {
var pidLogStdout = path.resolve(this.logDirectory + '/' + this.name + "-" + this.child.pid + "-" + time + "-stdout.txt");
fs.rename(tmpLogStdout, pidLogStdout, function(e) {
if (e !== null) {
log.debug("Error renaming log file from " + tmpLogStdout + " to " + pidLogStdout, e);
}
});
this.logStdout = pidLogStdout;
fs.closeSync(logStdout);
}
if (logStderr != 'ignore') {
var pidLogStderr = path.resolve(this.logDirectory + '/' + this.name + "-" + this.child.pid + "-" + time + "-stderr.txt");
fs.rename(tmpLogStderr, pidLogStderr, function(e) {
if (e !== null) {
log.debug("Error renaming log file from " + tmpLogStdout + " to " + pidLogStdout, e);
}
});
this.logStderr = pidLogStderr;
fs.closeSync(logStderr);
}
this.child.on('error', this.onChildStartError.bind(this));
this.child.on('close', this.onChildClose.bind(this));
log.debug("Child process started");
this.updateState(ProcessStates.STARTED);
this.emit('logs-updated');
},
stop: function(force) {
if (this.state == ProcessStates.STOPPED) {
log.warn("Can't stop process that is not started or stopping.");
return;
}
if (os.type() == "Windows_NT") {
var command = "taskkill /pid " + this.child.pid;
if (force) {
command += " /f /t";
}
childProcess.exec(command, {}, function(error) {
if (error) {
log.error('Error executing taskkill:', error);
}
});
} else {
var signal = force ? 'SIGKILL' : 'SIGTERM';
this.child.kill(signal);
}
log.debug("Stopping child process:", this.child.pid, this.name);
if (!force) {
this.stoppingTimeoutID = setTimeout(function() {
if (this.state == ProcessStates.STOPPING) {
log.debug("Force killling", this.name, this.child.pid);
this.stop(true);
}
}.bind(this), 2500);
}
this.updateState(ProcessStates.STOPPING);
},
updateState: function(newState) {
if (this.state != newState) {
this.state = newState;
this.emit('state-update', this);
return true;
}
return false;
},
getLogs: function() {
var logs = {};
logs[this.child.pid] = {
stdout: this.logStdout == 'ignore' ? null : this.logStdout,
stderr: this.logStderr == 'ignore' ? null : this.logStderr
};
return logs;
},
// Events
onChildStartError: function(error) {
log.debug("Child process error ", error);
this.updateState(ProcessStates.STOPPED);
},
onChildClose: function(code) {
log.debug("Child process closed with code ", code, this.name);
if (this.stoppingTimeoutID) {
clearTimeout(this.stoppingTimeoutID);
this.stoppingTimeoutID = null;
}
// Grab current state before updating it.
var unexpectedShutdown = this.state != ProcessStates.STOPPING;
this.updateState(ProcessStates.STOPPED);
if (unexpectedShutdown && this.restartOnCrash) {
var MAX_RESTARTS = 10;
var MAX_RESTARTS_PERIOD = 10; // 10 min
var MSEC_PER_MIN = 1000 * 60;
var now = Date.now();
var timeDiff = (now - this.firstRestartTimestamp) / MSEC_PER_MIN;
if (timeDiff > MAX_RESTARTS_PERIOD) {
this.firstRestartTimestamp = now;
this.restartCount = 0;
}
if (this.restartCount < 10) {
this.restartCount++;
log.warn("Child stopped unexpectedly, restarting.");
this.start();
} else {
log.warn("Child stopped unexpectedly too many times, not restarting.");
}
}
}
});
// ACMonitorProcess is an extension of Process that keeps track of the AC Montior's
// children status and log locations.
const CHECK_AC_STATUS_INTERVAL = 1000;
function ACMonitorProcess(name, path, args, httpStatusPort, logPath) {
Process.call(this, name, path, args, logPath);
this.httpStatusPort = httpStatusPort;
this.requestTimeoutID = null;
this.pendingRequest = null;
this.childServers = {};
};
util.inherits(ACMonitorProcess, Process);
ACMonitorProcess.prototype = extend(ACMonitorProcess.prototype, {
updateState: function(newState) {
if (ACMonitorProcess.super_.prototype.updateState.call(this, newState)) {
if (this.state == ProcessStates.STARTED) {
this._updateACMonitorStatus();
} else {
if (this.requestTimeoutID) {
clearTimeout(this.requestTimeoutID);
this.requestTimeoutID = null;
}
if (this.pendingRequest) {
this.pendingRequest.destroy();
this.pendingRequest = null;
}
}
}
},
getLogs: function() {
var logs = {};
logs[this.child.pid] = {
stdout: this.logStdout == 'ignore' ? null : this.logStdout,
stderr: this.logStderr == 'ignore' ? null : this.logStderr
};
for (var pid in this.childServers) {
logs[pid] = {
stdout: this.childServers[pid].logStdout,
stderr: this.childServers[pid].logStderr
}
}
return logs;
},
_updateACMonitorStatus: function() {
if (this.state != ProcessStates.STARTED) {
return;
}
// If there is a pending request, return
if (this.pendingRequest) {
return;
}
var options = {
url: "http://localhost:" + this.httpStatusPort + "/status",
json: true
};
this.pendingRequest = request(options, function(error, response, body) {
this.pendingRequest = null;
if (error) {
log.error('ERROR Getting AC Monitor status', error);
} else {
this.childServers = body.servers;
}
this.emit('logs-updated');
this.requestTimeoutID = setTimeout(this._updateACMonitorStatus.bind(this), CHECK_AC_STATUS_INTERVAL);
}.bind(this));
}
});
module.exports.Process = Process;
module.exports.ACMonitorProcess = ACMonitorProcess;
module.exports.ProcessGroup = ProcessGroup;
module.exports.ProcessGroupStates = ProcessGroupStates;
module.exports.ProcessStates = ProcessStates;