Source: backend/data_center/modules/server_socket.js

/**
 *      StreamRoller Copyright 2023 "SilenusTA https://www.twitch.tv/olddepressedgamer"
 * 
 *      StreamRoller is an all in one streaming solution designed to give a single
 *      'second monitor' control page and allow easy integration for configuring
 *      content (ie. tweets linked to chat, overlays triggered by messages, hue lights
 *      controlled by donations etc)
 * 
 *      This program is free software: you can redistribute it and/or modify
 *      it under the terms of the GNU Affero General Public License as published
 *      by the Free Software Foundation, either version 3 of the License, or
 *      (at your option) any later version.
 * 
 *      This program is distributed in the hope that it will be useful,
 *      but WITHOUT ANY WARRANTY; without even the implied warranty of
 *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *      GNU Affero General Public License for more details.
 * 
 *      You should have received a copy of the GNU Affero General Public License
 *      along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
/** ######################### SERVER_SOCKET.js #################################
// @file This file handles the server socket. This will consume client
// sockets and handle messages received and sent out
// -------------------------- Creation ----------------------------------------
// @author Silenus aka twitch.tv/OldDepressedGamer
// GitHub: https://github.com/SilenusTA/StreamRoller
// Date: 14-Jan-2021
// --------------------------- functionality ----------------------------------
// On connection the client will receive a client id. After initial connection
// the client should join(register)/create a room it wishes to use. Multiple 
// rooms can be created/joined(registered).
// You can then send/monitor the channel for messages as required
// === Rooms ===
// The API expects clients to join a room by registering.
// ie. if an extension is provided for twitch chat it might create a
// room called 'TWITCH_CHAT' and the any client can register
// for messages to that room. Each module will have to specify
// the rooms they create so that users know what to register for
// ............................. usage ........................................
// ------------------------ Import socket.io
//    import DataCenterIo from "socket.io-client";
// ------------------------ setup handlers to receive messages from the system normally three are used
//    DataCenterSocket.on("connect", (data) => onDataCenterConnect(DataCenterSocket));
//    DataCenterSocket.on("disconnect", (reason) => onDataCenterDisconnect(reason));
//    DataCenterSocket.on("message", (data) => onDataCenterMessage(data));
// ------------------------ Connect to the websocket handle
//    DataCenterSocket = DataCenterIo("http://localhost:3000", { transports: ["websocket"] });
// ------------------------ send a message to a room in the data center
//    DataCenterSocket.emit(messagetype, 
//                          {CLIENT_ID, 
//                           SOCKET_NAME, 
//                           CHANNEL_NAME, 
//                           MESSAGE_TYPE,
//                           data
//                          });
// --------------------------- description -------------------------------------
// 1) After the connection a "connect" message should be received. In the handler
//    for this "onDataCenterDisconnect(reason)" above you should either create or
//    register to a room/channel to send and receive messages from.
// 2) All data packets are stringified JSON objects as shown below
// 3) CLIENT_ID is what you get in the first message to the client when connecting
//    and gets received by the onDataCenterConnect(DataCenterSocket)) function
// ....... A NOTE ON MESSAGE TYPES ......
// There are three places where message types come up.
// 1) initial connection, this will be the websoctet type, normally set to
//            { transports: ["websocket"] }
// 2) In emit/receive funcions
//             messagetype ( lower case in emit meessage above in usage section)
//             First parameter in a received message (the .on handlers above)                
//    this is one of the following
//       createchannel, register, message, disconnect,unregister
//    this tells us what type of websocket service you are requesting/recieving. 
//    Once up and running you will only use the 'message' service with a data 
//    message as shown below
// 5) In the data section of a message. This is the one we will mostly use during
//    normal operation (ie not connecting or disconnecting the socket)
//          MESSAGE_TYPE is one of "data" or "info" or "error"
// ....................... data message format .................................
// ============================================================================
*/

