From 75a9ce1ce097a2cf49c7a303c57e75be3f660c98 Mon Sep 17 00:00:00 2001 From: panreyes Date: Fri, 24 Oct 2025 21:11:31 +0200 Subject: [PATCH 01/11] Corrected typo to avoid being able to create a job from a folder --- views/user.handlebars | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/views/user.handlebars b/views/user.handlebars index f69a731..058d140 100644 --- a/views/user.handlebars +++ b/views/user.handlebars @@ -359,7 +359,7 @@ function goRun() { var selScript = document.querySelectorAll('.liselected'); if (selScript.length) { var scriptId = selScript[0].getAttribute('x-data-id'); - if (scriptId == selScript[0].getAttribute('x-folder-id')) + if (scriptId == selScript[0].getAttribute('x-data-folder')) { parent.setDialogMode(2, "Oops!", 1, null, 'Please select a script. A folder is currently selected.'); } @@ -1020,3 +1020,4 @@ function redrawScriptTree() { + From 425412e27452a309105a9bc03452cc5e6bda4aa4 Mon Sep 17 00:00:00 2001 From: panreyes Date: Fri, 24 Oct 2025 21:16:27 +0200 Subject: [PATCH 02/11] Add suffix 'tmp_st_' to script file names --- modules_meshcore/scripttask.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/modules_meshcore/scripttask.js b/modules_meshcore/scripttask.js index 3143a4a..feab779 100644 --- a/modules_meshcore/scripttask.js +++ b/modules_meshcore/scripttask.js @@ -150,8 +150,8 @@ function runPowerShell(sObj, jObj) { const fs = require('fs'); var rand = Math.random().toString(32).replace('0.', ''); - var oName = 'st' + rand + '.txt'; - var pName = 'st' + rand + '.ps1'; + var oName = 'tmp_st_' + rand + '.txt'; + var pName = 'tmp_st_' + rand + '.ps1'; var pwshout = '', pwsherr = '', cancontinue = false; try { fs.writeFileSync(pName, sObj.content); @@ -221,8 +221,8 @@ function runPowerShellNonWin(sObj, jObj) { dbg('Path chosen is: ' + path); path = path + '/'; - var oName = 'st' + rand + '.txt'; - var pName = 'st' + rand + '.ps1'; + var oName = 'tmp_st_' + rand + '.txt'; + var pName = 'tmp_st_' + rand + '.ps1'; var pwshout = '', pwsherr = '', cancontinue = false; try { var childp = require('child_process').execFile('/bin/sh', ['sh']); @@ -298,8 +298,8 @@ function runBat(sObj, jObj) { } const fs = require('fs'); var rand = Math.random().toString(32).replace('0.', ''); - var oName = 'st' + rand + '.txt'; - var pName = 'st' + rand + '.bat'; + var oName = 'tmp_st_' + rand + '.txt'; + var pName = 'tmp_st_' + rand + '.bat'; try { fs.writeFileSync(pName, sObj.content); var outstr = '', errstr = ''; @@ -372,8 +372,8 @@ function runBash(sObj, jObj) { //child.execFile(process.env['windir'] + '\\system32\\cmd.exe', ['/c', 'RunDll32.exe user32.dll,LockWorkStation'], { type: 1 }); var rand = Math.random().toString(32).replace('0.', ''); - var oName = 'st' + rand + '.txt'; - var pName = 'st' + rand + '.sh'; + var oName = 'tmp_st_' + rand + '.txt'; + var pName = 'tmp_st_' + rand + '.sh'; try { fs.writeFileSync(path + pName, sObj.content); var outstr = '', errstr = ''; From 1ab23c3096c31ee6e0fd8eebb5d1282738a4feff Mon Sep 17 00:00:00 2001 From: panreyes Date: Fri, 24 Oct 2025 21:22:35 +0200 Subject: [PATCH 03/11] Download script: Added script type and TXT extensions This helps to: - Identify script types easier - Avoid issues with browsers and antivirus --- scripttask.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripttask.js b/scripttask.js index 609e720..b3c7f88 100644 --- a/scripttask.js +++ b/scripttask.js @@ -137,7 +137,7 @@ module.exports.scripttask = function (parent) { .then(found => { if (found.length != 1) { res.sendStatus(401); return; } var file = found[0]; - res.setHeader('Content-disposition', 'attachment; filename=' + file.name); + res.setHeader('Content-disposition', 'attachment; filename=' + file.name + "." + file.filetype + ".txt"); res.setHeader('Content-type', 'text/plain'); //var fs = require('fs'); res.send(file.content); From 7c777f37f26ae36d41743d016798c73bd0126e77 Mon Sep 17 00:00:00 2001 From: panreyes Date: Tue, 31 Mar 2026 00:23:50 +0200 Subject: [PATCH 04/11] Added support for MariaDB (vibe coded and still very JSON, but it works!) --- db.js | 20 ++++ nemariadb.js | 313 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 333 insertions(+) create mode 100644 nemariadb.js diff --git a/db.js b/db.js index 1d2cc60..34eb5d1 100644 --- a/db.js +++ b/db.js @@ -285,6 +285,26 @@ module.exports.CreateDB = function(meshserver) { } obj.initFunctions(); }); + } else if (meshserver.args.mariadb) { // use MariaDB + var mariadb = null; + try { mariadb = require('mariadb'); } catch (e) { console.log('PLUGIN: ScriptTask: mariadb module is required but not found.'); } + if (mariadb != null) { + var NEMariaDB = require(__dirname + '/nemariadb.js'); + var m_options = meshserver.args.mariadb; + if (typeof m_options === 'string') { + try { + const urlToConfig = require('mariadb/lib/misc/url-to-config.js'); + m_options = urlToConfig(m_options); + } catch (e) { + // Fallback to letting createPool parse it directly if supported + } + } + if (meshserver.args.mariadbname && typeof m_options === 'object') m_options.database = meshserver.args.mariadbname; + var pool = mariadb.createPool(m_options); + obj.scriptFile = new NEMariaDB(pool); + formatId = function(id) { return id; }; + obj.initFunctions(); + } } else { // use NeDb try { Datastore = require('@seald-io/nedb'); } catch (ex) { } // This is the NeDB with Node 23 support. if (Datastore == null) { diff --git a/nemariadb.js b/nemariadb.js new file mode 100644 index 0000000..a7ac315 --- /dev/null +++ b/nemariadb.js @@ -0,0 +1,313 @@ +/** +* @description MeshCentral database abstraction layer for MariaDB to be more Mongo-like +* @author Ryan Blenis +* @copyright +* @license Apache-2.0 +* This is a simple abstraction layer for many commonly used DB calls. +* It supplements the need to duplicate and modify all calls in the db.js file. +*/ + +class NEMariaDB { + constructor(pool) { + this.pool = pool; + this._find = null; + this._proj = null; + this._limit = null; + this._sort = null; + + // initialize table + this._initDB(); + + return this; + } + + _initDB() { + this.pool.query("CREATE TABLE IF NOT EXISTS plugin_scripttask (id VARCHAR(128) PRIMARY KEY, doc JSON)") + .then(() => { + // optionally create indexes on JSON fields in MariaDB if needed for speed + }) + .catch(err => { + console.log("PLUGIN: ScriptTask: Error creating database table", err); + }); + } + + _escape(val) { + if (typeof val === 'string') { + return "'" + val.replace(/'/g, "''").replace(/\\/g, "\\\\") + "'"; + } + if (typeof val === 'number') return val; + if (val === null) return "NULL"; + if (typeof val === 'boolean') return val ? 'TRUE' : 'FALSE'; + return "'" + JSON.stringify(val).replace(/'/g, "''").replace(/\\/g, "\\\\") + "'"; + } + + _buildWhere(filter) { + if (!filter || Object.keys(filter).length === 0) return "1=1"; + + var conditions = []; + for (var key in filter) { + if (key === '$or') { + var orConds = []; + for (var i in filter.$or) { + orConds.push("(" + this._buildWhere(filter.$or[i]) + ")"); + } + conditions.push("(" + orConds.join(" OR ") + ")"); + } else if (key === '$and') { + var andConds = []; + for (var i in filter.$and) { + andConds.push("(" + this._buildWhere(filter.$and[i]) + ")"); + } + conditions.push("(" + andConds.join(" AND ") + ")"); + } else { + var val = filter[key]; + var dbKey = key === '_id' ? 'id' : `JSON_UNQUOTE(JSON_EXTRACT(doc, '$.${key}'))`; + + if (val !== null && typeof val === 'object' && !Array.isArray(val)) { + // special operator + for (var op in val) { + if (op === '$in') { + var inList = val.$in.map(v => this._escape(v)).join(","); + conditions.push(`${dbKey} IN (${inList})`); + } else if (op === '$gte') { + conditions.push(`${dbKey} >= ${val.$gte}`); + } else if (op === '$lte') { + conditions.push(`${dbKey} <= ${val.$lte}`); + } else if (op === '$gt') { + conditions.push(`${dbKey} > ${val.$gt}`); + } else if (op === '$lt') { + conditions.push(`${dbKey} < ${val.$lt}`); + } + } + } else if (val === null) { + conditions.push(`(${dbKey} IS NULL OR ${dbKey} = 'null')`); + } else { + conditions.push(`${dbKey} = ${this._escape(val)}`); + } + } + } + return conditions.join(" AND "); + } + + find(args, proj) { + this._find = args; + this._proj = proj; + this._sort = null; + this._limit = null; + return this; + } + + project(args) { + this._proj = args; + return this; + } + + sort(args) { + this._sort = args; + return this; + } + + limit(limit) { + this._limit = limit; + return this; + } + + toArray(callback) { + var self = this; + return new Promise(function(resolve, reject) { + var where = self._buildWhere(self._find); + var query = `SELECT doc FROM plugin_scripttask WHERE ${where}`; + + if (self._sort) { + var order = []; + for (var key in self._sort) { + var dir = self._sort[key] === -1 ? "DESC" : "ASC"; + if (key === '_id') order.push(`id ${dir}`); + else order.push(`JSON_UNQUOTE(JSON_EXTRACT(doc, '$.${key}')) ${dir}`); + } + if (order.length > 0) query += " ORDER BY " + order.join(", "); + } + if (self._limit) { + query += ` LIMIT ${self._limit}`; + } + + self.pool.query(query) + .then(rows => { + var docs = []; + for (var i = 0; i < rows.length; i++) { + var doc = typeof rows[i].doc === 'string' ? JSON.parse(rows[i].doc) : rows[i].doc; + + if (self._proj) { + var pDoc = {}; + var keepFields = []; + var excludeFields = []; + for (var p in self._proj) { + if (self._proj[p] === 1) keepFields.push(p); + else if (self._proj[p] === 0) excludeFields.push(p); + } + if (keepFields.length > 0) { + for (var k of keepFields) { + if (doc[k] !== undefined) pDoc[k] = doc[k]; + } + if (excludeFields.indexOf('_id') === -1) pDoc._id = doc._id; + docs.push(pDoc); + } else { + for (var k of excludeFields) delete doc[k]; + docs.push(doc); + } + } else { + docs.push(doc); + } + } + if (callback != null && typeof callback == 'function') callback(null, docs); + resolve(docs); + }) + .catch(err => { + if (callback != null && typeof callback == 'function') callback(err, null); + reject(err); + }); + }); + } + + insertOne(args, options) { + var self = this; + return new Promise(function(resolve, reject) { + var id = args._id; + if (!id) { + // Generate a random 24 char hex id similar to Mongo + id = require('crypto').randomBytes(12).toString('hex'); + args._id = id; + } + var docStr = JSON.stringify(args); + self.pool.query("INSERT INTO plugin_scripttask (id, doc) VALUES (?, ?)", [id, docStr]) + .then(res => { + resolve({ insertedId: id }); + }) + .catch(err => reject(err)); + }); + } + + deleteOne(filter, options) { + var self = this; + var where = self._buildWhere(filter); + return new Promise(function(resolve, reject) { + self.pool.query(`DELETE FROM plugin_scripttask WHERE ${where} LIMIT 1`) + .then(res => resolve({ deletedCount: res.affectedRows })) + .catch(err => reject(err)); + }); + } + + deleteMany(filter, options) { + var self = this; + var where = self._buildWhere(filter); + return new Promise(function(resolve, reject) { + self.pool.query(`DELETE FROM plugin_scripttask WHERE ${where}`) + .then(res => resolve({ deletedCount: res.affectedRows })) + .catch(err => reject(err)); + }); + } + + updateOne(filter, update, options) { + var self = this; + var where = self._buildWhere(filter); + if (options == null) options = {}; + if (options.upsert == null) options.upsert = false; + + return new Promise(function(resolve, reject) { + self.pool.query(`SELECT id, doc FROM plugin_scripttask WHERE ${where} LIMIT 1`) + .then(rows => { + if (rows.length === 0) { + if (options.upsert) { + var newDoc = { ...filter }; + if (update.$set) newDoc = { ...newDoc, ...update.$set }; + return self.insertOne(newDoc).then(res => resolve({ matchedCount: 0, modifiedCount: 1, upsertedId: res.insertedId })); + } + return resolve({ matchedCount: 0, modifiedCount: 0 }); + } + + var id = rows[0].id; + var doc = typeof rows[0].doc === 'string' ? JSON.parse(rows[0].doc) : rows[0].doc; + var modifiedFields = 0; + + if (update.$set) { + for (var k in update.$set) { + doc[k] = update.$set[k]; + } + modifiedFields = 1; + } else { + doc = { ...doc, ...update }; + if (!doc._id) doc._id = id; + modifiedFields = 1; + } + + if (modifiedFields) { + var docStr = JSON.stringify(doc); + return self.pool.query("UPDATE plugin_scripttask SET doc = ? WHERE id = ?", [docStr, id]) + .then(res => resolve({ matchedCount: 1, modifiedCount: 1, upsertedId: id })); + } else { + return resolve({ matchedCount: 1, modifiedCount: 0 }); + } + }) + .catch(err => reject(err)); + }); + } + + updateMany(filter, update, options) { + var self = this; + var where = self._buildWhere(filter); + if (options == null) options = {}; + if (options.upsert == null) options.upsert = false; + + return new Promise(function(resolve, reject) { + self.pool.query(`SELECT id, doc FROM plugin_scripttask WHERE ${where}`) + .then(rows => { + if (rows.length === 0) { + if (options.upsert) { + // Upsert logic for updateMany doesn't normally generate multiple + var newDoc = { ...filter }; + if (update.$set) newDoc = { ...newDoc, ...update.$set }; + return self.insertOne(newDoc).then(res => resolve({ matchedCount: 0, modifiedCount: 1, upsertedId: res.insertedId })); + } + return resolve({ matchedCount: 0, modifiedCount: 0 }); + } + + var updates = []; + for (var i = 0; i < rows.length; i++) { + var id = rows[i].id; + var doc = typeof rows[i].doc === 'string' ? JSON.parse(rows[i].doc) : rows[i].doc; + + if (update.$set) { + for (var k in update.$set) { + doc[k] = update.$set[k]; + } + } else { + doc = { ...doc, ...update }; + if (!doc._id) doc._id = id; + } + var docStr = JSON.stringify(doc); + updates.push(self.pool.query("UPDATE plugin_scripttask SET doc = ? WHERE id = ?", [docStr, id])); + } + + if (updates.length > 0) { + return Promise.all(updates).then(() => resolve({ matchedCount: rows.length, modifiedCount: rows.length })); + } else { + return resolve({ matchedCount: rows.length, modifiedCount: 0 }); + } + }) + .catch(err => reject(err)); + }); + } + + indexes(callback) { + if (callback != null && typeof callback == 'function') callback(null, []); + } + + dropIndexes(callback) { + if (callback != null && typeof callback == 'function') callback(null); + } + + createIndex(args, options) { + // Ignored for JSON DB adapter for now + } +} + +module.exports = NEMariaDB; From fc7317f3ed00a5e3a1b28a3aea045e0991ee792f Mon Sep 17 00:00:00 2001 From: panreyes Date: Tue, 31 Mar 2026 00:48:58 +0200 Subject: [PATCH 05/11] Jobs are now on their own table, so it will be easier to relate them. --- nemariadb.js | 454 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 296 insertions(+), 158 deletions(-) diff --git a/nemariadb.js b/nemariadb.js index a7ac315..298e915 100644 --- a/nemariadb.js +++ b/nemariadb.js @@ -1,10 +1,10 @@ /** -* @description MeshCentral database abstraction layer for MariaDB to be more Mongo-like +* @description MeshCentral database abstraction layer for MariaDB * @author Ryan Blenis * @copyright * @license Apache-2.0 * This is a simple abstraction layer for many commonly used DB calls. -* It supplements the need to duplicate and modify all calls in the db.js file. +* It routes requests between the legacy JSON table and a dedicated Jobs table. */ class NEMariaDB { @@ -15,7 +15,7 @@ class NEMariaDB { this._limit = null; this._sort = null; - // initialize table + // initialize tables this._initDB(); return this; @@ -23,12 +23,10 @@ class NEMariaDB { _initDB() { this.pool.query("CREATE TABLE IF NOT EXISTS plugin_scripttask (id VARCHAR(128) PRIMARY KEY, doc JSON)") - .then(() => { - // optionally create indexes on JSON fields in MariaDB if needed for speed - }) - .catch(err => { - console.log("PLUGIN: ScriptTask: Error creating database table", err); - }); + .catch(err => { console.log("PLUGIN: ScriptTask: Error creating database table", err); }); + + this.pool.query("CREATE TABLE IF NOT EXISTS plugin_scripttask_jobs (id VARCHAR(128) PRIMARY KEY, type VARCHAR(64) DEFAULT 'job', queueTime BIGINT, dontQueueUntil BIGINT, dispatchTime BIGINT, completeTime BIGINT, node VARCHAR(256), scriptId VARCHAR(128), scriptName VARCHAR(512), replaceVars JSON, returnVal TEXT, errorVal TEXT, returnAct VARCHAR(256), runBy VARCHAR(256), jobSchedule VARCHAR(128))") + .catch(err => { console.log("PLUGIN: ScriptTask: Error creating jobs table", err); }); } _escape(val) { @@ -41,33 +39,31 @@ class NEMariaDB { return "'" + JSON.stringify(val).replace(/'/g, "''").replace(/\\/g, "\\\\") + "'"; } - _buildWhere(filter) { + _buildWhereDoc(filter) { if (!filter || Object.keys(filter).length === 0) return "1=1"; - var conditions = []; for (var key in filter) { if (key === '$or') { var orConds = []; - for (var i in filter.$or) { - orConds.push("(" + this._buildWhere(filter.$or[i]) + ")"); - } + for (var i in filter.$or) orConds.push("(" + this._buildWhereDoc(filter.$or[i]) + ")"); conditions.push("(" + orConds.join(" OR ") + ")"); } else if (key === '$and') { var andConds = []; - for (var i in filter.$and) { - andConds.push("(" + this._buildWhere(filter.$and[i]) + ")"); - } + for (var i in filter.$and) andConds.push("(" + this._buildWhereDoc(filter.$and[i]) + ")"); conditions.push("(" + andConds.join(" AND ") + ")"); } else { var val = filter[key]; var dbKey = key === '_id' ? 'id' : `JSON_UNQUOTE(JSON_EXTRACT(doc, '$.${key}'))`; if (val !== null && typeof val === 'object' && !Array.isArray(val)) { - // special operator for (var op in val) { if (op === '$in') { - var inList = val.$in.map(v => this._escape(v)).join(","); - conditions.push(`${dbKey} IN (${inList})`); + if (val.$in.length === 0) { + conditions.push('1=0'); + } else { + var inList = val.$in.map(v => this._escape(v)).join(","); + conditions.push(`${dbKey} IN (${inList})`); + } } else if (op === '$gte') { conditions.push(`${dbKey} >= ${val.$gte}`); } else if (op === '$lte') { @@ -87,6 +83,51 @@ class NEMariaDB { } return conditions.join(" AND "); } + + _buildWhereJob(filter) { + if (!filter || Object.keys(filter).length === 0) return "1=1"; + var conditions = []; + for (var key in filter) { + if (key === '$or') { + var orConds = []; + for (var i in filter.$or) orConds.push("(" + this._buildWhereJob(filter.$or[i]) + ")"); + conditions.push("(" + orConds.join(" OR ") + ")"); + } else if (key === '$and') { + var andConds = []; + for (var i in filter.$and) andConds.push("(" + this._buildWhereJob(filter.$and[i]) + ")"); + conditions.push("(" + andConds.join(" AND ") + ")"); + } else { + var val = filter[key]; + var dbKey = key === '_id' ? 'id' : key; + + if (val !== null && typeof val === 'object' && !Array.isArray(val)) { + for (var op in val) { + if (op === '$in') { + if (val.$in.length === 0) { + conditions.push('1=0'); + } else { + var inList = val.$in.map(v => this._escape(v)).join(","); + conditions.push(`${dbKey} IN (${inList})`); + } + } else if (op === '$gte') { + conditions.push(`${dbKey} >= ${val.$gte}`); + } else if (op === '$lte') { + conditions.push(`${dbKey} <= ${val.$lte}`); + } else if (op === '$gt') { + conditions.push(`${dbKey} > ${val.$gt}`); + } else if (op === '$lt') { + conditions.push(`${dbKey} < ${val.$lt}`); + } + } + } else if (val === null) { + conditions.push(`${dbKey} IS NULL`); // In native column, NULL is exactly NULL + } else { + conditions.push(`${dbKey} = ${this._escape(val)}`); + } + } + } + return conditions.join(" AND "); + } find(args, proj) { this._find = args; @@ -96,75 +137,101 @@ class NEMariaDB { return this; } - project(args) { - this._proj = args; - return this; - } - - sort(args) { - this._sort = args; - return this; - } + project(args) { this._proj = args; return this; } + sort(args) { this._sort = args; return this; } + limit(limit) { this._limit = limit; return this; } - limit(limit) { - this._limit = limit; - return this; + _applyProjection(docs) { + if (!this._proj) return docs; + var keepFields = []; + var excludeFields = []; + for (var p in this._proj) { + if (this._proj[p] === 1) keepFields.push(p); + else if (this._proj[p] === 0) excludeFields.push(p); + } + var ret = []; + for (var doc of docs) { + var pDoc = {}; + if (keepFields.length > 0) { + for (var k of keepFields) { if (doc[k] !== undefined) pDoc[k] = doc[k]; } + if (excludeFields.indexOf('_id') === -1) pDoc._id = doc._id || doc.id; + ret.push(pDoc); + } else { + var nDoc = {...doc}; + for (var k of excludeFields) delete nDoc[k]; + ret.push(nDoc); + } + } + return ret; } toArray(callback) { var self = this; return new Promise(function(resolve, reject) { - var where = self._buildWhere(self._find); - var query = `SELECT doc FROM plugin_scripttask WHERE ${where}`; + var isJob = self._find && self._find.type === 'job'; - if (self._sort) { - var order = []; - for (var key in self._sort) { - var dir = self._sort[key] === -1 ? "DESC" : "ASC"; - if (key === '_id') order.push(`id ${dir}`); - else order.push(`JSON_UNQUOTE(JSON_EXTRACT(doc, '$.${key}')) ${dir}`); + var queryJob = () => { + var wJ = self._buildWhereJob(self._find); + var q = `SELECT * FROM plugin_scripttask_jobs WHERE ${wJ}`; + if (self._sort) { + var order = []; + for (var key in self._sort) order.push(`${key === '_id' ? 'id' : key} ${self._sort[key] === -1 ? 'DESC' : 'ASC'}`); + if (order.length > 0) q += " ORDER BY " + order.join(", "); } - if (order.length > 0) query += " ORDER BY " + order.join(", "); - } - if (self._limit) { - query += ` LIMIT ${self._limit}`; - } - - self.pool.query(query) - .then(rows => { + if (self._limit) q += ` LIMIT ${self._limit}`; + return self.pool.query(q).then(rows => { + var docs = []; + for (var r of rows) { + var it = {...r}; + it._id = it.id; delete it.id; + for (var k in it) { + if (typeof it[k] === 'bigint') it[k] = Number(it[k]); + } + if (it.replaceVars && typeof it.replaceVars === 'string') { + try { it.replaceVars = JSON.parse(it.replaceVars); } catch(e) {} + } + docs.push(it); + } + return docs; + }); + }; + + var queryDoc = () => { + var wD = self._buildWhereDoc(self._find); + var q = `SELECT doc FROM plugin_scripttask WHERE ${wD}`; + if (self._sort) { + var order = []; + for (var key in self._sort) { + if (key === '_id') order.push(`id ${self._sort[key] === -1 ? 'DESC' : 'ASC'}`); + else order.push(`JSON_UNQUOTE(JSON_EXTRACT(doc, '$.${key}')) ${self._sort[key] === -1 ? 'DESC' : 'ASC'}`); + } + if (order.length > 0) q += " ORDER BY " + order.join(", "); + } + if (self._limit) q += ` LIMIT ${self._limit}`; + return self.pool.query(q).then(rows => { var docs = []; for (var i = 0; i < rows.length; i++) { var doc = typeof rows[i].doc === 'string' ? JSON.parse(rows[i].doc) : rows[i].doc; - - if (self._proj) { - var pDoc = {}; - var keepFields = []; - var excludeFields = []; - for (var p in self._proj) { - if (self._proj[p] === 1) keepFields.push(p); - else if (self._proj[p] === 0) excludeFields.push(p); - } - if (keepFields.length > 0) { - for (var k of keepFields) { - if (doc[k] !== undefined) pDoc[k] = doc[k]; - } - if (excludeFields.indexOf('_id') === -1) pDoc._id = doc._id; - docs.push(pDoc); - } else { - for (var k of excludeFields) delete doc[k]; - docs.push(doc); - } - } else { - docs.push(doc); - } + docs.push(doc); } - if (callback != null && typeof callback == 'function') callback(null, docs); - resolve(docs); - }) - .catch(err => { - if (callback != null && typeof callback == 'function') callback(err, null); - reject(err); + return docs; }); + }; + + var handleResults = (docs) => { + docs = self._applyProjection(docs); + if (callback != null && typeof callback == 'function') callback(null, docs); + resolve(docs); + } + + if (isJob) return queryJob().then(handleResults).catch(reject); + if (self._find && self._find.type && self._find.type !== 'job') return queryDoc().then(handleResults).catch(reject); + + // Generic ID query (or empty query) fallback to both + queryDoc().then(docs => { + if (docs.length > 0) return handleResults(docs); + return queryJob().then(handleResults); + }).catch(reject); }); } @@ -173,141 +240,212 @@ class NEMariaDB { return new Promise(function(resolve, reject) { var id = args._id; if (!id) { - // Generate a random 24 char hex id similar to Mongo id = require('crypto').randomBytes(12).toString('hex'); args._id = id; } - var docStr = JSON.stringify(args); - self.pool.query("INSERT INTO plugin_scripttask (id, doc) VALUES (?, ?)", [id, docStr]) - .then(res => { - resolve({ insertedId: id }); - }) - .catch(err => reject(err)); + if (args.type === 'job') { + var cols = ['id']; + var qmarks = ['?']; + var vals = [id]; + for (var k in args) { + if (k === '_id' || k === 'id') continue; + cols.push(k); + qmarks.push('?'); + var v = args[k]; + if (typeof v === 'object' && v !== null) v = JSON.stringify(v); + vals.push(v); + } + self.pool.query(`INSERT INTO plugin_scripttask_jobs (${cols.join(',')}) VALUES (${qmarks.join(',')})`, vals) + .then(res => resolve({ insertedId: id })) + .catch(reject); + } else { + var docStr = JSON.stringify(args); + self.pool.query("INSERT INTO plugin_scripttask (id, doc) VALUES (?, ?)", [id, docStr]) + .then(res => resolve({ insertedId: id })) + .catch(reject); + } }); } deleteOne(filter, options) { var self = this; - var where = self._buildWhere(filter); return new Promise(function(resolve, reject) { - self.pool.query(`DELETE FROM plugin_scripttask WHERE ${where} LIMIT 1`) - .then(res => resolve({ deletedCount: res.affectedRows })) - .catch(err => reject(err)); + var count = 0; + self.pool.query(`DELETE FROM plugin_scripttask WHERE ${self._buildWhereDoc(filter)} LIMIT 1`) + .then(res => { + count += res.affectedRows; + return self.pool.query(`DELETE FROM plugin_scripttask_jobs WHERE ${self._buildWhereJob(filter)} LIMIT 1`); + }) + .then(res => { + count += res.affectedRows; + resolve({ deletedCount: count }); + }) + .catch(reject); }); } deleteMany(filter, options) { var self = this; - var where = self._buildWhere(filter); return new Promise(function(resolve, reject) { - self.pool.query(`DELETE FROM plugin_scripttask WHERE ${where}`) - .then(res => resolve({ deletedCount: res.affectedRows })) - .catch(err => reject(err)); + var count = 0; + self.pool.query(`DELETE FROM plugin_scripttask WHERE ${self._buildWhereDoc(filter)}`) + .then(res => { + count += res.affectedRows; + return self.pool.query(`DELETE FROM plugin_scripttask_jobs WHERE ${self._buildWhereJob(filter)}`); + }) + .then(res => { + count += res.affectedRows; + resolve({ deletedCount: count }); + }) + .catch(reject); }); } updateOne(filter, update, options) { var self = this; - var where = self._buildWhere(filter); if (options == null) options = {}; if (options.upsert == null) options.upsert = false; return new Promise(function(resolve, reject) { - self.pool.query(`SELECT id, doc FROM plugin_scripttask WHERE ${where} LIMIT 1`) + var tryUpdateJob = () => { + var wJ = self._buildWhereJob(filter); + return self.pool.query(`SELECT id FROM plugin_scripttask_jobs WHERE ${wJ} LIMIT 1`) .then(rows => { - if (rows.length === 0) { - if (options.upsert) { - var newDoc = { ...filter }; - if (update.$set) newDoc = { ...newDoc, ...update.$set }; - return self.insertOne(newDoc).then(res => resolve({ matchedCount: 0, modifiedCount: 1, upsertedId: res.insertedId })); - } - return resolve({ matchedCount: 0, modifiedCount: 0 }); + if (rows.length === 0) return { matchedCount: 0, modifiedCount: 0 }; + var updates = [], vals = []; + var src = update.$set ? update.$set : update; + for (var k in src) { + if (k === '_id' || k === 'id') continue; + updates.push(`${k} = ?`); + var v = src[k]; + if (typeof v === 'object' && v !== null) v = JSON.stringify(v); + vals.push(v); } - + if (updates.length > 0) { + vals.push(rows[0].id); + return self.pool.query(`UPDATE plugin_scripttask_jobs SET ${updates.join(', ')} WHERE id = ?`, vals) + .then(() => ({ matchedCount: 1, modifiedCount: 1, upsertedId: rows[0].id })); + } else { + return { matchedCount: 1, modifiedCount: 0 }; + } + }); + }; + + var tryUpdateDoc = () => { + var wD = self._buildWhereDoc(filter); + return self.pool.query(`SELECT id, doc FROM plugin_scripttask WHERE ${wD} LIMIT 1`) + .then(rows => { + if (rows.length === 0) return { matchedCount: 0, modifiedCount: 0 }; var id = rows[0].id; var doc = typeof rows[0].doc === 'string' ? JSON.parse(rows[0].doc) : rows[0].doc; - var modifiedFields = 0; - + var modified = false; if (update.$set) { - for (var k in update.$set) { - doc[k] = update.$set[k]; - } - modifiedFields = 1; + for (var k in update.$set) doc[k] = update.$set[k]; + modified = true; } else { doc = { ...doc, ...update }; if (!doc._id) doc._id = id; - modifiedFields = 1; + modified = true; } - - if (modifiedFields) { - var docStr = JSON.stringify(doc); - return self.pool.query("UPDATE plugin_scripttask SET doc = ? WHERE id = ?", [docStr, id]) - .then(res => resolve({ matchedCount: 1, modifiedCount: 1, upsertedId: id })); + if (modified) { + return self.pool.query("UPDATE plugin_scripttask SET doc = ? WHERE id = ?", [JSON.stringify(doc), id]) + .then(() => ({ matchedCount: 1, modifiedCount: 1, upsertedId: id })); } else { - return resolve({ matchedCount: 1, modifiedCount: 0 }); + return { matchedCount: 1, modifiedCount: 0 }; } - }) - .catch(err => reject(err)); + }); + }; + + var isJob = filter.type === 'job'; + var isDoc = filter.type && filter.type !== 'job'; + + if (isJob) return tryUpdateJob().then(res => { + if (res.matchedCount === 0 && options.upsert) { + var newDoc = { ...filter, ...(update.$set || {}) }; + return self.insertOne(newDoc).then(r => ({matchedCount:0, modifiedCount:1, upsertedId: r.insertedId})); + } + resolve(res); + }).catch(reject); + + if (isDoc) return tryUpdateDoc().then(res => { + if (res.matchedCount === 0 && options.upsert) { + var newDoc = { ...filter, ...(update.$set || {}) }; + return self.insertOne(newDoc).then(r => ({matchedCount:0, modifiedCount:1, upsertedId: r.insertedId})); + } + resolve(res); + }).catch(reject); + + // generic branch + tryUpdateDoc().then(res => { + if (res.matchedCount > 0) return resolve(res); + return tryUpdateJob().then(res2 => { + if (res2.matchedCount === 0 && options.upsert) { + var newDoc = { ...filter, ...(update.$set || {}) }; // fallback to insert doc + return self.insertOne(newDoc).then(r => resolve({matchedCount:0, modifiedCount:1, upsertedId: r.insertedId})); + } + resolve(res2); + }); + }).catch(reject); }); } updateMany(filter, update, options) { var self = this; - var where = self._buildWhere(filter); if (options == null) options = {}; if (options.upsert == null) options.upsert = false; return new Promise(function(resolve, reject) { - self.pool.query(`SELECT id, doc FROM plugin_scripttask WHERE ${where}`) + var tryUpdateJob = () => { + var wJ = self._buildWhereJob(filter); + return self.pool.query(`SELECT id FROM plugin_scripttask_jobs WHERE ${wJ}`) .then(rows => { - if (rows.length === 0) { - if (options.upsert) { - // Upsert logic for updateMany doesn't normally generate multiple - var newDoc = { ...filter }; - if (update.$set) newDoc = { ...newDoc, ...update.$set }; - return self.insertOne(newDoc).then(res => resolve({ matchedCount: 0, modifiedCount: 1, upsertedId: res.insertedId })); - } - return resolve({ matchedCount: 0, modifiedCount: 0 }); + if (rows.length === 0) return { matchedCount: 0, modifiedCount: 0 }; + var updatesQ = [], vals = []; + var src = update.$set ? update.$set : update; + for (var k in src) { + if (k === '_id' || k === 'id') continue; + updatesQ.push(`${k} = ?`); + var v = src[k]; + if (typeof v === 'object' && v !== null) v = JSON.stringify(v); + vals.push(v); } - - var updates = []; - for (var i = 0; i < rows.length; i++) { - var id = rows[i].id; - var doc = typeof rows[i].doc === 'string' ? JSON.parse(rows[i].doc) : rows[i].doc; - + if (updatesQ.length > 0) { + var proms = rows.map(r => self.pool.query(`UPDATE plugin_scripttask_jobs SET ${updatesQ.join(', ')} WHERE id = ?`, [...vals, r.id])); + return Promise.all(proms).then(() => ({ matchedCount: rows.length, modifiedCount: rows.length })); + } else { + return { matchedCount: rows.length, modifiedCount: 0 }; + } + }); + }; + + var tryUpdateDoc = () => { + var wD = self._buildWhereDoc(filter); + return self.pool.query(`SELECT id, doc FROM plugin_scripttask WHERE ${wD}`) + .then(rows => { + if (rows.length === 0) return { matchedCount: 0, modifiedCount: 0 }; + var proms = rows.map(r => { + var doc = typeof r.doc === 'string' ? JSON.parse(r.doc) : r.doc; if (update.$set) { - for (var k in update.$set) { - doc[k] = update.$set[k]; - } + for (var k in update.$set) doc[k] = update.$set[k]; } else { doc = { ...doc, ...update }; - if (!doc._id) doc._id = id; + if (!doc._id) doc._id = r.id; } - var docStr = JSON.stringify(doc); - updates.push(self.pool.query("UPDATE plugin_scripttask SET doc = ? WHERE id = ?", [docStr, id])); - } - - if (updates.length > 0) { - return Promise.all(updates).then(() => resolve({ matchedCount: rows.length, modifiedCount: rows.length })); - } else { - return resolve({ matchedCount: rows.length, modifiedCount: 0 }); - } - }) - .catch(err => reject(err)); + return self.pool.query("UPDATE plugin_scripttask SET doc = ? WHERE id = ?", [JSON.stringify(doc), r.id]); + }); + return Promise.all(proms).then(() => ({ matchedCount: rows.length, modifiedCount: rows.length })); + }); + }; + + var isJob = filter.type === 'job'; + if (isJob) return tryUpdateJob().then(res => resolve(res)).catch(reject); + return tryUpdateDoc().then(res => resolve(res)).catch(reject); }); } - indexes(callback) { - if (callback != null && typeof callback == 'function') callback(null, []); - } - - dropIndexes(callback) { - if (callback != null && typeof callback == 'function') callback(null); - } - - createIndex(args, options) { - // Ignored for JSON DB adapter for now - } + indexes(callback) { if (callback != null && typeof callback == 'function') callback(null, []); } + dropIndexes(callback) { if (callback != null && typeof callback == 'function') callback(null); } + createIndex(args, options) { } } module.exports = NEMariaDB; From ede40f44caeffd19706551e38a26ca3925bbc73a Mon Sep 17 00:00:00 2001 From: Pablo Navarro Date: Tue, 31 Mar 2026 00:51:08 +0200 Subject: [PATCH 06/11] Add fork note and update configuration URL Updated README to reflect the fork and new configuration URL. --- readme.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index a289b34..a21276e 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,5 @@ # MeshCentral-ScriptTask +# Forked to add support to MariaDB! A script running plugin for the [MeshCentral2](https://github.com/Ylianst/MeshCentral) Project. The plugin supports PowerShell, BAT, and Bash scripts. Windows, MacOS, and Linux endpoints are all supported. PowerShell can be run on any OS that has PowerShell installed, not just Windows. @@ -14,7 +15,7 @@ A script running plugin for the [MeshCentral2](https://github.com/Ylianst/MeshCe Restart your MeshCentral server after making this change. To install, simply add the plugin configuration URL when prompted: - `https://raw.githubusercontent.com/ryanblenis/MeshCentral-ScriptTask/master/config.json` + `https://raw.githubusercontent.com/panreyes/MeshCentral-ScriptTask/master/config.json` ## Features - Add scripts to a central store From f15cf33baa22a8d94662cba166a7fbf01102f788 Mon Sep 17 00:00:00 2001 From: Pablo Navarro Date: Tue, 31 Mar 2026 00:52:06 +0200 Subject: [PATCH 07/11] Update version and author details in config.json --- config.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/config.json b/config.json index 00cd76a..f534b28 100644 --- a/config.json +++ b/config.json @@ -1,18 +1,18 @@ { "name": "ScriptTask", "shortName": "scripttask", - "version": "0.0.20", - "author": "Ryan Blenis", + "version": "0.0.21", + "author": "Ryan Blenis / Pablo Navarro", "description": "Script (PowerShell, BAT, Bash) runner for endpoints", "hasAdminPanel": false, - "homepage": "https://github.com/ryanblenis/MeshCentral-ScriptTask", - "changelogUrl": "https://raw.githubusercontent.com/ryanblenis/MeshCentral-ScriptTask/master/changelog.md", - "configUrl": "https://raw.githubusercontent.com/ryanblenis/MeshCentral-ScriptTask/master/config.json", - "downloadUrl": "https://github.com/ryanblenis/MeshCentral-ScriptTask/archive/master.zip", + "homepage": "https://github.com/panreyes/MeshCentral-ScriptTask", + "changelogUrl": "https://raw.githubusercontent.com/panreyes/MeshCentral-ScriptTask/master/changelog.md", + "configUrl": "https://raw.githubusercontent.com/panreyes/MeshCentral-ScriptTask/master/config.json", + "downloadUrl": "https://github.com/panreyes/MeshCentral-ScriptTask/archive/master.zip", "repository": { "type": "git", - "url": "https://github.com/ryanblenis/MeshCentral-ScriptTask.git" + "url": "https://github.com/panreyes/MeshCentral-ScriptTask.git" }, - "versionHistoryUrl": "https://api.github.com/repos/ryanblenis/MeshCentral-ScriptTask/tags", + "versionHistoryUrl": "https://api.github.com/repos/panreyes/MeshCentral-ScriptTask/tags", "meshCentralCompat": ">=1.1.35" -} \ No newline at end of file +} From fa83ad9b04600028987d6434bfaeda545ef97453 Mon Sep 17 00:00:00 2001 From: panreyes Date: Tue, 31 Mar 2026 01:02:42 +0200 Subject: [PATCH 08/11] A few improvements for the modern UI --- views/user.handlebars | 154 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 126 insertions(+), 28 deletions(-) diff --git a/views/user.handlebars b/views/user.handlebars index 058d140..8f5b92d 100644 --- a/views/user.handlebars +++ b/views/user.handlebars @@ -1,6 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
- New - Rename - Edit - Delete - New Folder - Download - Run + New + New Folder + Edit + Rename + Download + Delete + Run now
-
Advanced Run
-
-
+ Advanced Run + Node Schedules + Node History + Variables + Script Schedules + Script History + +
+
@@ -211,37 +279,32 @@
-
Node Schedules
ScriptAuthorEveryStartingEndingLast RunNext RunAction
-
Script Schedules
NodeAuthorEveryStartingEndingLast RunNext RunAction
-
Node History
TimeRun ByScriptStatusReturn Value
-
Script History
TimeRun ByNodeStatusReturn Value
-
Variables
Variable NameValueScopeScope TargetAction

- [+] + Add variable
@@ -272,6 +335,7 @@ function updateNodesTable() { }); var tagList = []; var nodeRowIns = document.querySelector('#mRunTbl'); + parent.nodes.sort((a,b) => (a.name > b.name) ? 1 : -1); parent.nodes.forEach(function(i) { var item = {...i, ...{}}; if (item.mtype == 2) { @@ -286,6 +350,17 @@ function updateNodesTable() { } }); tagList = tagList.filter(onlyUnique); tagList = tagList.sort(); + // parent.meshes.sort((a,b) => a.name.localeCompare(b.name)); + + // sort meshes by name + const sortedEntries = Object.entries(parent.meshes).sort(([, v1], [, v2]) => { + const a = v1 && v1.name ? v1.name : ""; + const b = v2 && v2.name ? v2.name : ""; + return a > b ? 1 : a < b ? -1 : 0; + }); + + parent.meshes = Object.fromEntries(sortedEntries); + var nodeRowIns = document.querySelector('#mRunTblMesh'); for (const i in parent.meshes) { // parent.meshes.forEach(function(i) { var item = {...parent.meshes[i], ...{}}; @@ -420,14 +495,27 @@ function goAdvancedRun() { var coll = document.getElementsByClassName("infoBar"); for (var i = 0; i < coll.length; i++) { coll[i].addEventListener("click", function() { + for (var j = 0; j < coll.length; j++) { + coll[j].classList.remove("active"); + //coll[j].nextElementSibling.style.display = "none"; + } + $('#multiRun').hide(200); + $('#nSch').hide(200); + $('#nodeHistory').hide(200); + $('#variables').hide(200); + $('#sSch').hide(200); + $('#scriptHistory').hide(200); + this.classList.toggle("active"); - var content = this.nextElementSibling; + //var content = this.nextElementSibling; + // var content = this; + var content = document.getElementById(this.id.slice(this.id.lastIndexOf('_') + 1)); if (content.style.display === "block") { content.style.display = "none"; } else { content.style.display = "block"; } - content.style.maxHeight = '300px'; + content.style.maxHeight = '400px'; content.style.overflowY = 'scroll'; resizeIframe(); }); @@ -441,6 +529,7 @@ function goDownload() { if (id == sel.getAttribute('x-data-folder')) return; window.location = '/pluginadmin.ashx?pin=scripttask&user=1&dl='+id; } + function addScript(name, content, path) { // file type testing var n = name.split('.').pop().toLowerCase(); @@ -452,6 +541,7 @@ function addScript(name, content, path) { parent.setDialogMode(2, "Oops!", 1, null, 'Currently accepted filetypes are .ps1, .bat, and bash scripts.'); } } + function redrawScriptTree() { var lastpath = null; var str = ''; @@ -513,14 +603,14 @@ function redrawScriptTree() { message.event.nodeHistory.forEach(function(nh) { nh.latestTime = Math.max(nh.completeTime, nh.queueTime, nh.dispatchTime, nh.dontQueueUntil); }); - message.event.nodeHistory.sort((a, b) => (a.latestTime < b.latestTime) ? 1 : -1); + message.event.nodeHistory.sort((a, b) => a.name.localeCompare(b.name)); message.event.nodeHistory.forEach(function(nh) { nh = prepHistory(nh); let tpl = '' + nh.timeStr + ' \ ' + nh.runBy + ' \ ' + nh.scriptName + ' \ ' + nh.statusTxt + ' \ - ' + nh.returnTxt + ''; +
' + nh.returnTxt + '
'; let tr = nHistTbl.insertRow(-1); tr.innerHTML = tpl; tr.classList.add('stNHRow'); @@ -548,7 +638,7 @@ function redrawScriptTree() { ' + nh.runBy + ' \ ' + nNames[nh.node] + ' \ ' + nh.statusTxt + ' \ - ' + nh.returnTxt + ''; +
' + nh.returnTxt + '
'; let tr = sHistTbl.insertRow(-1); tr.innerHTML = tpl; tr.classList.add('stSHRow'); @@ -598,6 +688,7 @@ function redrawScriptTree() { break; case 'script': var s = scriptTree.filter(obj => { return obj._id === vd.scopeTarget })[0] + if (s === undefined) { return; } vd.scopeTargetHtml = '' + s.name + ''; vd.scopeTargetTxt = s.name; break; @@ -606,8 +697,14 @@ function redrawScriptTree() { break; case 'node': var n = parent.nodes.filter(obj => { return obj._id === vd.scopeTarget })[0] - vd.scopeTargetHtml = '' + n.name + ''; - vd.scopeTargetTxt = n.name; + if (n === undefined) { + console.log("No existe el nodo " + vd.scopeTarget); + parent.meshserver.send({ action: 'plugin', plugin: 'scripttask', pluginaction: 'deleteVar', id: vd._id, currentNodeId: parent.currentNode._id }); + vd = null; + return; + } + vd.scopeTargetHtml = '' + n.name + ''; + vd.scopeTargetTxt = n.name; break; default: vd.scopeTargetTxt = vd.scopeTargetHtml = 'N/A'; @@ -639,10 +736,11 @@ function redrawScriptTree() { var el = scriptEl[0]; scopeTargetScriptId = el.getAttribute('x-data-id'); variables.forEach(function(vd) { + if (vd === null) return; if (vd.scope == 'script' && vd.scopeTarget != scopeTargetScriptId) return; if (vd.scope == 'mesh' && vd.scopeTarget != parent.currentNode.meshid) return; if (vd.scope == 'node' && vd.scopeTarget != parent.currentNode._id) return; - let actionHtml = 'Edit Delete'; + let actionHtml = 'Edit Delete'; let tpl = '' + vd.name + ' \ ' + vd.value + ' \ ' + vd.scopeTxt + ' \ @@ -689,6 +787,7 @@ function redrawScriptTree() { }); } } + var currentScript = document.getElementById('scriptHistory'); var currentScriptId = currentScript.getAttribute('x-data-id'); if (message.event.scriptSchedule != null && message.event.scriptId == currentScriptId) { @@ -1020,4 +1119,3 @@ function redrawScriptTree() { - From b97c8dacea8beac92ffe8e5c77484a3b9a8bdc2d Mon Sep 17 00:00:00 2001 From: Pablo Navarro Date: Sat, 4 Apr 2026 15:49:55 +0200 Subject: [PATCH 09/11] Change returnVal column type to MEDIUMTEXT --- nemariadb.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nemariadb.js b/nemariadb.js index 298e915..4782571 100644 --- a/nemariadb.js +++ b/nemariadb.js @@ -25,7 +25,7 @@ class NEMariaDB { this.pool.query("CREATE TABLE IF NOT EXISTS plugin_scripttask (id VARCHAR(128) PRIMARY KEY, doc JSON)") .catch(err => { console.log("PLUGIN: ScriptTask: Error creating database table", err); }); - this.pool.query("CREATE TABLE IF NOT EXISTS plugin_scripttask_jobs (id VARCHAR(128) PRIMARY KEY, type VARCHAR(64) DEFAULT 'job', queueTime BIGINT, dontQueueUntil BIGINT, dispatchTime BIGINT, completeTime BIGINT, node VARCHAR(256), scriptId VARCHAR(128), scriptName VARCHAR(512), replaceVars JSON, returnVal TEXT, errorVal TEXT, returnAct VARCHAR(256), runBy VARCHAR(256), jobSchedule VARCHAR(128))") + this.pool.query("CREATE TABLE IF NOT EXISTS plugin_scripttask_jobs (id VARCHAR(128) PRIMARY KEY, type VARCHAR(64) DEFAULT 'job', queueTime BIGINT, dontQueueUntil BIGINT, dispatchTime BIGINT, completeTime BIGINT, node VARCHAR(256), scriptId VARCHAR(128), scriptName VARCHAR(512), replaceVars JSON, returnVal MEDIUMTEXT, errorVal TEXT, returnAct VARCHAR(256), runBy VARCHAR(256), jobSchedule VARCHAR(128))") .catch(err => { console.log("PLUGIN: ScriptTask: Error creating jobs table", err); }); } From f23354e2f5987f19a5bcc197fc59c70ac379d2dd Mon Sep 17 00:00:00 2001 From: panreyes Date: Sat, 4 Apr 2026 19:56:13 +0200 Subject: [PATCH 10/11] Sadly, many changes put together, and Antigravity automatic linter. Removed behaviour in which it would retry jobs infinitely if they didn't complete in less than one minute. Added a behaviour to detect stuck jobs and cancel them. Added a button to cancel pending jobs. Improved size of the iframe (still not perfect). Improved behaviour for showing the history to the related users. When deleting a script, it will also cancel its related jobs. Fixed issue when canceling a job when using MariaDB. --- db.js | 324 +++--- modules_meshcore/scripttask.js | 27 + nemariadb.js | 363 ++++--- scripttask.js | 898 +++++++++-------- views/user.handlebars | 1726 +++++++++++++++++--------------- 5 files changed, 1782 insertions(+), 1556 deletions(-) diff --git a/db.js b/db.js index 34eb5d1..ce23ced 100644 --- a/db.js +++ b/db.js @@ -9,30 +9,30 @@ var Datastore = null; var formatId = null; -module.exports.CreateDB = function(meshserver) { +module.exports.CreateDB = function (meshserver) { var obj = {}; var NEMongo = require(__dirname + '/nemongo.js'); module.paths.push(require('path').join(meshserver.parentpath, 'node_modules')); // we need to push the node_modules folder for nedb obj.dbVersion = 1; - + obj.initFunctions = function () { - obj.updateDBVersion = function(new_version) { - return obj.scriptFile.updateOne({type: "db_version"}, { $set: {version: new_version} }, {upsert: true}); + obj.updateDBVersion = function (new_version) { + return obj.scriptFile.updateOne({ type: "db_version" }, { $set: { version: new_version } }, { upsert: true }); }; - - obj.getDBVersion = function() { - return new Promise(function(resolve, reject) { - obj.scriptFile.find( { type: "db_version" } ).project( { _id: 0, version: 1 } ).toArray(function(err, vers){ + + obj.getDBVersion = function () { + return new Promise(function (resolve, reject) { + obj.scriptFile.find({ type: "db_version" }).project({ _id: 0, version: 1 }).toArray(function (err, vers) { if (vers.length == 0) resolve(1); else resolve(vers[0]['version']); }); }); }; - obj.addScript = function(name, content, path, filetype) { + obj.addScript = function (name, content, path, filetype) { if (path == null) path = "Shared" if (filetype == 'bash') content = content.split('\r\n').join('\n').split('\r').join('\n'); - var sObj = { + var sObj = { type: 'script', path: path, name: name, @@ -42,8 +42,8 @@ module.exports.CreateDB = function(meshserver) { }; return obj.scriptFile.insertOne(sObj); }; - - obj.addFolder = function(name, path) { + + obj.addFolder = function (name, path) { var sObj = { type: 'folder', path: path, @@ -51,11 +51,12 @@ module.exports.CreateDB = function(meshserver) { }; return obj.scriptFile.insertOne(sObj); }; - - obj.getScriptTree = function() { + + obj.getScriptTree = function () { return obj.scriptFile.find( - { type: - { $in: [ 'script', 'folder' ] } + { + type: + { $in: ['script', 'folder'] } } ).sort( { path: 1, type: 1, name: 1 } @@ -63,61 +64,62 @@ module.exports.CreateDB = function(meshserver) { { name: 1, path: 1, type: 1, filetype: 1 } ).toArray(); }; - - obj.update = function(id, args) { + + obj.update = function (id, args) { id = formatId(id); - if (args.type == 'script' && args.content !== null) { + if (args.type == 'script' && args.content !== null) { if (args.filetype == 'bash') { args.content = args.content = split('\r\n').join('\n').split('\r').join('\n'); } args.contentHash = require('crypto').createHash('sha384').update(args.content).digest('hex'); } - return obj.scriptFile.updateOne( { _id: id }, { $set: args } ); + return obj.scriptFile.updateOne({ _id: id }, { $set: args }); }; - obj.delete = function(id) { + obj.delete = function (id) { id = formatId(id); - return obj.scriptFile.deleteOne( { _id: id } ); + return obj.scriptFile.deleteOne({ _id: id }); }; - obj.deleteByPath = function(path) { - return obj.scriptFile.deleteMany( { path: path, type: { $in: ['script', 'folder'] } } ); + obj.deleteByPath = function (path) { + return obj.scriptFile.deleteMany({ path: path, type: { $in: ['script', 'folder'] } }); }; - obj.deleteSchedulesForScript = function(id) { + obj.deleteSchedulesForScript = function (id) { id = formatId(id); - return obj.scriptFile.deleteMany( { type: 'jobSchedule', scriptId: id } ); + return obj.scriptFile.deleteMany({ type: 'jobSchedule', scriptId: id }); }; - obj.getByPath = function(path) { - return obj.scriptFile.find( { type: { $in: [ 'script', 'folder' ] }, path: path }).toArray(); + obj.getByPath = function (path) { + return obj.scriptFile.find({ type: { $in: ['script', 'folder'] }, path: path }).toArray(); }; - obj.get = function(id) { - if (id == null || id == 'null') return new Promise(function(resolve, reject) { resolve([]); }); + obj.get = function (id) { + if (id == null || id == 'null') return new Promise(function (resolve, reject) { resolve([]); }); id = formatId(id); - return obj.scriptFile.find( { _id: id } ).toArray(); + return obj.scriptFile.find({ _id: id }).toArray(); }; - obj.addJob = function(passedObj) { - var nowTime = Math.floor(new Date() / 1000); - var defaultObj = { - type: 'job', - queueTime: nowTime, - dontQueueUntil: nowTime, - dispatchTime: null, - completeTime: null, - node: null, - scriptId: null, - scriptName: null, // in case the original reference is deleted in the future - replaceVars: null, - returnVal: null, - errorVal: null, - returnAct: null, - runBy: null, - jobSchedule: null - }; - var jObj = {...defaultObj, ...passedObj}; - - if (jObj.node == null || jObj.scriptId == null) { console.log('PLUGIN: SciptTask: Could not add job'); return false; } - - return obj.scriptFile.insertOne(jObj); + obj.addJob = function (passedObj) { + var nowTime = Math.floor(new Date() / 1000); + var defaultObj = { + type: 'job', + queueTime: nowTime, + dontQueueUntil: nowTime, + dispatchTime: null, + completeTime: null, + lastPing: null, + node: null, + scriptId: null, + scriptName: null, // in case the original reference is deleted in the future + replaceVars: null, + returnVal: null, + errorVal: null, + returnAct: null, + runBy: null, + jobSchedule: null + }; + var jObj = { ...defaultObj, ...passedObj }; + + if (jObj.node == null || jObj.scriptId == null) { console.log('PLUGIN: SciptTask: Could not add job'); return false; } + + return obj.scriptFile.insertOne(jObj); }; - obj.addJobSchedule = function(schedObj) { + obj.addJobSchedule = function (schedObj) { schedObj.type = 'jobSchedule'; if (schedObj.node == null || schedObj.scriptId == null) { console.log('PLUGIN: SciptTask: Could not add job schedule'); return false; } return obj.scriptFile.insertOne(schedObj); @@ -125,13 +127,13 @@ module.exports.CreateDB = function(meshserver) { obj.removeJobSchedule = function (id) { return obj.delete(id); }; - obj.getSchedulesDueForJob = function(scheduleId) { + obj.getSchedulesDueForJob = function (scheduleId) { var nowTime = Math.floor(new Date() / 1000); var scheduleIdLimiter = {}; if (scheduleId != null) { scheduleIdLimiter._id = scheduleId; } - return obj.scriptFile.find( { + return obj.scriptFile.find({ type: 'jobSchedule', // startAt: { $gte: nowTime }, $or: [ @@ -139,69 +141,84 @@ module.exports.CreateDB = function(meshserver) { { endAt: { $lte: nowTime } } ], $or: [ - { nextRun: null }, + { nextRun: null }, { nextRun: { $lte: (nowTime + 60) } } // check a minute into the future ], ...scheduleIdLimiter }).toArray(); }; - obj.deletePendingJobsForNode = function(node) { - return obj.scriptFile.deleteMany({ - type: 'job', + obj.deletePendingJobsForNode = function (node) { + return obj.scriptFile.deleteMany({ + type: 'job', node: node, completeTime: null, }); }; - obj.getPendingJobs = function(nodeScope) { - if (nodeScope == null || !Array.isArray(nodeScope)) { - return false; - } - // return jobs that has online nodes and queue time requirements have been met - return obj.scriptFile.find( { - type: 'job', - node: { $in: nodeScope }, - completeTime: null, - //dispatchTime: null, - $or: [ - { dontQueueUntil: null }, - { dontQueueUntil: { $lte: Math.floor(new Date() / 1000) } } - ] - }).toArray(); + obj.getPendingJobs = function (nodeScope) { + if (nodeScope == null || !Array.isArray(nodeScope)) { + return false; + } + // return jobs that has online nodes and queue time requirements have been met + return obj.scriptFile.find({ + type: 'job', + node: { $in: nodeScope }, + completeTime: null, + dispatchTime: null, + $or: [ + { dontQueueUntil: null }, + { dontQueueUntil: { $lte: Math.floor(new Date() / 1000) } } + ] + }).toArray(); }; - obj.getJobNodeHistory = function(nodeId) { - return obj.scriptFile.find( { - type: 'job', + obj.getStuckJobs = function (timeoutSeconds) { + var thresholdTime = Math.floor(new Date() / 1000) - timeoutSeconds; + return obj.scriptFile.find({ + type: 'job', + completeTime: null, + dispatchTime: { $ne: null }, + $or: [ + { lastPing: { $lt: thresholdTime } }, + { lastPing: null, dispatchTime: { $lt: thresholdTime } } + ] + }).toArray(); + }; + obj.getJobNodeHistory = function (nodeId) { + return obj.scriptFile.find({ + type: 'job', node: nodeId, }).sort({ queueTime: -1 }).limit(200).toArray(); }; - obj.getJobScriptHistory = function(scriptId) { - return obj.scriptFile.find( { - type: 'job', + obj.getJobScriptHistory = function (scriptId) { + return obj.scriptFile.find({ + type: 'job', scriptId: scriptId, }).sort({ completeTime: -1, queueTime: -1 }).limit(200).toArray(); }; - obj.updateScriptJobName = function(scriptId, scriptName) { - return obj.scriptFile.updateMany({ type: 'job', scriptId: scriptId }, { $set: { scriptName: scriptName } }); + obj.updateScriptJobName = function (scriptId, scriptName) { + return obj.scriptFile.updateMany({ type: 'job', scriptId: scriptId }, { $set: { scriptName: scriptName } }); }; - obj.getJobSchedulesForScript = function(scriptId) { - return obj.scriptFile.find( { type: 'jobSchedule', scriptId: scriptId } ).toArray(); + obj.getJobSchedulesForScript = function (scriptId) { + return obj.scriptFile.find({ type: 'jobSchedule', scriptId: scriptId }).toArray(); }; obj.getJobSchedulesForNode = function (nodeId) { - return obj.scriptFile.find( { type: 'jobSchedule', node: nodeId } ).toArray(); + return obj.scriptFile.find({ type: 'jobSchedule', node: nodeId }).toArray(); }; obj.getIncompleteJobsForSchedule = function (schedId) { - return obj.scriptFile.find( { type: 'job', jobSchedule: schedId, completeTime: null } ).toArray(); + return obj.scriptFile.find({ type: 'job', jobSchedule: schedId, completeTime: null }).toArray(); }; obj.deletePendingJobsForSchedule = function (schedId) { - return obj.scriptFile.deleteMany( { type: 'job', jobSchedule: schedId, completeTime: null } ); + return obj.scriptFile.deleteMany({ type: 'job', jobSchedule: schedId, completeTime: null }); + }; + obj.deletePendingJobsForScript = function (scriptId) { + return obj.scriptFile.deleteMany({ type: 'job', scriptId: scriptId, completeTime: null }); }; - obj.deleteOldHistory = function() { + obj.deleteOldHistory = function () { var nowTime = Math.floor(new Date() / 1000); var oldTime = nowTime - (86400 * 90); // 90 days - return obj.scriptFile.deleteMany( { type: 'job', completeTime: { $lte: oldTime } } ); + return obj.scriptFile.deleteMany({ type: 'job', completeTime: { $lte: oldTime } }); }; - obj.addVariable = function(name, scope, scopeTarget, value) { - var vObj = { + obj.addVariable = function (name, scope, scopeTarget, value) { + var vObj = { type: 'variable', name: name, scope: scope, @@ -210,27 +227,30 @@ module.exports.CreateDB = function(meshserver) { }; return obj.scriptFile.insertOne(vObj); }; - obj.getVariables = function(limiters) { + obj.getVariables = function (limiters) { if (limiters != null) { - var find = { + var find = { type: 'variable', name: { $in: limiters.names }, - $or: [ + $or: [ { scope: 'global' }, - { $and: [ - { scope: 'script' }, - { scopeTarget: limiters.scriptId } - ] + { + $and: [ + { scope: 'script' }, + { scopeTarget: limiters.scriptId } + ] }, - { $and: [ - { scope: 'mesh' }, - { scopeTarget: limiters.meshId } - ] + { + $and: [ + { scope: 'mesh' }, + { scopeTarget: limiters.meshId } + ] }, - { $and: [ - { scope: 'node' }, - { scopeTarget: limiters.nodeId } - ] + { + $and: [ + { scope: 'node' }, + { scopeTarget: limiters.nodeId } + ] } ] }; @@ -240,51 +260,51 @@ module.exports.CreateDB = function(meshserver) { return obj.scriptFile.find({ type: 'variable' }).sort({ name: 1 }).toArray(); } }; - obj.checkDefaults = function() { - obj.scriptFile.find( { type: 'folder', name: 'Shared', path: 'Shared' } ).toArray() - .then(found => { - if (found.length == 0) obj.addFolder('Shared', 'Shared'); - }) - .catch(e => { console.log('PLUGIN: ScriptTask: Default folder check failed. Error was: ', e); }); + obj.checkDefaults = function () { + obj.scriptFile.find({ type: 'folder', name: 'Shared', path: 'Shared' }).toArray() + .then(found => { + if (found.length == 0) obj.addFolder('Shared', 'Shared'); + }) + .catch(e => { console.log('PLUGIN: ScriptTask: Default folder check failed. Error was: ', e); }); }; - + obj.checkDefaults(); }; - + if (meshserver.args.mongodb) { // use MongDB - require('mongodb').MongoClient.connect(meshserver.args.mongodb, { useNewUrlParser: true, useUnifiedTopology: true }, function (err, client) { - if (err != null) { console.log("Unable to connect to database: " + err); process.exit(); return; } - - var dbname = 'meshcentral'; - if (meshserver.args.mongodbname) { dbname = meshserver.args.mongodbname; } - const db = client.db(dbname); - - obj.scriptFile = db.collection('plugin_scripttask'); - obj.scriptFile.indexes(function (err, indexes) { - // Check if we need to reset indexes - var indexesByName = {}, indexCount = 0; - for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; } - if ((indexCount != 6) || (indexesByName['ScriptName1'] == null) || (indexesByName['ScriptPath1'] == null) || (indexesByName['JobTime1'] == null) || (indexesByName['JobNode1'] == null) || (indexesByName['JobScriptID1'] == null)) { - // Reset all indexes - console.log('Resetting plugin (ScriptTask) indexes...'); - obj.scriptFile.dropIndexes(function (err) { - obj.scriptFile.createIndex({ name: 1 }, { name: 'ScriptName1' }); - obj.scriptFile.createIndex({ path: 1 }, { name: 'ScriptPath1' }); - obj.scriptFile.createIndex({ queueTime: 1 }, { name: 'JobTime1' }); - obj.scriptFile.createIndex({ node: 1 }, { name: 'JobNode1' }); - obj.scriptFile.createIndex({ scriptId: 1 }, { name: 'JobScriptID1' }); - }); - } - }); - - - if (typeof require('mongodb').ObjectID == 'function') { - formatId = require('mongodb').ObjectID; - } else { - formatId = require('mongodb').ObjectId; - } - obj.initFunctions(); - }); + require('mongodb').MongoClient.connect(meshserver.args.mongodb, { useNewUrlParser: true, useUnifiedTopology: true }, function (err, client) { + if (err != null) { console.log("Unable to connect to database: " + err); process.exit(); return; } + + var dbname = 'meshcentral'; + if (meshserver.args.mongodbname) { dbname = meshserver.args.mongodbname; } + const db = client.db(dbname); + + obj.scriptFile = db.collection('plugin_scripttask'); + obj.scriptFile.indexes(function (err, indexes) { + // Check if we need to reset indexes + var indexesByName = {}, indexCount = 0; + for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; } + if ((indexCount != 6) || (indexesByName['ScriptName1'] == null) || (indexesByName['ScriptPath1'] == null) || (indexesByName['JobTime1'] == null) || (indexesByName['JobNode1'] == null) || (indexesByName['JobScriptID1'] == null)) { + // Reset all indexes + console.log('Resetting plugin (ScriptTask) indexes...'); + obj.scriptFile.dropIndexes(function (err) { + obj.scriptFile.createIndex({ name: 1 }, { name: 'ScriptName1' }); + obj.scriptFile.createIndex({ path: 1 }, { name: 'ScriptPath1' }); + obj.scriptFile.createIndex({ queueTime: 1 }, { name: 'JobTime1' }); + obj.scriptFile.createIndex({ node: 1 }, { name: 'JobNode1' }); + obj.scriptFile.createIndex({ scriptId: 1 }, { name: 'JobScriptID1' }); + }); + } + }); + + + if (typeof require('mongodb').ObjectID == 'function') { + formatId = require('mongodb').ObjectID; + } else { + formatId = require('mongodb').ObjectId; + } + obj.initFunctions(); + }); } else if (meshserver.args.mariadb) { // use MariaDB var mariadb = null; try { mariadb = require('mariadb'); } catch (e) { console.log('PLUGIN: ScriptTask: mariadb module is required but not found.'); } @@ -302,7 +322,7 @@ module.exports.CreateDB = function(meshserver) { if (meshserver.args.mariadbname && typeof m_options === 'object') m_options.database = meshserver.args.mariadbname; var pool = mariadb.createPool(m_options); obj.scriptFile = new NEMariaDB(pool); - formatId = function(id) { return id; }; + formatId = function (id) { return id; }; obj.initFunctions(); } } else { // use NeDb @@ -321,9 +341,9 @@ module.exports.CreateDB = function(meshserver) { obj.scriptFilex.ensureIndex({ fieldName: 'scriptId' }); } obj.scriptFile = new NEMongo(obj.scriptFilex); - formatId = function(id) { return id; }; + formatId = function (id) { return id; }; obj.initFunctions(); } - + return obj; } diff --git a/modules_meshcore/scripttask.js b/modules_meshcore/scripttask.js index feab779..72ad77b 100644 --- a/modules_meshcore/scripttask.js +++ b/modules_meshcore/scripttask.js @@ -17,6 +17,32 @@ var debug_flag = false; var runningJobs = []; var runningJobPIDs = {}; +var jobPingTimer = null; + +function startJobPing() { + if (jobPingTimer == null) { + jobPingTimer = setInterval(function() { + if (runningJobs.length == 0) { + clearInterval(jobPingTimer); + jobPingTimer = null; + return; + } + runningJobs.forEach(function(jobId) { + try { + mesh.SendCommand({ + "action": "plugin", + "plugin": "scripttask", + "pluginaction": "jobRunningPing", + "jobId": jobId, + "sessionid": _sessionid, + "tag": "console" + }); + } catch(e){} + }); + }, 60000); // 1 minute + } +} + var dbg = function(str) { if (debug_flag !== true) return; var fs = require('fs'); @@ -460,6 +486,7 @@ function runScript(sObj, jObj) { sObj.content = sObj.content.replace(new RegExp('#(.*?)#', 'g'), 'VAR_NOT_FOUND'); } runningJobs.push(jObj.jobId); + startJobPing(); dbg('Running Script '+ sObj._id); switch (sObj.filetype) { case 'ps1': diff --git a/nemariadb.js b/nemariadb.js index 298e915..dec83f4 100644 --- a/nemariadb.js +++ b/nemariadb.js @@ -14,29 +14,38 @@ class NEMariaDB { this._proj = null; this._limit = null; this._sort = null; - + // initialize tables this._initDB(); - + return this; } - + _initDB() { this.pool.query("CREATE TABLE IF NOT EXISTS plugin_scripttask (id VARCHAR(128) PRIMARY KEY, doc JSON)") .catch(err => { console.log("PLUGIN: ScriptTask: Error creating database table", err); }); - - this.pool.query("CREATE TABLE IF NOT EXISTS plugin_scripttask_jobs (id VARCHAR(128) PRIMARY KEY, type VARCHAR(64) DEFAULT 'job', queueTime BIGINT, dontQueueUntil BIGINT, dispatchTime BIGINT, completeTime BIGINT, node VARCHAR(256), scriptId VARCHAR(128), scriptName VARCHAR(512), replaceVars JSON, returnVal TEXT, errorVal TEXT, returnAct VARCHAR(256), runBy VARCHAR(256), jobSchedule VARCHAR(128))") + + this.pool.query("CREATE TABLE IF NOT EXISTS plugin_scripttask_jobs (id VARCHAR(128) PRIMARY KEY, type VARCHAR(64) DEFAULT 'job', queueTime INT, dontQueueUntil INT, dispatchTime INT, completeTime INT, lastPing INT, node VARCHAR(256), scriptId VARCHAR(128), scriptName VARCHAR(512), replaceVars JSON, returnVal TEXT, errorVal TEXT, returnAct VARCHAR(256), runBy VARCHAR(256), jobSchedule VARCHAR(128))") .catch(err => { console.log("PLUGIN: ScriptTask: Error creating jobs table", err); }); } _escape(val) { - if (typeof val === 'string') { - return "'" + val.replace(/'/g, "''").replace(/\\/g, "\\\\") + "'"; + // Use the native secure pool escaper to serialize standard values. + // Handles \x00, \n, \r, \, ', ", and \x1a safely. + if (typeof val === 'object' && val !== null) { + return this.pool.escape(JSON.stringify(val)); } - if (typeof val === 'number') return val; - if (val === null) return "NULL"; - if (typeof val === 'boolean') return val ? 'TRUE' : 'FALSE'; - return "'" + JSON.stringify(val).replace(/'/g, "''").replace(/\\/g, "\\\\") + "'"; + return this.pool.escape(val); + } + + _escapeCol(col) { + // Enclose in backticks and remove internal backticks to comprehensively protect column names + return '`' + col.replace(/`/g, '') + '`'; + } + + _escapeJsonPath(path) { + // Securely format JSON path keys by double quoting them inside the single-quoted string literal + return "'$.\"" + path.replace(/"/g, '\\"') + "\"'"; } _buildWhereDoc(filter) { @@ -53,8 +62,8 @@ class NEMariaDB { conditions.push("(" + andConds.join(" AND ") + ")"); } else { var val = filter[key]; - var dbKey = key === '_id' ? 'id' : `JSON_UNQUOTE(JSON_EXTRACT(doc, '$.${key}'))`; - + var dbKey = key === '_id' ? '`id`' : `JSON_UNQUOTE(JSON_EXTRACT(\`doc\`, ${this._escapeJsonPath(key)}))`; + if (val !== null && typeof val === 'object' && !Array.isArray(val)) { for (var op in val) { if (op === '$in') { @@ -65,13 +74,19 @@ class NEMariaDB { conditions.push(`${dbKey} IN (${inList})`); } } else if (op === '$gte') { - conditions.push(`${dbKey} >= ${val.$gte}`); + conditions.push(`${dbKey} >= ${this._escape(val.$gte)}`); } else if (op === '$lte') { - conditions.push(`${dbKey} <= ${val.$lte}`); + conditions.push(`${dbKey} <= ${this._escape(val.$lte)}`); } else if (op === '$gt') { - conditions.push(`${dbKey} > ${val.$gt}`); + conditions.push(`${dbKey} > ${this._escape(val.$gt)}`); } else if (op === '$lt') { - conditions.push(`${dbKey} < ${val.$lt}`); + conditions.push(`${dbKey} < ${this._escape(val.$lt)}`); + } else if (op === '$ne') { + if (val.$ne === null) { + conditions.push(`(${dbKey} IS NOT NULL AND ${dbKey} != 'null')`); + } else { + conditions.push(`${dbKey} != ${this._escape(val.$ne)}`); + } } } } else if (val === null) { @@ -83,7 +98,7 @@ class NEMariaDB { } return conditions.join(" AND "); } - + _buildWhereJob(filter) { if (!filter || Object.keys(filter).length === 0) return "1=1"; var conditions = []; @@ -98,8 +113,8 @@ class NEMariaDB { conditions.push("(" + andConds.join(" AND ") + ")"); } else { var val = filter[key]; - var dbKey = key === '_id' ? 'id' : key; - + var dbKey = key === '_id' ? '`id`' : this._escapeCol(key); + if (val !== null && typeof val === 'object' && !Array.isArray(val)) { for (var op in val) { if (op === '$in') { @@ -110,17 +125,23 @@ class NEMariaDB { conditions.push(`${dbKey} IN (${inList})`); } } else if (op === '$gte') { - conditions.push(`${dbKey} >= ${val.$gte}`); + conditions.push(`${dbKey} >= ${this._escape(val.$gte)}`); } else if (op === '$lte') { - conditions.push(`${dbKey} <= ${val.$lte}`); + conditions.push(`${dbKey} <= ${this._escape(val.$lte)}`); } else if (op === '$gt') { - conditions.push(`${dbKey} > ${val.$gt}`); + conditions.push(`${dbKey} > ${this._escape(val.$gt)}`); } else if (op === '$lt') { - conditions.push(`${dbKey} < ${val.$lt}`); + conditions.push(`${dbKey} < ${this._escape(val.$lt)}`); + } else if (op === '$ne') { + if (val.$ne === null) { + conditions.push(`${dbKey} IS NOT NULL`); + } else { + conditions.push(`${dbKey} != ${this._escape(val.$ne)}`); + } } } } else if (val === null) { - conditions.push(`${dbKey} IS NULL`); // In native column, NULL is exactly NULL + conditions.push(`${dbKey} IS NULL`); } else { conditions.push(`${dbKey} = ${this._escape(val)}`); } @@ -136,11 +157,11 @@ class NEMariaDB { this._limit = null; return this; } - + project(args) { this._proj = args; return this; } sort(args) { this._sort = args; return this; } - limit(limit) { this._limit = limit; return this; } - + limit(limit) { this._limit = Number(limit) || null; return this; } + _applyProjection(docs) { if (!this._proj) return docs; var keepFields = []; @@ -151,59 +172,59 @@ class NEMariaDB { } var ret = []; for (var doc of docs) { - var pDoc = {}; - if (keepFields.length > 0) { - for (var k of keepFields) { if (doc[k] !== undefined) pDoc[k] = doc[k]; } - if (excludeFields.indexOf('_id') === -1) pDoc._id = doc._id || doc.id; - ret.push(pDoc); - } else { - var nDoc = {...doc}; - for (var k of excludeFields) delete nDoc[k]; - ret.push(nDoc); - } + var pDoc = {}; + if (keepFields.length > 0) { + for (var k of keepFields) { if (doc[k] !== undefined) pDoc[k] = doc[k]; } + if (excludeFields.indexOf('_id') === -1) pDoc._id = doc._id || doc.id; + ret.push(pDoc); + } else { + var nDoc = { ...doc }; + for (var k of excludeFields) delete nDoc[k]; + ret.push(nDoc); + } } return ret; } - + toArray(callback) { - var self = this; - return new Promise(function(resolve, reject) { + var self = this; + return new Promise(function (resolve, reject) { var isJob = self._find && self._find.type === 'job'; - + var queryJob = () => { var wJ = self._buildWhereJob(self._find); - var q = `SELECT * FROM plugin_scripttask_jobs WHERE ${wJ}`; + var q = `SELECT * FROM \`plugin_scripttask_jobs\` WHERE ${wJ}`; if (self._sort) { var order = []; - for (var key in self._sort) order.push(`${key === '_id' ? 'id' : key} ${self._sort[key] === -1 ? 'DESC' : 'ASC'}`); + for (var key in self._sort) order.push(`${key === '_id' ? '`id`' : self._escapeCol(key)} ${self._sort[key] === -1 ? 'DESC' : 'ASC'}`); if (order.length > 0) q += " ORDER BY " + order.join(", "); } if (self._limit) q += ` LIMIT ${self._limit}`; return self.pool.query(q).then(rows => { var docs = []; for (var r of rows) { - var it = {...r}; + var it = { ...r }; it._id = it.id; delete it.id; for (var k in it) { if (typeof it[k] === 'bigint') it[k] = Number(it[k]); } if (it.replaceVars && typeof it.replaceVars === 'string') { - try { it.replaceVars = JSON.parse(it.replaceVars); } catch(e) {} + try { it.replaceVars = JSON.parse(it.replaceVars); } catch (e) { } } docs.push(it); } return docs; }); }; - + var queryDoc = () => { var wD = self._buildWhereDoc(self._find); - var q = `SELECT doc FROM plugin_scripttask WHERE ${wD}`; + var q = `SELECT \`doc\` FROM \`plugin_scripttask\` WHERE ${wD}`; if (self._sort) { var order = []; for (var key in self._sort) { - if (key === '_id') order.push(`id ${self._sort[key] === -1 ? 'DESC' : 'ASC'}`); - else order.push(`JSON_UNQUOTE(JSON_EXTRACT(doc, '$.${key}')) ${self._sort[key] === -1 ? 'DESC' : 'ASC'}`); + if (key === '_id') order.push(`\`id\` ${self._sort[key] === -1 ? 'DESC' : 'ASC'}`); + else order.push(`JSON_UNQUOTE(JSON_EXTRACT(\`doc\`, ${self._escapeJsonPath(key)})) ${self._sort[key] === -1 ? 'DESC' : 'ASC'}`); } if (order.length > 0) q += " ORDER BY " + order.join(", "); } @@ -226,55 +247,62 @@ class NEMariaDB { if (isJob) return queryJob().then(handleResults).catch(reject); if (self._find && self._find.type && self._find.type !== 'job') return queryDoc().then(handleResults).catch(reject); - - // Generic ID query (or empty query) fallback to both + queryDoc().then(docs => { if (docs.length > 0) return handleResults(docs); return queryJob().then(handleResults); }).catch(reject); }); } - + insertOne(args, options) { var self = this; - return new Promise(function(resolve, reject) { + return new Promise(function (resolve, reject) { var id = args._id; if (!id) { id = require('crypto').randomBytes(12).toString('hex'); args._id = id; } if (args.type === 'job') { - var cols = ['id']; + var cols = ['`id`']; var qmarks = ['?']; var vals = [id]; for (var k in args) { if (k === '_id' || k === 'id') continue; - cols.push(k); + cols.push(self._escapeCol(k)); qmarks.push('?'); var v = args[k]; if (typeof v === 'object' && v !== null) v = JSON.stringify(v); vals.push(v); } - self.pool.query(`INSERT INTO plugin_scripttask_jobs (${cols.join(',')}) VALUES (${qmarks.join(',')})`, vals) + self.pool.query(`INSERT INTO \`plugin_scripttask_jobs\` (${cols.join(',')}) VALUES (${qmarks.join(',')})`, vals) .then(res => resolve({ insertedId: id })) .catch(reject); } else { var docStr = JSON.stringify(args); - self.pool.query("INSERT INTO plugin_scripttask (id, doc) VALUES (?, ?)", [id, docStr]) + self.pool.query("INSERT INTO \`plugin_scripttask\` (`id`, `doc`) VALUES (?, ?)", [id, docStr]) .then(res => resolve({ insertedId: id })) .catch(reject); } }); } - + deleteOne(filter, options) { var self = this; - return new Promise(function(resolve, reject) { + return new Promise(function (resolve, reject) { + var isJob = filter.type === 'job'; + var isDoc = filter.type && filter.type !== 'job'; + var tryJob = () => self.pool.query(`DELETE FROM \`plugin_scripttask_jobs\` WHERE ${self._buildWhereJob(filter)} LIMIT 1`); + var tryDoc = () => self.pool.query(`DELETE FROM \`plugin_scripttask\` WHERE ${self._buildWhereDoc(filter)} LIMIT 1`); + + if (isJob) return tryJob().then(res => resolve({ deletedCount: res.affectedRows })).catch(reject); + if (isDoc) return tryDoc().then(res => resolve({ deletedCount: res.affectedRows })).catch(reject); + var count = 0; - self.pool.query(`DELETE FROM plugin_scripttask WHERE ${self._buildWhereDoc(filter)} LIMIT 1`) + tryDoc().catch(err => { if (err.errno === 1054) return { affectedRows: 0 }; throw err; }) .then(res => { count += res.affectedRows; - return self.pool.query(`DELETE FROM plugin_scripttask_jobs WHERE ${self._buildWhereJob(filter)} LIMIT 1`); + return tryJob().catch(err => { if (err.errno === 1054) return { affectedRows: 0 }; throw err; }); }) .then(res => { count += res.affectedRows; @@ -283,15 +311,23 @@ class NEMariaDB { .catch(reject); }); } - + deleteMany(filter, options) { var self = this; - return new Promise(function(resolve, reject) { + return new Promise(function (resolve, reject) { + var isJob = filter.type === 'job'; + var isDoc = filter.type && filter.type !== 'job'; + var tryJob = () => self.pool.query(`DELETE FROM \`plugin_scripttask_jobs\` WHERE ${self._buildWhereJob(filter)}`); + var tryDoc = () => self.pool.query(`DELETE FROM \`plugin_scripttask\` WHERE ${self._buildWhereDoc(filter)}`); + + if (isJob) return tryJob().then(res => resolve({ deletedCount: res.affectedRows })).catch(reject); + if (isDoc) return tryDoc().then(res => resolve({ deletedCount: res.affectedRows })).catch(reject); + var count = 0; - self.pool.query(`DELETE FROM plugin_scripttask WHERE ${self._buildWhereDoc(filter)}`) + tryDoc().catch(err => { if (err.errno === 1054) return { affectedRows: 0 }; throw err; }) .then(res => { count += res.affectedRows; - return self.pool.query(`DELETE FROM plugin_scripttask_jobs WHERE ${self._buildWhereJob(filter)}`); + return tryJob().catch(err => { if (err.errno === 1054) return { affectedRows: 0 }; throw err; }); }) .then(res => { count += res.affectedRows; @@ -300,141 +336,140 @@ class NEMariaDB { .catch(reject); }); } - + updateOne(filter, update, options) { var self = this; if (options == null) options = {}; if (options.upsert == null) options.upsert = false; - - return new Promise(function(resolve, reject) { + + return new Promise(function (resolve, reject) { var tryUpdateJob = () => { var wJ = self._buildWhereJob(filter); - return self.pool.query(`SELECT id FROM plugin_scripttask_jobs WHERE ${wJ} LIMIT 1`) - .then(rows => { - if (rows.length === 0) return { matchedCount: 0, modifiedCount: 0 }; - var updates = [], vals = []; - var src = update.$set ? update.$set : update; - for (var k in src) { - if (k === '_id' || k === 'id') continue; - updates.push(`${k} = ?`); - var v = src[k]; - if (typeof v === 'object' && v !== null) v = JSON.stringify(v); - vals.push(v); - } - if (updates.length > 0) { - vals.push(rows[0].id); - return self.pool.query(`UPDATE plugin_scripttask_jobs SET ${updates.join(', ')} WHERE id = ?`, vals) - .then(() => ({ matchedCount: 1, modifiedCount: 1, upsertedId: rows[0].id })); - } else { - return { matchedCount: 1, modifiedCount: 0 }; - } - }); + return self.pool.query(`SELECT \`id\` FROM \`plugin_scripttask_jobs\` WHERE ${wJ} LIMIT 1`) + .then(rows => { + if (rows.length === 0) return { matchedCount: 0, modifiedCount: 0 }; + var updates = [], vals = []; + var src = update.$set ? update.$set : update; + for (var k in src) { + if (k === '_id' || k === 'id') continue; + updates.push(`${self._escapeCol(k)} = ?`); + var v = src[k]; + if (typeof v === 'object' && v !== null) v = JSON.stringify(v); + vals.push(v); + } + if (updates.length > 0) { + vals.push(rows[0].id); + return self.pool.query(`UPDATE \`plugin_scripttask_jobs\` SET ${updates.join(', ')} WHERE \`id\` = ?`, vals) + .then(() => ({ matchedCount: 1, modifiedCount: 1, upsertedId: rows[0].id })); + } else { + return { matchedCount: 1, modifiedCount: 0 }; + } + }); }; - + var tryUpdateDoc = () => { var wD = self._buildWhereDoc(filter); - return self.pool.query(`SELECT id, doc FROM plugin_scripttask WHERE ${wD} LIMIT 1`) - .then(rows => { - if (rows.length === 0) return { matchedCount: 0, modifiedCount: 0 }; - var id = rows[0].id; - var doc = typeof rows[0].doc === 'string' ? JSON.parse(rows[0].doc) : rows[0].doc; - var modified = false; - if (update.$set) { - for (var k in update.$set) doc[k] = update.$set[k]; - modified = true; - } else { - doc = { ...doc, ...update }; - if (!doc._id) doc._id = id; - modified = true; - } - if (modified) { - return self.pool.query("UPDATE plugin_scripttask SET doc = ? WHERE id = ?", [JSON.stringify(doc), id]) - .then(() => ({ matchedCount: 1, modifiedCount: 1, upsertedId: id })); - } else { - return { matchedCount: 1, modifiedCount: 0 }; - } - }); + return self.pool.query(`SELECT \`id\`, \`doc\` FROM \`plugin_scripttask\` WHERE ${wD} LIMIT 1`) + .then(rows => { + if (rows.length === 0) return { matchedCount: 0, modifiedCount: 0 }; + var id = rows[0].id; + var doc = typeof rows[0].doc === 'string' ? JSON.parse(rows[0].doc) : rows[0].doc; + var modified = false; + if (update.$set) { + for (var k in update.$set) doc[k] = update.$set[k]; + modified = true; + } else { + doc = { ...doc, ...update }; + if (!doc._id) doc._id = id; + modified = true; + } + if (modified) { + return self.pool.query("UPDATE \`plugin_scripttask\` SET \`doc\` = ? WHERE \`id\` = ?", [JSON.stringify(doc), id]) + .then(() => ({ matchedCount: 1, modifiedCount: 1, upsertedId: id })); + } else { + return { matchedCount: 1, modifiedCount: 0 }; + } + }); }; - + var isJob = filter.type === 'job'; var isDoc = filter.type && filter.type !== 'job'; - + if (isJob) return tryUpdateJob().then(res => { - if (res.matchedCount === 0 && options.upsert) { - var newDoc = { ...filter, ...(update.$set || {}) }; - return self.insertOne(newDoc).then(r => ({matchedCount:0, modifiedCount:1, upsertedId: r.insertedId})); - } - resolve(res); + if (res.matchedCount === 0 && options.upsert) { + var newDoc = { ...filter, ...(update.$set || {}) }; + return self.insertOne(newDoc).then(r => ({ matchedCount: 0, modifiedCount: 1, upsertedId: r.insertedId })); + } + resolve(res); }).catch(reject); - + if (isDoc) return tryUpdateDoc().then(res => { - if (res.matchedCount === 0 && options.upsert) { - var newDoc = { ...filter, ...(update.$set || {}) }; - return self.insertOne(newDoc).then(r => ({matchedCount:0, modifiedCount:1, upsertedId: r.insertedId})); - } - resolve(res); + if (res.matchedCount === 0 && options.upsert) { + var newDoc = { ...filter, ...(update.$set || {}) }; + return self.insertOne(newDoc).then(r => ({ matchedCount: 0, modifiedCount: 1, upsertedId: r.insertedId })); + } + resolve(res); }).catch(reject); - // generic branch tryUpdateDoc().then(res => { if (res.matchedCount > 0) return resolve(res); return tryUpdateJob().then(res2 => { if (res2.matchedCount === 0 && options.upsert) { - var newDoc = { ...filter, ...(update.$set || {}) }; // fallback to insert doc - return self.insertOne(newDoc).then(r => resolve({matchedCount:0, modifiedCount:1, upsertedId: r.insertedId})); + var newDoc = { ...filter, ...(update.$set || {}) }; // fallback to insert doc + return self.insertOne(newDoc).then(r => resolve({ matchedCount: 0, modifiedCount: 1, upsertedId: r.insertedId })); } resolve(res2); }); }).catch(reject); }); } - + updateMany(filter, update, options) { var self = this; if (options == null) options = {}; if (options.upsert == null) options.upsert = false; - - return new Promise(function(resolve, reject) { + + return new Promise(function (resolve, reject) { var tryUpdateJob = () => { var wJ = self._buildWhereJob(filter); - return self.pool.query(`SELECT id FROM plugin_scripttask_jobs WHERE ${wJ}`) - .then(rows => { - if (rows.length === 0) return { matchedCount: 0, modifiedCount: 0 }; - var updatesQ = [], vals = []; - var src = update.$set ? update.$set : update; - for (var k in src) { - if (k === '_id' || k === 'id') continue; - updatesQ.push(`${k} = ?`); - var v = src[k]; - if (typeof v === 'object' && v !== null) v = JSON.stringify(v); - vals.push(v); - } - if (updatesQ.length > 0) { - var proms = rows.map(r => self.pool.query(`UPDATE plugin_scripttask_jobs SET ${updatesQ.join(', ')} WHERE id = ?`, [...vals, r.id])); - return Promise.all(proms).then(() => ({ matchedCount: rows.length, modifiedCount: rows.length })); - } else { - return { matchedCount: rows.length, modifiedCount: 0 }; - } - }); + return self.pool.query(`SELECT \`id\` FROM \`plugin_scripttask_jobs\` WHERE ${wJ}`) + .then(rows => { + if (rows.length === 0) return { matchedCount: 0, modifiedCount: 0 }; + var updatesQ = [], vals = []; + var src = update.$set ? update.$set : update; + for (var k in src) { + if (k === '_id' || k === 'id') continue; + updatesQ.push(`${self._escapeCol(k)} = ?`); + var v = src[k]; + if (typeof v === 'object' && v !== null) v = JSON.stringify(v); + vals.push(v); + } + if (updatesQ.length > 0) { + var proms = rows.map(r => self.pool.query(`UPDATE \`plugin_scripttask_jobs\` SET ${updatesQ.join(', ')} WHERE \`id\` = ?`, [...vals, r.id])); + return Promise.all(proms).then(() => ({ matchedCount: rows.length, modifiedCount: rows.length })); + } else { + return { matchedCount: rows.length, modifiedCount: 0 }; + } + }); }; - + var tryUpdateDoc = () => { var wD = self._buildWhereDoc(filter); - return self.pool.query(`SELECT id, doc FROM plugin_scripttask WHERE ${wD}`) - .then(rows => { - if (rows.length === 0) return { matchedCount: 0, modifiedCount: 0 }; - var proms = rows.map(r => { - var doc = typeof r.doc === 'string' ? JSON.parse(r.doc) : r.doc; - if (update.$set) { - for (var k in update.$set) doc[k] = update.$set[k]; - } else { - doc = { ...doc, ...update }; - if (!doc._id) doc._id = r.id; - } - return self.pool.query("UPDATE plugin_scripttask SET doc = ? WHERE id = ?", [JSON.stringify(doc), r.id]); + return self.pool.query(`SELECT \`id\`, \`doc\` FROM \`plugin_scripttask\` WHERE ${wD}`) + .then(rows => { + if (rows.length === 0) return { matchedCount: 0, modifiedCount: 0 }; + var proms = rows.map(r => { + var doc = typeof r.doc === 'string' ? JSON.parse(r.doc) : r.doc; + if (update.$set) { + for (var k in update.$set) doc[k] = update.$set[k]; + } else { + doc = { ...doc, ...update }; + if (!doc._id) doc._id = r.id; + } + return self.pool.query("UPDATE \`plugin_scripttask\` SET \`doc\` = ? WHERE \`id\` = ?", [JSON.stringify(doc), r.id]); + }); + return Promise.all(proms).then(() => ({ matchedCount: rows.length, modifiedCount: rows.length })); }); - return Promise.all(proms).then(() => ({ matchedCount: rows.length, modifiedCount: rows.length })); - }); }; var isJob = filter.type === 'job'; diff --git a/scripttask.js b/scripttask.js index b3c7f88..5001ad8 100644 --- a/scripttask.js +++ b/scripttask.js @@ -15,32 +15,33 @@ module.exports.scripttask = function (parent) { obj.intervalTimer = null; obj.debug = obj.meshServer.debug; obj.VIEWS = __dirname + '/views/'; - obj.exports = [ + obj.exports = [ 'onDeviceRefreshEnd', 'resizeContent', 'historyData', 'variableData', + 'newScriptTree', 'malix_triggerOption' ]; - - obj.malix_triggerOption = function(selectElem) { + + obj.malix_triggerOption = function (selectElem) { selectElem.options.add(new Option("ScriptTask - Run Script", "scripttask_runscript")); } - obj.malix_triggerFields_scripttask_runscript = function() { - + obj.malix_triggerFields_scripttask_runscript = function () { + } - obj.resetQueueTimer = function() { + obj.resetQueueTimer = function () { clearTimeout(obj.intervalTimer); obj.intervalTimer = setInterval(obj.queueRun, 1 * 60 * 1000); // every minute }; - - obj.server_startup = function() { - obj.meshServer.pluginHandler.scripttask_db = require (__dirname + '/db.js').CreateDB(obj.meshServer); + + obj.server_startup = function () { + obj.meshServer.pluginHandler.scripttask_db = require(__dirname + '/db.js').CreateDB(obj.meshServer); obj.db = obj.meshServer.pluginHandler.scripttask_db; obj.resetQueueTimer(); }; - - obj.onDeviceRefreshEnd = function() { + + obj.onDeviceRefreshEnd = function () { pluginHandler.registerPluginTab({ tabTitle: 'ScriptTask', tabId: 'pluginScriptTask' @@ -48,7 +49,7 @@ module.exports.scripttask = function (parent) { QA('pluginScriptTask', '