gpii-windows
Version:
Components of the GPII personalization infrastructure for use on Microsoft's "Windows" ™
489 lines (438 loc) • 18.3 kB
JavaScript
/* Manages the child processes.
*
* Copyright 2017 Raising the Floor - International
*
* Licensed under the New BSD license. You may not use this file except in
* compliance with this License.
*
* The R&D leading to these results received funding from the
* Department of Education - Grant H421A150005 (GPII-APCP). However,
* these results do not necessarily represent the policy of the
* Department of Education, and you should not assume endorsement by the
* Federal Government.
*
* You may obtain a copy of the License at
* https://github.com/GPII/universal/blob/master/LICENSE.txt
*/
;
var service = require("./service.js"),
ipc = require("./gpii-ipc.js"),
windows = require("./windows.js"),
winapi = require("./winapi.js");
var processHandling = {};
module.exports = processHandling;
/**
* Configuration of a child process (taken from service.json5)
* @typedef {Object} ProcessConfig
* @property {String} command The command.
* @property {String} key Identifier.
* @property {Boolean} autoRestart true to re-start the process if terminates.
* @property {String} ipc IPC channel name (optional).
* @property {Object} env Environment variables to set (optional).
* @property {String} currentDir The current dir (optional).
*/
/**
* A running child process
* @typedef {Object} ChildProcess
* @property {ProcessConfig} procConfig The process configuration.
* @property {Number} pid Process ID.
* @property {Array<Number>} lastStart When the process was started (used for restart-throttling)
* @property {Number} failureCount How many times this process has failed to start.
* @property {Boolean} shutdown true if shutting down.
* @property {String} creationTime A number representing the time the process started. Used to ensure the process
* identified by pid is the same process.
*/
/**
* @type {Array<ChildProcess>}
*/
processHandling.childProcesses = {};
/**
* The active console session has changed.
* @param {String} eventType The event type for the sessionChange event (see service.controlHandler()).
*/
processHandling.sessionChange = function (eventType) {
service.logDebug("session change", eventType);
switch (eventType) {
case "session-logon":
// User just logged on - start the processes.
processHandling.startChildProcesses();
break;
case "session-logoff":
// User just logged off - stop the processes (windows should have done this already).
processHandling.stopChildProcesses();
break;
}
};
/**
* Starts the configured processes.
*/
processHandling.startChildProcesses = function () {
var processes = Object.keys(service.config.processes);
// Start each child process sequentially.
var startNext = function () {
var key = processes.shift();
if (key && !service.config.processes[key].disabled) {
var proc = Object.assign({key: key}, service.config.processes[key]);
processHandling.startChildProcess(proc).then(startNext, function (err) {
service.logError("startChildProcess failed for " + key, err);
startNext();
});
}
};
startNext();
};
/**
* Starts a process.
*
* @param {ProcessConfig} procConfig The process configuration (from service-config.json).
* @return {Promise} Resolves (with the pid) when the process has started.
*/
processHandling.startChildProcess = function (procConfig) {
var childProcess = processHandling.childProcesses[procConfig.key];
service.log("Starting " + procConfig.key + ": " + (procConfig.command || "pipe only"));
service.logDebug("Process config: ", JSON.stringify(procConfig));
if (childProcess) {
if (processHandling.isProcessRunning(childProcess.pid, childProcess.creationTime)) {
service.logWarn("Process " + procConfig.key + " is already running");
return Promise.reject();
}
} else {
childProcess = {
procConfig: procConfig
};
processHandling.childProcesses[procConfig.key] = childProcess;
}
childProcess.pid = 0;
childProcess.pipe = null;
childProcess.lastStart = process.hrtime();
childProcess.shutdown = false;
var startOptions = {
env: procConfig.env,
currentDir: procConfig.currentDir,
authenticate: !procConfig.noAuth,
admin: procConfig.admin,
processKey: procConfig.key
};
var processPromise = null;
if (procConfig.ipc) {
startOptions.messaging = true;
// Start the process with a pipe.
processPromise = ipc.startProcess(procConfig.command, procConfig.ipc, startOptions).then(function (p) {
if (procConfig.command) {
childProcess.pid = p.pid;
childProcess.pipe = null;
childProcess.creationTime = processHandling.getProcessCreationTime(childProcess.pid);
}
return p.pid;
});
} else if (procConfig.command) {
// Start the process without a pipe.
childProcess.pid = ipc.execute(procConfig.command, startOptions);
childProcess.creationTime = processHandling.getProcessCreationTime(childProcess.pid);
processPromise = Promise.resolve(childProcess.pid);
}
return processPromise.then(function (pid) {
if (procConfig.command && procConfig.autoRestart) {
processHandling.autoRestartProcess(procConfig.key);
}
return pid;
});
};
/**
* Stops all child processes. This is performed when the service has been told to stop.
*/
processHandling.stopChildProcesses = function () {
service.log("Stopping processes");
var processKeys = Object.keys(processHandling.childProcesses);
processKeys.forEach(function (processKey) {
processHandling.stopChildProcess(processKey);
});
};
/**
* Stops a child process.
* @param {String} processKey Identifies the child process.
* @param {Boolean} [restart] Allow the process to be restarted, if configured.
*/
processHandling.stopChildProcess = function (processKey, restart) {
var childProcess = processHandling.childProcesses[processKey];
if (childProcess) {
service.log("Stopping " + processKey + ": " + childProcess.procConfig.command);
childProcess.shutdown = !restart;
if (processHandling.isProcessRunning(childProcess.pid, childProcess.creationTime)) {
try {
process.kill(childProcess.pid);
} catch (e) {
// Ignored.
}
} else {
service.logDebug("Process '" + processKey + "' is not running");
}
}
};
/**
* Set a running process to not restart. Called when the impending termination is intentional.
* @param {String} processKey Identifies the child process.
*/
processHandling.dontRestartProcess = function (processKey) {
var childProcess = processHandling.childProcesses[processKey];
if (childProcess) {
childProcess.shutdown = true;
}
};
/**
* Auto-restarts a child process when it terminates.
*
* @param {String} processKey Identifies the child process.
*/
processHandling.autoRestartProcess = function (processKey) {
var childProcess = processHandling.childProcesses[processKey];
processHandling.monitorProcess(childProcess.pid).then(function () {
service.log("Child process '" + processKey + "' died");
service.emit("process.stop", processKey);
if (childProcess.shutdown) {
service.log("Not restarting process (shutting down)");
} else {
var restart = true;
// Check if it's failing to start - if it's been running for less than 20 seconds.
var timespan = process.hrtime(childProcess.lastStart);
var seconds = timespan[0];
if (seconds > 20) {
childProcess.failureCount = 0;
} else {
service.logWarn("Process '" + processKey + "' failed at start.");
childProcess.failureCount = (childProcess.failureCount || 0) + 1;
if (childProcess.failureCount > 5) {
// Crashed at the start too many times.
service.logError("Unable to start process '" + processKey + "'");
restart = false;
}
}
if (restart) {
// Delay restart it.
var delay = processHandling.throttleRate(childProcess.failureCount);
service.logDebug("Restarting process '" + processKey + "' in " + Math.round(delay / 1000) + " seconds.");
setTimeout(processHandling.startChildProcess, delay, childProcess.procConfig);
}
}
});
};
/**
* Gets the number of milliseconds to delay a process restart.
*
* @param {Number} failureCount The number of times the process has failed to start.
* @return {Number} Returns 10 seconds for every failure count.
*/
processHandling.throttleRate = function (failureCount) {
return failureCount * 10000;
};
/**
* Determine if a process is running.
*
* To deal with PID re-use, provide creationTime (from a previous call to getProcessCreationTime) to also determine if
* the running process ID still refers to the original one at the time of the getProcessCreationTime call, and hasn't
* been re-used.
*
* @param {Number} pid The process ID.
* @param {String} creationTime [Optional] Numeric string representing the time the process started.
* @return {Boolean} true if the process is running, and has the same creation time (if provided).
*/
processHandling.isProcessRunning = function (pid, creationTime) {
var running = false;
if (pid > 0) {
try {
process.kill(pid, 0);
running = true;
} catch (e) {
// It's not running.
}
var newCreationTime = processHandling.getProcessCreationTime(pid);
if (running) {
if (creationTime && newCreationTime) {
// The pid is running, return true if it's the same process.
running = newCreationTime === creationTime;
} else {
running = !!newCreationTime;
}
} else {
if (newCreationTime) {
// The process isn't running, but the pid is still valid.
// This could mean CloseHandle hasn't been called (by this, or any other process).
service.logDebug("Possible process handle leak on pid " + pid);
}
}
}
return running;
};
/**
* Gets the time that the given process was started. This is used when determining if a pid still refers to the same
* process, due to the high level of pid re-use that Windows provides.
*
* The return value is intended to be compared to another call to this function, so the actual value (microseconds
* between 1601-01-01 and when the process started) isn't important.
*
* @param {Number} pid The process ID.
* @return {String} A numeric string, representing the time the process started - null if there's no such process.
*/
processHandling.getProcessCreationTime = function (pid) {
var creationTime = new winapi.FILETIME(),
exitTime = new winapi.FILETIME(),
kernelTime = new winapi.FILETIME(),
userTime = new winapi.FILETIME();
var success = false;
var processHandle = null;
try {
if (pid > 0) {
processHandle = winapi.kernel32.OpenProcess(winapi.constants.PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
}
if (processHandle === winapi.NULL) {
service.logDebug(winapi.errorText("OpenProcess", "NULL"));
} else if (processHandle) {
success = winapi.kernel32.GetProcessTimes(
processHandle, creationTime.ref(), exitTime.ref(), kernelTime.ref(), userTime.ref());
if (!success) {
service.logDebug(winapi.errorText("GetProcessTimes", success));
}
}
} finally {
if (processHandle) {
winapi.kernel32.CloseHandle(processHandle);
}
}
return success
? creationTime.ref().readUInt64LE()
: null;
};
// handle: { handle, pid, resolve, reject }
processHandling.monitoredProcesses = {};
// The last process to be monitored.
processHandling.lastProcess = null;
/**
* Resolves when the given process terminates.
*
* Using WaitForSingleObject is normally enough, but because calling that (using FFI's async method) creates a thread,
* and there will be several processes to wait upon, WaitForMultipleObjects is used instead.
*
* An event (https://msdn.microsoft.com/library/ms686915) will also be added to the things to wait for, so when another
* process is added to the monitoring list WaitForMultipleObjects can be restarted. (A nicer way would be to alert the
* thread, but the thread is handled by ffi+libuv).
*
* @param {Number} pid The process ID.
* @return {Promise} Resolves when the process identified by pid terminates.
*/
processHandling.monitorProcess = function (pid) {
return new Promise(function (resolve, reject) {
// Get the process handle.
var processHandle = winapi.kernel32.OpenProcess(winapi.constants.SYNCHRONIZE, 0, pid);
if (processHandle === winapi.NULL) {
reject(windows.win32Error("OpenProcess"));
}
processHandling.lastProcess = {
handle: processHandle,
pid: pid,
resolve: resolve,
reject: reject
};
// Add this process to the monitored list.
processHandling.monitoredProcesses[processHandle] = processHandling.lastProcess;
// (Re)start the waiting.
var event = processHandling.monitoredProcesses.event;
if (event) {
// Cause the current call to WaitForMultipleObjects to unblock, so the new process can also be monitored.
winapi.kernel32.SetEvent(event.handle);
} else {
// Create the event.
var eventHandle = winapi.kernel32.CreateEventW(winapi.NULL, false, false, winapi.NULL);
processHandling.monitoredProcesses.event = {
handle: eventHandle,
isEvent: true
};
processHandling.startWait();
}
});
};
/**
* Stops a monitored process from being monitored. The promises for the process will resolve with "removed".
*
* @param {Number|Object} process The process ID, or the object in processHandling.monitoredProcesses.
* @param {Boolean} removeOnly true to only remove it from the list of monitored processes.
*/
processHandling.unmonitorProcess = function (process, removeOnly) {
var resolves = [];
var pid = parseInt(process);
if (pid) {
for (var key in processHandling.monitoredProcesses) {
var proc = !isNaN(key) && processHandling.monitoredProcesses[key];
if (proc && proc.pid === pid) {
processHandling.unmonitorProcess(proc, removeOnly);
}
}
} else {
winapi.kernel32.CloseHandle(process.handle);
resolves.push(process.resolve);
delete processHandling.monitoredProcesses[process.handle];
if (!removeOnly) {
if (processHandling.monitoredProcesses.event) {
// Cause the current call to WaitForMultipleObjects to unblock to update the list.
winapi.kernel32.SetEvent(processHandling.monitoredProcesses.event.handle);
}
resolves.forEach(function (resolve) {
resolve("removed");
});
}
}
};
/**
* Performs the actual monitoring of the processes added by monitorProcess().
* Explained in processHandling.monitorProcess().
*/
processHandling.startWait = function () {
var handles = Object.keys(processHandling.monitoredProcesses).map(function (key) {
return processHandling.monitoredProcesses[key].handle;
});
if (handles.length <= 1) {
// Other than the event, there's nothing to wait for. Release the event and don't start the wait.
winapi.kernel32.CloseHandle(processHandling.monitoredProcesses.event.handle);
delete processHandling.monitoredProcesses.event;
} else {
// Wait for one or more of the handles (processes or the event) to do something.
windows.waitForMultipleObjects(handles).then(function (handle) {
var proc = processHandling.monitoredProcesses[handle] || processHandling.monitoredProcesses.event;
if (proc.isEvent) {
// The event was triggered to re-start waiting.
} else {
// Remove it from the list, and resolve.
processHandling.unmonitorProcess(proc, true);
proc.resolve(proc.pid);
}
// Start waiting again.
processHandling.startWait();
}, function (reason) {
// The wait failed - it could be due to the most recent one so reject+remove that one and try again.
var last = processHandling.lastProcess &&
processHandling.monitoredProcesses[processHandling.lastProcess.handle];
if (last) {
if (processHandling.lastProcess.reject) {
processHandling.unmonitorProcess(processHandling.monitoredProcesses[last.handle], true);
processHandling.lastProcess.reject(reason);
}
} else {
// Reject + remove all of them
Object.keys(processHandling.monitoredProcesses).forEach(function (proc) {
if (!proc.isEvent) {
processHandling.unmonitorProcess(proc, true);
if (proc.reject) {
proc.reject(reason);
}
}
});
}
processHandling.lastProcess = null;
// Try the wait again (or release the event).
processHandling.startWait();
});
}
};
// Listen for session change.
service.on("service.sessionchange", processHandling.sessionChange);
// Listen for service stop.
service.on("stop", processHandling.stopChildProcesses);