// ============================================================================
//                           IMPORTS/VARIABLES
// ============================================================================
// Description: Import/Variable section
// ----------------------------- notes ----------------------------------------
// none
// ============================================================================
import * as childprocess from "child_process";
import process from 'node:process';
import { Server } from "socket.io";
import * as cm from "./common.js";
import * as logger from "./logger.js";
import * as mh from "./message_handlers.js";
import * as v8 from 'node:v8';
import { Buffer } from 'buffer';
import * as fs from "fs";
import { dirname } from "path";
import { fileURLToPath } from "url";
import sr_api from "../public/streamroller-message-api.cjs";

const __dirname = dirname(fileURLToPath(import.meta.url));
const localConfig =
{
    extensionname: "",// set from server
    channel: "datacenter_channel",
    channels: [],
    extensions: {},// all extensions
    connected_extensionlist: [],// extensions with a socket connection
    backend_server: null,
    server_socket: null,
    serverConfig: {},
    monitorHeapStats: false,
    //monitor data over websocket
    monitorSocketReceivedData: false,
    socketReceivedSize: 0,
    monitorDataHandle: 0,
    // these maintain a list of extensions that have requested an extension list
    // it only handles 'connected' extensions and will ignore extensions without 
    // a valid socket connection
    extensionlist_requesters: [],
    extensionlist_requesters_handles: [],
    all_extensionlist_requesters: [],
    all_extensionlist_requesters_handles: [],

    // callback to main server to save the updated server config
    saveServerConfigFunc: null,
}
const triggersandactions =
{
    extensionname: localConfig.extensionname,
    description: "StreamRoller server settings",
    version: "0.3",
    channel: localConfig.channel,
    // these are messages we can sendout that other extensions might want to use to trigger an action
    triggers:
        [
            {
                name: "StreamRoller IP Address Changed",
                displaytitle: "URL/IP of StreamRoller changed",
                description: "The URL or IP of main StreamRoller server has changed",
                messagetype: "trigger_StreamRollerIPChanged",
                parameters:
                {
                    triggerActionRef: "",
                    triggerActionRef_UIDescription: "Identifying references for this action",
                    address: "",
                    address_UIDescription: "IP or url name for the socket. ie localhost",
                    port: "",
                    port_UIDescription: "port number for the socket. ie 3000",
                }
            },
            {
                name: "Extension startup setting changed",
                displaytitle: "Extension startup changed",
                description: "The given extension name startup flag was changed",
                messagetype: "trigger_ExtensionStartupChanged",
                parameters:
                {
                    triggerActionRef: "",
                    triggerActionRef_UIDescription: "Identifying references for this action",
                    extension: "",
                    extension_UIDescription: "extensions name to set",
                    startup: "",
                    startup_UIDescription: "true or false",
                }
            },

        ],
    // these are messages we can receive to perform an action
    actions:
        [
            {
                name: "Change StreamRoller IP Address",
                displaytitle: "Change URL/IP of StreamRoller",
                description: "Change he URL or IP of main StreamRoller server has",
                messagetype: "action_StreamRollerChangeIP",
                parameters:
                {
                    triggerActionRef: "",
                    triggerActionRef_UIDescription: "Identifying references for this action",
                    address: "",
                    address_UIDescription: "IP or url name for the socket. ie localhost",
                    port: "",
                    port_UIDescription: "port number for the socket. ie 3000",
                }
            },
            {
                name: "Change extension startup",
                displaytitle: "Change startup extension",
                description: "Change the startup state for an extension",
                messagetype: "action_StreamRollerChangeStartupExtension",
                parameters:
                {
                    triggerActionRef: "",
                    triggerActionRef_UIDescription: "Identifying references for this action",
                    extension: "",
                    extension_UIDescription: "extensions name to set",
                    startup: "",
                    startup_UIDescription: "true or false",
                }
            },
            {
                name: "Reboot Server",
                displaytitle: "Reboot Server",
                description: "Reboot the StreamRoller Server",
                messagetype: "action_RebootServer",
                parameters:
                {
                }
            },
            {
                name: "Stop Server",
                displaytitle: "Stop Server",
                description: "Stop the StreamRoller Server",
                messagetype: "action_StopServer",
                parameters:
                {
                }
            }
        ],
}
// ============================================================================
//                           FUNCTION: start
// ============================================================================
/**
 * Starts the server
 * @param {Express} app 
 * @param {Object} server
 * @param {Array} exts 
 */
