clexi
Version:
Node.js CLEXI is a lightweight client extension interface that enhances connected clients with functions of the underlying operating system using a duplex, realtime Websocket connection.
247 lines (234 loc) • 7.06 kB
JavaScript
const exec = require('child_process').exec;
const fs = require('fs');
/**
* An extension that supports certain runtime commands like 'reboot', 'shutdown' and custom calls.
*/
RuntimeCommands = function(onStartCallback, onEventCallback, onErrorCallback){
//Commands accessible by this extension
var availableCommands = {
shutdown: shutdown,
reboot: reboot,
freeMemory: freeMemory,
removeScheduled: removeScheduled,
callCustom: callCustom
}
var globalTimeout = 15000; //15s
var globalOptions = {
timeout: globalTimeout,
windowsHide: true
};
var delayedExecutions = {};
//Platform
function isWindows(){
return (process.platform === "win32");
}
function isMac(){
return (process.platform === "darwin");
}
function isLinux(){
return (process.platform === "linux");
}
//Shutdown OS
function shutdown(cmdId, arguments, successCallback, errorCallback){
var convertFun = function(res){ return res; };
var cmd;
var delay = arguments.delay || 5000;
//Linux
if (isLinux()){
cmd = "shutdown -h now"; //TODO: requires sudo
//Windows
}else if (isWindows()){
cmd = "shutdown /s"; //TODO: untested
}
callExecuteDelayed(cmdId, delay, cmd, convertFun, globalOptions, successCallback, errorCallback);
}
//Reboot OS
function reboot(cmdId, arguments, successCallback, errorCallback){
var convertFun = function(res){ return res; };
var cmd;
var delay = arguments.delay || 5000;
//Linux
if (isLinux()){
cmd = "reboot"; //TODO: requires sudo
//Windows
}else if (isWindows()){
cmd = "shutdown /r"; //TODO: untested
}
callExecuteDelayed(cmdId, delay, cmd, convertFun, globalOptions, successCallback, errorCallback);
}
//Get free memory
function freeMemory(cmdId, arguments, successCallback, errorCallback){
var convertFun;
var cmd;
//Linux
if (isLinux()){
cmd = "free -h | grep Mem: | awk '{print $7}'"; //typical answer: 158Mi
convertFun = function(res){
return res.replace("Mi", " MB").replace(/(\r\n|\r|\n)+/g, " ").replace(/\s+/g, " ").trim();
}
//Windows
}else if (isWindows()){
cmd = "wmic OS get FreePhysicalMemory /Value"; //typical answer: FreePhysicalMemory=4176328
convertFun = function(res){
res = parseInt(res.replace("FreePhysicalMemory=", "").replace(/(\r\n|\r|\n)+/g, " "));
return (res/1024 + " MB");
}
}
if (arguments.delay){
callExecuteDelayed(cmdId, arguments.delay, cmd, convertFun, globalOptions, successCallback, errorCallback);
}else{
callExecute(cmdId, cmd, convertFun, globalOptions, successCallback, errorCallback);
}
}
//Remove a delayed/scheduled command
function removeScheduled(cmdId, arguments, successCallback, errorCallback){
if (arguments.cmdId && delayedExecutions[arguments.cmdId]){
clearTimeout(delayedExecutions[arguments.cmdId]);
successCallback("done", 200);
}else{
successCallback("no cmd with ID was scheduled", 204);
}
}
//Call a script in default folder ../runtime_commands
function callCustom(cmdId, arguments, successCallback, errorCallback){
//check
if (!arguments || !arguments.file){
errorCallback("runtime command name empty", 404);
return;
}else{
//sanitize
arguments.file = arguments.file.split(".")[0].replace(/[^a-zA-Z0-9_-]/g, "");
//still ok?
if (!arguments.file){
errorCallback("runtime command name invalid", 404);
return;
}
}
//build script path
var path = "runtime_commands/" + arguments.file;
if (isLinux()){
//.linux
path += ".linux";
}else if (isWindows()){
//.windows
path += ".windows";
}else if (isMac()){
//.mac
path += ".mac";
}
//check if script exists
var cmd;
if (arguments.file && fs.existsSync(path)){
cmd = fs.readFileSync(path, 'utf8'); //the content is supposed to be small so we use sync.
}
if (cmd){
//check variables
var variables = cmd.match(/\${.*?}/g);
if (variables && variables.length > 0){
for (var i=0; i<variables.length; i++){
var k = variables[i].substring(2, variables[i].length-1);
cmd = cmd.replace(new RegExp("\\${" + k + "}", "g"), "'" + arguments[k] + "'");
}
}
var convertFun = function(res){ return res.replace(/(\r\n|\r|\n)+$/, "").trim(); }; //no conversion just remove last line-break
if (arguments.delay){
callExecuteDelayed(cmdId, arguments.delay, cmd, convertFun, globalOptions, successCallback, errorCallback);
}else{
callExecute(cmdId, cmd, convertFun, globalOptions, successCallback, errorCallback);
}
//not found
}else{
errorCallback("runtime command with path '" + path + "' not found or empty", 404);
}
}
//----------------------------------------------
//Execute runtime commands
function executeCommand(cmd, cmdId, arguments){
var fun = availableCommands[cmd];
if (!fun){
if (onErrorCallback) onErrorCallback({
error: {
msg: "command not found",
code: 404,
cmd: cmd,
cmdId: cmdId
}
});
return
}else{
//Success
var successCallback = function(result, code){
//console.log(`result: ${result}`); //DEBUG
if (onEventCallback) onEventCallback({
data: {
result: result,
code: code || 200,
cmd: cmd,
cmdId: cmdId
}
});
}
//Fail
var errorCallback = function(err, code){
//console.error(`error: ${err}`); //DEBUG
if (onErrorCallback) onErrorCallback({
error: {
msg: err,
code: code || 500,
cmd: cmd,
cmdId: cmdId
}
});
}
try{
fun(cmdId, arguments, successCallback, errorCallback);
}catch (err){
//Error
if (onErrorCallback) onErrorCallback({
error: {
msg: err,
code: 500,
cmd: cmd,
cmdId: cmdId
}
});
}
}
}
function callExecute(cmdId, cmd, convertFun, globalOptions, successCallback, errorCallback){
if (delayedExecutions[cmdId]) clearTimeout(delayedExecutions[cmdId]);
if (cmd && convertFun){
exec(cmd, globalOptions, function(error, stdout, stderr){
var err = error || stderr;
if (err){
if (errorCallback) errorCallback(err);
}else{
if (successCallback) successCallback(convertFun(stdout));
}
});
}else{
if (errorCallback) errorCallback("not yet supported by this OS", 501);
}
}
function callExecuteDelayed(cmdId, delay, cmd, convertFun, globalOptions, successCallback, errorCallback){
if (successCallback) successCallback("request sent", 202);
if (delayedExecutions[cmdId]) clearTimeout(delayedExecutions[cmdId]);
delayedExecutions[cmdId] = setTimeout(callExecute, delay, cmdId, cmd, convertFun, globalOptions, successCallback, errorCallback);
}
//Input
this.input = function(msg, socket){
//console.log(JSON.stringify(msg, null, ' '));
var cmdData = msg.data;
if (cmdData && cmdData.id && cmdData.cmd){
executeCommand(cmdData.cmd, cmdData.id, cmdData.args);
return "sent";
}else{
return "sent but invalid";
}
}
//On start
if (onStartCallback) onStartCallback({
msg: "Runtime-Commands initialized."
});
};
module.exports = RuntimeCommands;