Added deviceinfo support to meshCtrl.

Ylian Saint-Hilaire committed May 2, 2020 at 13:49 UTC a2b944cd3384724beb9f754e906e5420ca44b61b
6 files changed +253 -11
meshctrl.js
+203 -1
@@ -7,7 +7,7 @@ try { require('ws'); } catch (ex) { console.log('Missing module "ws", type "npm
7 var settings = {};
8 const crypto = require('crypto');
9 const args = require('minimist')(process.argv.slice(2));
10 -const possibleCommands = ['listusers', 'listdevicegroups', 'listdevices', 'listusersofdevicegroup', 'serverinfo', 'userinfo', 'adduser', 'removeuser', 'adddevicegroup', 'removedevicegroup', 'broadcast', 'showevents', 'addusertodevicegroup', 'removeuserfromdevicegroup', 'addusertodevice', 'removeuserfromdevice', 'sendinviteemail', 'generateinvitelink', 'config', 'movetodevicegroup'];
10 +const possibleCommands = ['listusers', 'listdevicegroups', 'listdevices', 'listusersofdevicegroup', 'serverinfo', 'userinfo', 'adduser', 'removeuser', 'adddevicegroup', 'removedevicegroup', 'broadcast', 'showevents', 'addusertodevicegroup', 'removeuserfromdevicegroup', 'addusertodevice', 'removeuserfromdevice', 'sendinviteemail', 'generateinvitelink', 'config', 'movetodevicegroup', 'deviceinfo'];
11 if (args.proxy != null) { try { require('https-proxy-agent'); } catch (ex) { console.log('Missing module "https-proxy-agent", type "npm install https-proxy-agent" to install it.'); return; } }
12
13 if (args['_'].length == 0) {
@@ -22,6 +22,7 @@ if (args['_'].length == 0) {
22 console.log(" ListDevices - List devices.");
23 console.log(" ListDeviceGroups - List device groups.");
24 console.log(" ListUsersOfDeviceGroup - List the users in a device group.");
25 + console.log(" DeviceInfo - Show information about a device.");
26 console.log(" Config - Perform operation on config.json file.");
27 console.log(" AddUser - Create a new user account.");
28 console.log(" RemoveUser - Delete a user account.");
@@ -64,6 +65,11 @@ if (args['_'].length == 0) {
65 else { ok = true; }
66 break;
67 }
68 + case 'deviceinfo': {
69 + if (args.id == null) { console.log("Missing device id, use --id [deviceid]"); }
70 + else { ok = true; }
71 + break;
72 + }
73 case 'addusertodevicegroup': {
74 if ((args.id == null) && (args.group == null)) { console.log("Device group identifier missing, use --id [groupid] or --group [groupname]"); }
75 else if (args.userid == null) { console.log("Add user to group missing useid, use --userid [userid]"); }
@@ -364,6 +370,17 @@ if (args['_'].length == 0) {
370 console.log(" --msg [message] - Message to display.");
371 break;
372 }
373 + case 'deviceinfo': {
374 + console.log("Display information about a device, Example usages:\r\n");
375 + console.log(" MeshCtrl DeviceInfo --id deviceid");
376 + console.log(" MeshCtrl DeviceInfo --id deviceid --json");
377 + console.log("\r\nRequired arguments:\r\n");
378 + console.log(" --id [deviceid] - The device identifier.");
379 + console.log("\r\nOptional arguments:\r\n");
380 + console.log(" --raw - Output raw data in JSON format.");
381 + console.log(" --json - Give results in JSON format.");
382 + break;
383 + }
384 default: {
385 console.log("Get help on an action. Type:\r\n\r\n help [action]\r\n\r\nPossible actions are: " + possibleCommands.join(', ') + '.');
386 }
@@ -696,6 +713,13 @@ function serverConnect() {
713 console.log('Connected. Press ctrl-c to end.');
714 break;
715 }
716 + case 'deviceinfo': {
717 + settings.deviceinfocount = 3;
718 + ws.send(JSON.stringify({ action: 'getnetworkinfo', nodeid: args.id, nodeinfo: true, responseid: 'meshctrl' }));
719 + ws.send(JSON.stringify({ action: 'lastconnect', nodeid: args.id, nodeinfo: true, responseid: 'meshctrl' }));
720 + ws.send(JSON.stringify({ action: 'getsysinfo', nodeid: args.id, nodeinfo: true, responseid: 'meshctrl' }));
721 + break;
722 + }
723 }
724 });
725
@@ -738,6 +762,32 @@ function serverConnect() {
762 }
763 break;
764 }
765 + case 'getsysinfo': { // DEVICEINFO
766 + if (settings.cmd == 'deviceinfo') {
767 + if (data.result) {
768 + console.log(data.result);
769 + process.exit();
770 + } else {
771 + settings.sysinfo = data;
772 + if (--settings.deviceinfocount == 0) { displayDeviceInfo(settings.sysinfo, settings.lastconnect, settings.networking); process.exit(); }
773 + }
774 + }
775 + break;
776 + }
777 + case 'lastconnect': {
778 + if (settings.cmd == 'deviceinfo') {
779 + settings.lastconnect = (data.result)?null:data;
780 + if (--settings.deviceinfocount == 0) { displayDeviceInfo(settings.sysinfo, settings.lastconnect, settings.networking); process.exit(); }
781 + }
782 + break;
783 + }
784 + case 'getnetworkinfo': {
785 + if (settings.cmd == 'deviceinfo') {
786 + settings.networking = (data.result) ? null : data;
787 + if (--settings.deviceinfocount == 0) { displayDeviceInfo(settings.sysinfo, settings.lastconnect, settings.networking); process.exit(); }
788 + }
789 + break;
790 + }
791 case 'adduser': // ADDUSER
792 case 'deleteuser': // REMOVEUSER
793 case 'createmesh': // ADDDEVICEGROUP
@@ -919,3 +969,155 @@ function encodeCookie(o, key) {
969 // Generate a random Intel AMT password
970 function checkAmtPassword(p) { return (p.length > 7) && (/\d/.test(p)) && (/[a-z]/.test(p)) && (/[A-Z]/.test(p)) && (/\W/.test(p)); }
971 function getRandomAmtPassword() { var p; do { p = Buffer.from(crypto.randomBytes(9), 'binary').toString('base64').split('/').join('@'); } while (checkAmtPassword(p) == false); return p; }
972 +function format(format) { var args = Array.prototype.slice.call(arguments, 1); return format.replace(/{(\d+)}/g, function (match, number) { return typeof args[number] != 'undefined' ? args[number] : match; }); };
973 +
974 +function displayDeviceInfo(sysinfo, lastconnect, network) {
975 + var node = sysinfo.node;
976 + var hardware = sysinfo.hardware;
977 + var info = {};
978 +
979 + if (network != null) { sysinfo.netif = network.netif; }
980 + if (lastconnect != null) { node.lastconnect = lastconnect.time; node.lastaddr = lastconnect.addr; }
981 + if (args.raw) { console.log(JSON.stringify(sysinfo, ' ', 2)); return; }
982 +
983 + // Operating System
984 + if ((hardware.windows && hardware.windows.osinfo) || node.osdesc) {
985 + var output = {}, outputCount = 0;
986 + if (node.rname) { output["Name"] = node.rname; outputCount++; }
987 + if (node.osdesc) { output["Version"] = node.osdesc; outputCount++; }
988 + if (hardware.windows && hardware.windows.osinfo) { var m = hardware.windows.osinfo; if (m.OSArchitecture) { output["Architecture"] = m.OSArchitecture; outputCount++; } }
989 + if (outputCount > 0) { info["Operating System"] = output; }
990 + }
991 +
992 + // MeshAgent
993 + if (node.agent) {
994 + var output = {}, outputCount = 0;
995 + var agentsStr = ["Unknown", "Windows 32bit console", "Windows 64bit console", "Windows 32bit service", "Windows 64bit service", "Linux 32bit", "Linux 64bit", "MIPS", "XENx86", "Android ARM", "Linux ARM", "MacOS 32bit", "Android x86", "PogoPlug ARM", "Android APK", "Linux Poky x86-32bit", "MacOS 64bit", "ChromeOS", "Linux Poky x86-64bit", "Linux NoKVM x86-32bit", "Linux NoKVM x86-64bit", "Windows MinCore console", "Windows MinCore service", "NodeJS", "ARM-Linaro", "ARMv6l / ARMv7l", "ARMv8 64bit", "ARMv6l / ARMv7l / NoKVM", "Unknown", "Unknown", "FreeBSD x86-64"];
996 + if ((node.agent != null) && (node.agent.id != null) && (node.agent.ver != null)) {
997 + var str = '';
998 + if (node.agent.id <= agentsStr.length) { str = agentsStr[node.agent.id]; } else { str = agentsStr[0]; }
999 + if (node.agent.ver != 0) { str += ' v' + node.agent.ver; }
1000 + output["Mesh Agent"] = str; outputCount++;
1001 + }
1002 + if ((node.conn & 1) != 0) {
1003 + output["Last agent connection"] = "Connected now"; outputCount++;
1004 + } else {
1005 + if (node.lastconnect) { output["Last agent connection"] = new Date(node.lastconnect).toLocaleString(); outputCount++; }
1006 + }
1007 + if (node.lastaddr) {
1008 + var splitip = node.lastaddr.split(':');
1009 + if (splitip.length > 2) {
1010 + output["Last agent address"] = node.lastaddr; outputCount++; // IPv6
1011 + } else {
1012 + output["Last agent address"] = splitip[0]; outputCount++; // IPv4
1013 + }
1014 + }
1015 + if (outputCount > 0) { info["Mesh Agent"] = output; }
1016 + }
1017 +
1018 + // Networking
1019 + if (network.netif != null) {
1020 + var output = {}, outputCount = 0, minfo = {};
1021 + for (var i in network.netif) {
1022 + var m = network.netif[i], moutput = {}, moutputCount = 0;
1023 + if (m.desc) { moutput["Description"] = m.desc; moutputCount++; }
1024 + if (m.mac) {
1025 + if (m.gatewaymac) {
1026 + moutput["MAC Layer"] = format("MAC: {0}, Gateway: {1}", m.mac, m.gatewaymac); moutputCount++;
1027 + } else {
1028 + moutput["MAC Layer"] = format("MAC: {0}", m.mac); moutputCount++;
1029 + }
1030 + }
1031 + if (m.v4addr && (m.v4addr != '0.0.0.0')) {
1032 + if (m.v4gateway && m.v4mask) {
1033 + moutput["IPv4 Layer"] = format("IP: {0}, Mask: {1}, Gateway: {2}", m.v4addr, m.v4mask, m.v4gateway); moutputCount++;
1034 + } else {
1035 + moutput["IPv4 Layer"] = format("IP: {0}", m.v4addr); moutputCount++;
1036 + }
1037 + }
1038 + if (moutputCount > 0) { minfo[m.name + (m.dnssuffix ? (', ' + m.dnssuffix) : '')] = moutput; info["Networking"] = minfo; }
1039 + }
1040 + }
1041 +
1042 + // Intel AMT
1043 + if (node.intelamt != null) {
1044 + var output = {}, outputCount = 0;
1045 + output["Version"] = (node.intelamt.ver) ? ('v' + node.intelamt.ver) : ('<i>' + "Unknown" + '</i>'); outputCount++;
1046 + var provisioningStates = { 0: "Not Activated (Pre)", 1: "Not Activated (In)", 2: "Activated" };
1047 + var provisioningMode = '';
1048 + if ((node.intelamt.state == 2) && node.intelamt.flags) { if (node.intelamt.flags & 2) { provisioningMode = (', ' + "Client Control Mode (CCM)"); } else if (node.intelamt.flags & 4) { provisioningMode = (', ' + "Admin Control Mode (ACM)"); } }
1049 + output["Provisioning State"] = ((node.intelamt.state) ? (provisioningStates[node.intelamt.state]) : ('<i>' + "Unknown" + '</i>')) + provisioningMode; outputCount++;
1050 + output["Security"] = (node.intelamt.tls == 1) ? "Secured using TLS" : "TLS is not setup"; outputCount++;
1051 + output["Admin Credentials"] = (node.intelamt.user == null || node.intelamt.user == '') ? "Not Known" : "Known"; outputCount++;
1052 + if (outputCount > 0) { info["Intel Active Management Technology (Intel AMT)"] = output; }
1053 + }
1054 +
1055 + if (hardware.identifiers) {
1056 + var output = {}, outputCount = 0, ident = hardware.identifiers;
1057 + // BIOS
1058 + if (ident.bios_vendor) { output["Vendor"] = ident.bios_vendor; outputCount++; }
1059 + if (ident.bios_version) { output["Version"] = ident.bios_version; outputCount++; }
1060 + if (outputCount > 0) { info["BIOS"] = output; }
1061 + output = {}, outputCount = 0;
1062 +
1063 + // Motherboard
1064 + if (ident.board_vendor) { output["Vendor"] = ident.board_vendor; outputCount++; }
1065 + if (ident.board_name) { output["Name"] = ident.board_name; outputCount++; }
1066 + if (ident.board_serial && (ident.board_serial != '')) { output["Serial"] = ident.board_serial; outputCount++; }
1067 + if (ident.board_version) { output["Version"] = ident.board_version; }
1068 + if (ident.product_uuid) { output["Identifier"] = ident.product_uuid; }
1069 + if (ident.cpu_name) { output["CPU"] = ident.cpu_name; }
1070 + if (ident.gpu_name) { for (var i in ident.gpu_name) { output["GPU" + (parseInt(i) + 1)] = ident.gpu_name[i]; } }
1071 + if (outputCount > 0) { info["Motherboard"] = output; }
1072 + }
1073 +
1074 + // Memory
1075 + if (hardware.windows) {
1076 + if (hardware.windows.memory) {
1077 + var output = {}, outputCount = 0, minfo = {};
1078 + hardware.windows.memory.sort(function (a, b) { if (a.BankLabel > b.BankLabel) return 1; if (a.BankLabel < b.BankLabel) return -1; return 0; });
1079 + for (var i in hardware.windows.memory) {
1080 + var m = hardware.windows.memory[i], moutput = {}, moutputCount = 0;
1081 + if (m.Capacity) { moutput["Capacity/Speed"] = (m.Capacity / 1024 / 1024) + " Mb, " + m.Speed + " Mhz"; moutputCount++; }
1082 + if (m.PartNumber) { moutput["Part Number"] = ((m.Manufacturer && m.Manufacturer != 'Undefined') ? (m.Manufacturer + ', ') : '') + m.PartNumber; moutputCount++; }
1083 + if (moutputCount > 0) { minfo[m.BankLabel] = moutput; info["Memory"] = minfo; }
1084 + }
1085 + }
1086 + }
1087 +
1088 + // Storage
1089 + if (hardware.identifiers && ident.storage_devices) {
1090 + var output = {}, outputCount = 0, minfo = {};
1091 + // Sort Storage
1092 + ident.storage_devices.sort(function (a, b) { if (a.Caption > b.Caption) return 1; if (a.Caption < b.Caption) return -1; return 0; });
1093 + for (var i in ident.storage_devices) {
1094 + var m = ident.storage_devices[i], moutput = {};
1095 + if (m.Size) {
1096 + if (m.Model && (m.Model != m.Caption)) { moutput["Model"] = m.Model; outputCount++; }
1097 + if ((typeof m.Size == 'string') && (parseInt(m.Size) == m.Size)) { m.Size = parseInt(m.Size); }
1098 + if (typeof m.Size == 'number') { moutput["Capacity"] = Math.floor(m.Size / 1024 / 1024) + 'Mb'; outputCount++; }
1099 + if (typeof m.Size == 'string') { moutput["Capacity"] = m.Size; outputCount++; }
1100 + if (moutputCount > 0) { minfo[m.Caption] = moutput; info["Storage"] = minfo; }
1101 + }
1102 + }
1103 + }
1104 +
1105 + // Display everything
1106 + if (args.json) {
1107 + console.log(JSON.stringify(info, ' ', 2));
1108 + } else {
1109 + for (var i in info) {
1110 + console.log('--- ' + i + ' ---');
1111 + for (var j in info[i]) {
1112 + if (typeof info[i][j] == 'string') {
1113 + console.log(' ' + j + ': ' + info[i][j]);
1114 + } else {
1115 + console.log(' ' + j + ':');
1116 + for (var k in info[i][j]) {
1117 + console.log(' ' + k + ': ' + info[i][j][k]);
1118 + }
1119 + }
1120 + }
1121 + }
1122 + }
1123 +}
\ No newline at end of file
meshuser.js
+17 -4
@@ -616,9 +616,13 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
616 }
617 case 'getsysinfo':
618 {
619 + if (common.validateString(command.nodeid, 1, 1024) == false) break; // Check the nodeid
620 + if (command.nodeid.indexOf('/') == -1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
621 + if ((command.nodeid.split('/').length != 3) || (command.nodeid.split('/')[1] != domain.id)) return; // Invalid domain, operation only valid for current domain
622 +
623 // Get the node and the rights for this node
624 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
621 - if (visible == false) return;
625 + if (visible == false) { try { ws.send(JSON.stringify({ action: 'getsysinfo', nodeid: command.nodeid, tag: command.tag, noinfo: true, result: 'Invalid device id' })); } catch (ex) { } return; }
626 // Query the database system information
627 db.Get('si' + command.nodeid, function (err, docs) {
628 if ((docs != null) && (docs.length > 0)) {
@@ -629,9 +633,10 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
633 delete doc.type;
634 delete doc.domain;
635 delete doc._id;
636 + if (command.nodeinfo === true) { doc.node = node; doc.rights = rights; }
637 try { ws.send(JSON.stringify(doc)); } catch (ex) { }
638 } else {
634 - try { ws.send(JSON.stringify({ action: 'getsysinfo', nodeid: node._id, tag: command.tag, noinfo: true })); } catch (ex) { }
639 + try { ws.send(JSON.stringify({ action: 'getsysinfo', nodeid: node._id, tag: command.tag, noinfo: true, result: 'Invalid device id' })); } catch (ex) { }
640 }
641 });
642 });
@@ -639,13 +644,20 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
644 }
645 case 'lastconnect':
646 {
647 + if (common.validateString(command.nodeid, 1, 1024) == false) break; // Check the nodeid
648 + if (command.nodeid.indexOf('/') == -1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
649 + if ((command.nodeid.split('/').length != 3) || (command.nodeid.split('/')[1] != domain.id)) return; // Invalid domain, operation only valid for current domain
650 +
651 // Get the node and the rights for this node
652 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
644 - if (visible == false) return;
653 + if (visible == false) { try { ws.send(JSON.stringify({ action: 'lastconnect', nodeid: command.nodeid, tag: command.tag, noinfo: true, result: 'Invalid device id' })); } catch (ex) { } return; }
654 +
655 // Query the database for the last time this node connected
656 db.Get('lc' + command.nodeid, function (err, docs) {
657 if ((docs != null) && (docs.length > 0)) {
658 try { ws.send(JSON.stringify({ action: 'lastconnect', nodeid: command.nodeid, time: docs[0].time, addr: docs[0].addr })); } catch (ex) { }
659 + } else {
660 + try { ws.send(JSON.stringify({ action: 'lastconnect', nodeid: command.nodeid, tag: command.tag, noinfo: true, result: 'No data' })); } catch (ex) { }
661 }
662 });
663 });
@@ -3187,11 +3199,12 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
3199 {
3200 // Argument validation
3201 if (common.validateString(command.nodeid, 1, 1024) == false) break; // Check nodeid
3202 + if (command.nodeid.indexOf('/') == -1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
3203 if ((command.nodeid.split('/').length != 3) || (command.nodeid.split('/')[1] != domain.id)) return; // Invalid domain, operation only valid for current domain
3204
3205 // Get the node and the rights for this node
3206 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
3194 - if (visible == false) return;
3207 + if (visible == false) { try { ws.send(JSON.stringify({ action: 'getnetworkinfo', nodeid: command.nodeid, tag: command.tag, noinfo: true, result: 'Invalid device id' })); } catch (ex) { } return; }
3208
3209 // Get network information about this node
3210 db.Get('if' + node._id, function (err, netinfos) {
public/images/icon-film.png
Binary files /dev/null and b/public/images/icon-film.png differ
public/scripts/agent-desktop-0.0.2.js
+13
@@ -196,6 +196,7 @@ var CreateAgentRemoteDesktop = function (canvasid, scrolldiv) {
196 if (obj.debugmode > 1) { console.log("KRecv(" + str.length + "): " + rstr2hex(str.substring(0, Math.min(str.length, 40)))); }
197 if (str.length < 4) return;
198 var cmdmsg = null, X = 0, Y = 0, command = ReadShort(str, 0), cmdsize = ReadShort(str, 2), jumboAdd = 0;
199 + if (obj.recordedData != null) { obj.recordedData.push({ t: Date.now(), d: str }); }
200 if ((command == 27) && (cmdsize == 8)) {
201 // Jumbo packet
202 if (str.length < 12) return;
@@ -777,6 +778,18 @@ var CreateAgentRemoteDesktop = function (canvasid, scrolldiv) {
778 return true;
779 }
780
781 + obj.StartRecording = function () {
782 + obj.recordedData = [];
783 + obj.recordedStart = Date.now();
784 + }
785 +
786 + obj.StopRecording = function () {
787 + var r = obj.recordedData;
788 + delete obj.recordedData;
789 + delete obj.recordedStart;
790 + return r;
791 + }
792 +
793 // Private method
794 obj.MuchTheSame = function (a, b) { return (Math.abs(a - b) < 4); }
795 obj.Debug = function (msg) { console.log(msg); }
public/styles/style.css
+1 -1
@@ -2339,7 +2339,7 @@ a {
2339 -ms-grid-row: 4;
2340 }
2341
2342 -#DeskChatButton, #DeskNotifyButton, #DeskOpenWebButton, #DeskBackgroundButton, #DeskSaveImageButton {
2342 +#DeskChatButton, #DeskNotifyButton, #DeskOpenWebButton, #DeskBackgroundButton, #DeskSaveImageButton, #DeskRecordButton {
2343 float: right;
2344 margin-top: 1px;
2345 margin-right: 4px;
views/default.handlebars
+19 -5
@@ -572,6 +572,7 @@
572 <span id=DeskOpenWebButton title="Open a web address on the remote computer"><img src='images/icon-url2.png' onclick=deviceUrlFunction() height=16 width=16 style=padding-top:2px /></span>
573 <span id=DeskBackgroundButton title="Toggle remote desktop background"><img src='images/icon-background.png' onclick=deviceToggleBackground(event) height=16 width=16 style=padding-top:2px /></span>
574 <span id=DeskSaveImageButton title="Save a screenshot of the remote desktop"><img src='images/icon-camera.png' onclick=deskSaveImage() height=16 width=16 style=padding-top:2px /></span>
575 + <span id=DeskRecordButton title="Record remote desktop session to file" style="display:none"><img src='images/icon-film.png' onclick=deskRecordSession() height=16 width=16 style=padding-top:2px /></span>
576 </div>
577 <div>
578 <select id="deskkeys">
@@ -5759,9 +5760,6 @@
5760 desktopNode = currentNode;
5761 updateDesktopButtons();
5762 deskAdjust();
5762 -
5763 - // On some browsers like IE, we can't save screen shots. Hide the scheenshot/capture buttons.
5764 - if (!Q('Desk')['toBlob']) { QV('DeskSaveImageButton', false); }
5763 }
5764
5765 // Show and enable the right buttons
@@ -5797,7 +5795,6 @@
5795 QE('connectbutton1', online);
5796 var hwonline = ((currentNode.conn & 6) != 0); // If CIRA (2) or AMT (4) connected, enable hardware terminal
5797 QE('connectbutton1h', hwonline);
5800 - QE('DeskSaveImageButton', deskState == 3);
5798 QV('deskFocusBtn', (desktop != null) && (desktop.contype == 2) && (deskState != 0) && (desktopsettings.showfocus));
5799 QV('DeskClip', (currentNode.agent) && (currentNode.agent.id != 11) && (currentNode.agent.id != 16) && ((desktop == null) || (desktop.contype != 2))); // Clipboard not supported on MacOS
5800 QE('DeskClip', deskState == 3);
@@ -5808,6 +5805,8 @@
5805 QE('deskkeys', deskState == 3);
5806
5807 // Display this only if we have Chat & Notify permissions
5808 + QV('DeskSaveImageButton', (deskState == 3) && (Q('Desk')['toBlob'] != null));
5809 + QV('DeskRecordButton', (deskState == 3));
5810 QV('DeskChatButton', ((rights & 16384) != 0) && (browserfullscreen == false) && (inputAllowed) && (currentNode.agent) && online);
5811 QV('DeskNotifyButton', ((rights & 16384) != 0) && (browserfullscreen == false) && (currentNode.agent) && (currentNode.agent.id < 5) && (inputAllowed) && (currentNode.agent) && online);
5812
@@ -6526,6 +6525,21 @@
6525 // Toggle mouse and keyboard input
6526 function toggleKvmControl() { putstore('DeskControl', (Q('DeskControl').checked?1:0)); QS('DeskControlSpan').color = Q('DeskControl').checked?null:'red'; }
6527
6528 + // Toggle desktop session recording
6529 + function deskRecordSession() {
6530 + if (desktop == null) return;
6531 + if (desktop.m.recordedData == null) {
6532 + // Start recording
6533 + console.log('Start record');
6534 + desktop.m.StartRecording();
6535 + } else {
6536 + // Stop recording
6537 + console.log('Stop record');
6538 + var rec = desktop.m.StopRecording();
6539 + console.log('frames', rec.length);
6540 + }
6541 + }
6542 +
6543 // Save the desktop image to file
6544 function deskSaveImage() {
6545 if (xxdialogMode || desktop == null || desktop.State != 3) return;
@@ -7672,7 +7686,7 @@
7686 // Storage
7687 if (hardware.identifiers && ident.storage_devices) {
7688 var x = '';
7675 - // Sort Memory
7689 + // Sort Storage
7690 ident.storage_devices.sort(function(a, b) { if (a.Caption > b.Caption) return 1; if (a.Caption < b.Caption) return -1; return 0; });
7691
7692 x += '<table style=width:100%>';