function start (app, server, exts, serverConfig, saveServerConfigFunc)
{
    // save the server config in our temp config
    localConfig.serverConfig = serverConfig;
    //update our extension name to match the server
    localConfig.extensionname = serverConfig.extensionname;
    triggersandactions.extensionname = serverConfig.extensionname
    // save the callback function to save the servers data file
    localConfig.saveServerConfigFunc = saveServerConfigFunc;
    //add ourselves to the list of extensions that the server will load
    // done here so we don't use this in the extensions loading section in the loader
    localConfig.serverConfig.enabledExtensions[localConfig.extensionname] = true;
    //add ourselves to the list of extensions we have connected so we can send
    // those out to the extensions that ask for the list
    localConfig.connected_extensionlist[localConfig.extensionname] = true;
    mh.setExtensionName(serverConfig.extensionname);
    // create our extension array
    exts.forEach((elem, i) =>
    {
        localConfig.extensions[elem] = {};
    });
    localConfig.backend_server = server;
    cm.initcrypto();
    //localConfig.serverConfig = cm.loadConfig(localConfig.extensionname);
    //setup our server socket on the http server
    try
    {
        // note. can't use the api functions here as we are creating a server socket
        localConfig.server_socket = new Server(server, {
            transports: ["websocket"],
            maxHttpBufferSize: 1e8,// 100MB had to increase for flight sim
        });
        logger.log("[" + localConfig.serverConfig.SYSTEM_LOGGING_TAG + "]server_socket.start", "Server is running and waiting for clients");
        if (localConfig.monitorSocketReceivedData)
            dataMonitorScheduler();
        // wait for messages
        localConfig.server_socket.on("connection", (socket) =>
        {
            // call onConnect to store the connection details
            if (localConfig.monitorSocketReceivedData)
            {
                onConnect(socket);
                socket.on("disconnect", (reason) => onDisconnect(socket, reason));
                socket.on("message", (data) => socketReceiveDebug(socket, data));
            }
            else
            {
                onConnect(socket);
                socket.on("disconnect", (reason) => onDisconnect(socket, reason));
                socket.on("message", (data) => onMessage(socket, data));
            }
        });
    } catch (err)
    {
        logger.err("[" + localConfig.serverConfig.SYSTEM_LOGGING_TAG + "]server_socket.start", "server startup failed:", err);
    }
}
// ============================================================================
//                           FUNCTION: onConnect
// ============================================================================
/**
 * receives connect messages
 * @param {Socket} socket socket received on
 */
function onConnect (socket)
{
    socket.emit("connected", socket.id);
    socket.join(localConfig.channel);
    sendAddressTrigger();
}
// ============================================================================
//                           FUNCTION: onDisconnect
// ============================================================================
/**
 * receive disconnect messages
 * @param {Socket} socket 
 * @param {string} reason 
 */
function onDisconnect (socket, reason)
{
    // set the extension as disconnected
    let ext = Object.keys(localConfig.extensions).find((entry) => (localConfig.extensions[entry].socket != undefined && localConfig.extensions[entry].socket.id == socket.id))
    localConfig.connected_extensionlist[ext] = socket.connected
    // the disconnected extension previously was on our requesters list so we need to remove it
    if (localConfig.extensionlist_requesters.includes(ext))
    {
        clearTimeout(localConfig.extensionlist_requesters_handles[ext])
        localConfig.extensionlist_requesters.splice(localConfig.extensionlist_requesters.indexOf(ext), 1)
        // update anyone who has previously reqested an extension list if we have changed it
        updateExtensionsListRequesters()
    }
    logger.info("[" + localConfig.serverConfig.SYSTEM_LOGGING_TAG + "]server_socket.onDisconnect", reason, socket);
}
// ============================================================================
//                           FUNCTION: onMessage
// ============================================================================
/**
 * receives messages
 * @param {Socket} socket 
 * @param {Object} server_packet 
 */
function onMessage (socket, server_packet)
{
    // make sure we are using the same api version
    if (server_packet.version != localConfig.serverConfig.apiVersion)
    {
        logger.err("[" + localConfig.serverConfig.SYSTEM_LOGGING_TAG + "]server_socket.onMessage",
            "Version mismatch:", server_packet.version, "!=", localConfig.serverConfig.apiVersion);

        logger.info("[" + localConfig.serverConfig.SYSTEM_LOGGING_TAG + "]server_socket.onMessage",
            "!!!!!!! Message System API version doesn't match: ", server_packet);
        mh.errorMessage(socket, "Incorrect api version", server_packet);
        return;
    }

    // check that the sender has sent a name and id
    if (!server_packet.type || !server_packet.from || !server_packet.type === "" || !server_packet.from === "")
    {
        console.log(JSON.stringify(server_packet, null, 2))
        logger.err("[" + localConfig.serverConfig.SYSTEM_LOGGING_TAG + "]server_socket.onMessage",
            "!!!!!!! Invalid data: ", server_packet);
        mh.errorMessage(socket, "Missing type/from field", server_packet);
        return;
    }
    // add this socket to the extension if it doesn't already exist
    if (typeof (localConfig.extensions[server_packet.from]) === "undefined" || !localConfig.extensions[server_packet.from])
        localConfig.extensions[server_packet.from] = {};
    // check we have a valid socket for this extension, don't need to check if the extension exists as it will have been added above
    if (typeof (localConfig.extensions[server_packet.from].socket) === "undefined" || !localConfig.extensions[server_packet.from].socket)
    {
        logger.log("[" + localConfig.serverConfig.SYSTEM_LOGGING_TAG + "]server_socket.onMessage", "registering new socket for ", server_packet.from);
        localConfig.extensions[server_packet.from].socket = socket;
        localConfig.connected_extensionlist[server_packet.from] = localConfig.extensions[server_packet.from].socket.connected
        // if we add a new extension send the list out to anyone who has requested it so far to update them
        updateExtensionsListRequesters()
    }
    else
    {
        // note that we currently only have one slot per connection. this works for extensions that are only loaded once
        // but for webpage stuff we will need to allow more than one sockect for that extension name
        // we need to append the socket id to the extension name to fix this!!!
        if (localConfig.extensions[server_packet.from].socket.id != socket.id)
        {
            logger.warn("[" + localConfig.serverConfig.SYSTEM_LOGGING_TAG + "]server_socket.onMessage", "Extension socket id changed for " + server_packet.from);
            logger.warn("[" + localConfig.serverConfig.SYSTEM_LOGGING_TAG + "]server_socket.onMessage", "Data ", server_packet);
            logger.warn("[" + localConfig.serverConfig.SYSTEM_LOGGING_TAG + "]server_socket.onMessage", "Previous id " + localConfig.extensions[server_packet.from].socket.id);
            logger.warn("[" + localConfig.serverConfig.SYSTEM_LOGGING_TAG + "]server_socket.onMessage", "New id " + socket.id);
            //update the extensions socket as it has changed
            localConfig.extensions[server_packet.from].socket = socket;

        }
        localConfig.connected_extensionlist[server_packet.from] = localConfig.extensions[server_packet.from].socket.connected
    }
    // process the clients request
    if (server_packet.type === "RequestSoftwareVersion")
        mh.sendSoftwareVersion(socket, server_packet.from);
    else if (server_packet.type === "RequestConfig")
        mh.sendConfig(socket, server_packet.from);
    else if (server_packet.type === "SaveConfig")
        mh.saveConfig(server_packet.from, server_packet.data);
    else if (server_packet.type === "RequestData")
        mh.sendData(socket, server_packet.from);
    else if (server_packet.type === "SaveData")
        mh.saveData(server_packet.from, server_packet.data);
    else if (server_packet.type === "StopServer")
        process.exit(0);
    else if (server_packet.type == "RestartServer")
        RestartServer();
    else if (server_packet.type === "UpdateCredentials")
    {
        mh.UpdateCredentials(server_packet.data);
        //resend new credentials to extension
        mh.RetrieveCredentials(server_packet.data.ExtensionName, localConfig.extensions);
    }
    else if (server_packet.type === "RequestCredentials")
        mh.RetrieveCredentials(server_packet.from, localConfig.extensions);
    else if (server_packet.type === "DeleteCredentials")
        mh.DeleteCredentials(socket, server_packet.from);
    else if (server_packet.type === "RequestExtensionsList")
    {
        if (!localConfig.extensionlist_requesters.includes(server_packet.from))
            localConfig.extensionlist_requesters.push(server_packet.from)
        localConfig.connected_extensionlist.sort()
        mh.sendExtensionList(socket, server_packet.from, localConfig.connected_extensionlist);
    }
    else if (server_packet.type === "RequestAllExtensionsList")
    {
        if (!localConfig.all_extensionlist_requesters.includes(server_packet.from))
            localConfig.all_extensionlist_requesters.push(server_packet.from)
        mh.sendExtensionList(socket, server_packet.from, localConfig.extensions);
    }
    else if (server_packet.type === "RequestChannelsList")
        mh.sendChannelList(socket, server_packet.from, localConfig.channels);
    else if (server_packet.type === "CreateChannel")
        mh.createChannel(localConfig.server_socket, socket, server_packet.data, localConfig.channels, server_packet.from);
    else if (server_packet.type === "JoinChannel")
        mh.joinChannel(localConfig.server_socket, socket, server_packet.data, localConfig.channels, server_packet.from);
    else if (server_packet.type === "LeaveChannel")
        mh.leaveChannel(socket, server_packet.from, server_packet.data);
    else if (server_packet.type === "ExtensionMessage")
    {
        let extension_packet = server_packet.data;
        if (server_packet.to === undefined)
            mh.errorMessage(socket, "No extension name specified for ExtensionMessage", server_packet);
        else if (server_packet.to === localConfig.extensionname)
        {
            // *******************************************
            // we are the target of this extension message
            // *******************************************
            if (server_packet.data.type === "RequestSettingsWidgetSmallCode")
                SendSettingsWidgetSmall(server_packet.from);
            else if (server_packet.data.type === "SettingsWidgetSmallData")
                processSettingsWidgetSmallData(server_packet.data.data);
            else if (extension_packet.type === "SendTriggerAndActions")
            {
                sr_api.sendMessage(socket,
                    sr_api.ServerPacket("ExtensionMessage",
                        localConfig.extensionname,
                        sr_api.ExtensionPacket(
                            "TriggerAndActions",
                            localConfig.extensionname,
                            triggersandactions,
                            "",
                            server_packet.from
                        ),
                        "",
                        server_packet.from
                    )
                )
            }
            else if (extension_packet.type === "action_StreamRollerChangeIP")
            {
                localConfig.serverConfig.url = extension_packet.data.address;
                sendAddressTrigger();
                localConfig.saveServerConfigFunc();
                RestartServer();
            }
            else if (extension_packet.type === "action_StreamRollerChangeStartupExtension")
            {
                try
                {
                    if (extension_packet.data.startup == "true")
                        localConfig.serverConfig.enabledExtensions[extension_packet.data.extension] = true
                    else
                        localConfig.serverConfig.enabledExtensions[extension_packet.data.extension] = false
                    localConfig.saveServerConfigFunc();
                    sendExtensionEnabledTrigger(extension_packet.data.triggerActionRef, extension_packet.data.extension);
                    SendSettingsWidgetSmall();
                }
                catch (err)
                {
                    console.log("action_StreamRollerChangeStartupExtension:Error", err)
                }
            }
            else if (extension_packet.type === "action_StopServer")
            {
                process.exit()
            }
            else if (extension_packet.type === "action_RebootServer")
            {
                RestartServer();
            }
        }
        else
            mh.forwardMessage(socket, server_packet, localConfig.channels, localConfig.extensions);
    }
    else if (server_packet.type === "ChannelData")
    {
        if (server_packet.dest_channel === undefined)
            mh.errorMessage(socket, "No dest_channel specified for ChannelData", server_packet);
        else
            mh.forwardMessage(socket, server_packet, localConfig.channels, localConfig.extensions);
    }
    else if (server_packet.type === "SetLoggingLevel")
    {
        logger.log("[" + localConfig.serverConfig.SYSTEM_LOGGING_TAG + "]server_socket.onMessage", "logging set to level ", server_packet.data);
        mh.setLoggingLevel(localConfig.server_socket, server_packet.data);
        localConfig.serverConfig.logginglevel = server_packet.data;
        cm.saveConfig(localConfig.serverConfig.extensionname, localConfig.serverConfig);
    }
    else if (server_packet.type === "RequestLoggingLevel")
        mh.sendLoggingLevel(socket);
    else if (server_packet.type === "MonitorHeap")
    {
        // if we are turning it on then start the monitoring process
        if (server_packet.data == 1 && !localConfig.monitorHeapStats)
        {
            localConfig.monitorHeapStats = true;
            heapStatsScheduler()
        }
        else
            localConfig.monitorHeapStats = false;
    }
    else
    {
        logger.err("[" + localConfig.serverConfig.SYSTEM_LOGGING_TAG + "]server_socket.onMessage", "Unhandled message");
        console.log(JSON.stringify(server_packet, null, 2))
    }
}
// ============================================================================
//                      updateExtensionsListRequesters()
// ===========================================================================
function updateExtensionsListRequesters ()
{
    // update users who requested extensions with sockets
    for (let i = 0; i < localConfig.extensionlist_requesters.length; i++)
    {
        // buffer the extensionlist messages using a timer
        clearTimeout(localConfig.extensionlist_requesters_handles[localConfig.extensionlist_requesters[i]])
        localConfig.extensionlist_requesters_handles[localConfig.extensionlist_requesters[i]] = setTimeout(() =>
        {
            // send the connected extension list
            mh.sendExtensionList(localConfig.extensions[localConfig.extensionlist_requesters[i]].socket, localConfig.extensionlist_requesters[i], localConfig.connected_extensionlist);
        }, 2000);
    }
    // update users who have requested the full list (even extensions without sockets)
    for (let i = 0; i < localConfig.all_extensionlist_requesters.length; i++)
    {
        // buffer the extensionlist messages using a timer
        clearTimeout(localConfig.all_extensionlist_requesters_handles[localConfig.all_extensionlist_requesters[i]])
        localConfig.all_extensionlist_requesters_handles[localConfig.all_extensionlist_requesters[i]] = setTimeout(() =>
        {
            // send the connected extension list
            mh.sendExtensionList(localConfig.extensions[localConfig.all_extensionlist_requesters[i]].socket, localConfig.all_extensionlist_requesters[i], localConfig.extensions);
        }, 2000);
    }
}
// ===========================================================================
//                           FUNCTION: SendSettingsWidgetSmall
// ===========================================================================
/**
 * @param {String} to 
 */
function SendSettingsWidgetSmall (to = "")
{
    // read our modal file
    fs.readFile(__dirname + "/partials/datacentersettingswidgetsmall.html", function (err, filedata)
    {
        if (err)
            logger.err(localConfig.SYSTEM_LOGGING_TAG + localConfig.extensionname +
                ".SendSettingsWidgetSmall", "failed to load modal", err);
        //throw err;
        else
        {
            let modalstring = filedata.toString();
            for (const [key, value] of Object.entries(localConfig))
            {
                if (typeof (value) == "string")
                    modalstring = modalstring.replace(key + "text", value);
            }

            let checkboxes = "<div class='form-check form-check-inline py-2'>"
            for (var key in localConfig.serverConfig.enabledExtensions)
            {
                checkboxes += createCheckBox(key, key, localConfig.serverConfig.enabledExtensions[key] ? "checked" : "")
                //checkboxes += "<BR>"
            }
            checkboxes += "</div>"
            modalstring = modalstring.replace("datacenterExtensionListCheckboxes", checkboxes)

            sr_api.sendMessage(localConfig.server_socket,
                sr_api.ServerPacket(
                    "ExtensionMessage", // this type of message is just forwarded on to the extension
                    localConfig.extensionname,
                    sr_api.ExtensionPacket(
                        "SettingsWidgetSmallCode", // message type
                        localConfig.extensionname, //our name
                        modalstring,// data
                        "",
                        to,
                        "",
                    ),
                    "",
                    to // in this case we only need the "to" channel as we will send only to the requester
                )
            )
        }
    });
}
// ===========================================================================
//                           FUNCTION: processSettingsWidgetSmallData
// ===========================================================================
/**
 * @param {object} data 
 */
function processSettingsWidgetSmallData (data)
{
    let restart = false;
    // set the extension list load settings
    for (var key in localConfig.serverConfig.enabledExtensions)
    {
        if (data[key])
        {
            if (!localConfig.serverConfig.enabledExtensions[key])
                restart = true;
            localConfig.serverConfig.enabledExtensions[key] = true;
        }
        else
        {
            if (localConfig.serverConfig.enabledExtensions[key])
                restart = true;
            localConfig.serverConfig.enabledExtensions[key] = false;
        }
    }

    // check if we are changing the an extensions state and restart if needed
    if (restart)
        RestartServer()
    // broadcast our modal out so anyone showing it can update it
    SendSettingsWidgetSmall("");
    localConfig.saveServerConfigFunc(localConfig.serverConfig);
}
// ===========================================================================
//                           FUNCTION: createCheckBox
// ===========================================================================
/**
 * 
 * @param {string} description 
 * @param {string} name 
 * @param {boolean} checked 
 * @returns html code of the checkbox
 */
function createCheckBox (description, name, checked)
{
    let checkedtext = "";
    if (checked)
        checkedtext = "checked";
    return `<input class="form-check-input" name="${name}" type="checkbox" id="${name}" ${checkedtext}>
        <label class="form-check-label" for="${name}">&nbsp;${description}</label><BR>`
}
// ===========================================================================
//                      RestartServer()
// ===========================================================================
function RestartServer ()
{
    console.log("Restarting...");

    const child = childprocess.exec('cmd /k start /MIN "Streamroller" node backend\\data_center\\server.js 5',
        //const child = childprocess.exec('start /MIN "Streamroller" node_modules\\.bin\\node backend\\data_center\\server.js 5',
        {
            detached: true,
            stdio: 'ignore'
        })
    child.unref();
    setTimeout(() =>
    {
        console.log("exiting")
        localConfig.backend_server.close();
        process.exit();
    }, 1000);
}
// ===========================================================================
//                      heapStatsScheduler()
// ===========================================================================


// DEBUG monitoring heap status
function heapStatsScheduler ()
{
    setTimeout(() =>
    {
        dumpHeapStats()
    }
        , 1000);
}
function dumpHeapStats ()
{
    let maxHeap = v8.getHeapStatistics().heap_size_limit;
    let heap = v8.getHeapStatistics();
    let used_heap_size = heap.used_heap_size
    console.log("server Heap usage \t" + ((used_heap_size / maxHeap) * 100).toFixed(2) + "%", heap.number_of_native_contexts, heap.number_of_detached_contexts)
    if (localConfig.monitorHeapStats)
        heapStatsScheduler()
}
//end Debugheap

// ===========================================================================
//                      DataMonitor
// ===========================================================================
function socketReceiveDebug (socket, server_packet)
{
    localConfig.socketReceivedSize = localConfig.socketReceivedSize + Buffer.byteLength(JSON.stringify(server_packet))
    onMessage(socket, server_packet)
}
// ============================================================================
//                           FUNCTION: dataMonitorScheduler
// ============================================================================
function dataMonitorScheduler ()
{
    mh.sendDataLoad(localConfig.server_socket, localConfig.socketReceivedSize);
    localConfig.socketReceivedSize = 0;
    if (localConfig.monitorSocketReceivedData)
    {

        clearTimeout(localConfig.monitorDataHandle);
        localConfig.monitorDataHandle = setTimeout(() =>
        {
            dataMonitorScheduler();
        }, "1000");
    }
}
// ============================================================================
//                           FUNCTION: sendAddressTrigger
// ============================================================================
/**
 * Finds the trigger using the passed messagetype
 * @returns trigger
 */
function sendAddressTrigger ()
{
    let message = findTriggerByMessageType("trigger_StreamRollerIPChanged")
    if (localConfig.serverConfig.HOST != "" && localConfig.serverConfig.PORT != "")
    {
        message.address = localConfig.serverConfig.HOST
        message.address = localConfig.serverConfig.PORT
        sendTrigger(message)
    }
    else
        logger.err(localConfig.serverConfig.SYSTEM_LOGGING_TAG + localConfig.serverConfig.extensionname + ".onDataCenterMessage", localConfig.serverConfig.extensionname + " sendAddressTrigger", "address or port empty", "'" + localConfig.serverConfig.HOST + "'", "'" + localConfig.serverConfig.PORT + "'");
}
// ============================================================================
//                           FUNCTION: sendExtensionEnabledTrigger
// ============================================================================
/**
 * Sends out a extensions startup flag for the given extensions
 * @param {string} ref reference from action 
 * @param {string} ext extension name
 */
function sendExtensionEnabledTrigger (ref, ext)
{
    let message = findTriggerByMessageType("trigger_ExtensionStartupChanged")
    message.parameters.triggerActionRef = ref;
    message.parameters.extension = ext;
    message.parameters.startup = localConfig.serverConfig.enabledExtensions[ext].toString();
    sendTrigger(message)
}
// ============================================================================
//                           FUNCTION: findTriggerByMessageType
// ============================================================================
/**
 * Finds the trigger using the passed messagetype
 * @param {string} messagetype 
 * @returns trigger
 */
function findTriggerByMessageType (messagetype)
{
    for (let i = 0; i < triggersandactions.triggers.length; i++)
    {
        if (triggersandactions.triggers[i].messagetype.toLowerCase() == messagetype.toLowerCase())
            return triggersandactions.triggers[i];
    }
    logger.err(localConfig.SYSTEM_LOGGING_TAG + localConfig.extensionname +
        ".findTriggerByMessageType", "failed to find trigger", messagetype);
}
// ===========================================================================
//                           FUNCTION: sendTrigger
// ===========================================================================
/**
 * Sends the given trigger or action out on our channel if to is ""
 * or sends to the extension as an extension message if to specifies extension name
 * @param {object} data 
 */
function sendTrigger (data)
{
    sr_api.sendMessage(localConfig.server_socket,
        sr_api.ServerPacket(
            'ChannelData',
            localConfig.extensionname,
            sr_api.ExtensionPacket(
                data.messagetype,
                localConfig.extensionname,
                data,
                localConfig.channel,
                ''),
            localConfig.channel,
            ''
        )
    );
}
// ============================================================================
//                           EXPORTS:
// ============================================================================
export { start, triggersandactions };