From 0539fa6621948e0c8188927a32931759b3dccceb Mon Sep 17 00:00:00 2001 From: PhatPhuckDave <> Date: Mon, 22 Jul 2024 19:50:03 +0200 Subject: [PATCH] Update --- .gitignore | 2 + .vscode/settings.json | 10 - README.md | 224 +++++++++--------- center.js | 472 +++++++++++++++++++------------------- cliOptions.js | 332 +++++++++++++-------------- client.js | 328 +++++++++++++------------- logger.js | 150 ++++++------ metrics/circularBuffer.js | 92 ++++---- metrics/metricManager.js | 60 ++--- metrics/metrics.js | 78 +++---- tests/utils.test.js | 188 +++++++-------- utils.js | 208 ++++++++--------- 12 files changed, 1068 insertions(+), 1076 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index 9209ef5..f54d68c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ node_modules out +.idea +.vscode diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index db7e31f..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "sqltools.connections": [ - { - "previewLimit": 50, - "driver": "SQLite", - "name": "1", - "database": "E:\\tmp\\output.db" - } - ] -} diff --git a/README.md b/README.md index 22c2ef4..1b8b288 100644 --- a/README.md +++ b/README.md @@ -1,112 +1,112 @@ -# SMPP_CLI - -### A small application used to create SMPP clients or centers. Serves as both the SMSC and ESME. - -(Taken from the help page) - ---- - -``` -CLI SMPP (Client) - -Options - - --help Display this usage guide. - -h, --host string The host (IP) to connect to. - -p, --port number The port to connect to. - -s, --systemid string SMPP related login info. - -w, --password string SMPP related login info. - --sessions number Number of sessions to start, defaults to 1. - --messagecount number Number of messages to send; Optional, defaults to 1. - --window number Defines the amount of messages that are allowed to be - 'in flight'. The client no longer waits for a - response before sending the next message for up to - messages. Defaults to 100. - --windowsleep number Defines the amount time (in ms) waited between - retrying in the case of full window. Defaults to 100. - --mps number Number of messages to send per second - --source string Source field of the sent messages. - --destination string Destination field of the sent messages. - --message string Text content of the sent messages. - --debug Display all traffic to and from the client; Debug - mode. -``` ---- -``` -CLI SMPP (Center) - -Options - - --help Display this usage guide. - -p, --port number The port to connect to. - -s, --systemid string SMPP related login info. - -w, --password string SMPP related login info. - --dr Whether or not to send Delivery Reports. - --sessions number Maximum number of client sessions to accept, defaults - to 8. - --messagecount number Number of messages to send; Optional, defaults to 0. - --window number Defines the amount of messages that are allowed to be - 'in flight'. The client no longer waits for a - response before sending the next message for up to - messages. Defaults to 100. - --windowsleep number Defines the amount time (in ms) waited between - retrying in the case of full window. Defaults to 100. - --mps number Number of messages to send per second - --source string Source field of the sent messages. - --destination string Destination field of the sent messages. - --message string Text content of the sent messages. - --debug Display all traffic to and from the center; Debug - mode. -``` ---- -#### Center example usage: -``` -./center-win.exe \ ---port 7001 \ ---systemid test \ ---password test \ ---sessions 10 -``` -Running this command will spawn an SMPP center (SMSC) which will: -- Start listening on 7001 -- Accept clients with test:test credentials -- Allow up to a maximum of 10 sessions -#### Client example usage: -``` -./client-win.exe \ ---host localhost \ ---port 7001 \ ---systemid test \ ---password test \ ---window 5 \ ---windowsleep 100 \ ---messagecount 10000 \ ---mps 10 \ ---sessions 10 -``` -Running this command will spawn an SMPP client (ESME) which will: -- Try to connect 10 sessions to localhost:7001 -- Once connected try to bind using test:test -- Once bound send 10000 messages at a rate of 10 per second and a window size of 5 -- If the window is filled during sending the session will wait 100ms before attempting send again ---- ---- -#### Center example usage (sending): -``` -./center-win.exe \ ---port 7001 \ ---systemid test \ ---password test \ ---window 5 \ ---windowsleep 100 \ ---messagecount 10000 \ ---mps 10 \ ---sessions 10 -``` -Running this command will spawn an SMPP center (SMSC) which will: -- Start listening on 7001 -- Accept clients with test:test credentials -- Allow up to a maximum of 10 sessions -- Once a client is connected start sending 10000 messages at a rate of 10 per second with a window size of 5 -- If the window is filled during sending the session will wait 100ms before attempting send again ---- +# SMPP_CLI + +### A small application used to create SMPP clients or centers. Serves as both the SMSC and ESME. + +(Taken from the help page) + +--- + +``` +CLI SMPP (Client) + +Options + + --help Display this usage guide. + -h, --host string The host (IP) to connect to. + -p, --port number The port to connect to. + -s, --systemid string SMPP related login info. + -w, --password string SMPP related login info. + --sessions number Number of sessions to start, defaults to 1. + --messagecount number Number of messages to send; Optional, defaults to 1. + --window number Defines the amount of messages that are allowed to be + 'in flight'. The client no longer waits for a + response before sending the next message for up to + messages. Defaults to 100. + --windowsleep number Defines the amount time (in ms) waited between + retrying in the case of full window. Defaults to 100. + --mps number Number of messages to send per second + --source string Source field of the sent messages. + --destination string Destination field of the sent messages. + --message string Text content of the sent messages. + --debug Display all traffic to and from the client; Debug + mode. +``` +--- +``` +CLI SMPP (Center) + +Options + + --help Display this usage guide. + -p, --port number The port to connect to. + -s, --systemid string SMPP related login info. + -w, --password string SMPP related login info. + --dr Whether or not to send Delivery Reports. + --sessions number Maximum number of client sessions to accept, defaults + to 8. + --messagecount number Number of messages to send; Optional, defaults to 0. + --window number Defines the amount of messages that are allowed to be + 'in flight'. The client no longer waits for a + response before sending the next message for up to + messages. Defaults to 100. + --windowsleep number Defines the amount time (in ms) waited between + retrying in the case of full window. Defaults to 100. + --mps number Number of messages to send per second + --source string Source field of the sent messages. + --destination string Destination field of the sent messages. + --message string Text content of the sent messages. + --debug Display all traffic to and from the center; Debug + mode. +``` +--- +#### Center example usage: +``` +./center-win.exe \ +--port 7001 \ +--systemid test \ +--password test \ +--sessions 10 +``` +Running this command will spawn an SMPP center (SMSC) which will: +- Start listening on 7001 +- Accept clients with test:test credentials +- Allow up to a maximum of 10 sessions +#### Client example usage: +``` +./client-win.exe \ +--host localhost \ +--port 7001 \ +--systemid test \ +--password test \ +--window 5 \ +--windowsleep 100 \ +--messagecount 10000 \ +--mps 10 \ +--sessions 10 +``` +Running this command will spawn an SMPP client (ESME) which will: +- Try to connect 10 sessions to localhost:7001 +- Once connected try to bind using test:test +- Once bound send 10000 messages at a rate of 10 per second and a window size of 5 +- If the window is filled during sending the session will wait 100ms before attempting send again +--- +--- +#### Center example usage (sending): +``` +./center-win.exe \ +--port 7001 \ +--systemid test \ +--password test \ +--window 5 \ +--windowsleep 100 \ +--messagecount 10000 \ +--mps 10 \ +--sessions 10 +``` +Running this command will spawn an SMPP center (SMSC) which will: +- Start listening on 7001 +- Accept clients with test:test credentials +- Allow up to a maximum of 10 sessions +- Once a client is connected start sending 10000 messages at a rate of 10 per second with a window size of 5 +- If the window is filled during sending the session will wait 100ms before attempting send again +--- diff --git a/center.js b/center.js index 42eb976..c1060dd 100644 --- a/center.js +++ b/center.js @@ -1,236 +1,236 @@ -const smpp = require("smpp"); -const commandLineArgs = require("command-line-args"); -const commandLineUsage = require("command-line-usage"); -const NanoTimer = require("nanotimer"); -const { createBaseLogger, createSessionLogger } = require("./logger"); -const { verifyDefaults, verifyExists, sendPdu } = require("./utils"); -const { centerOptions } = require("./cliOptions"); -const crypto = require("crypto"); -const { MetricManager } = require("./metrics/metricManager"); - -const options = commandLineArgs(centerOptions); -const logger = createBaseLogger(options); - -if (options.help) { - const usage = commandLineUsage([ - { - header: "CLI SMPP (Center)", - }, - { - header: "Options", - optionList: centerOptions, - }, - { - content: "Project home: {underline https://github.com/PhatDave/SMPP_CLI}", - }, - ]); - console.log(usage); - process.exit(0); -} - -verifyDefaults(options, centerOptions); -verifyExists(options.port, "Port can not be undefined or empty! (--port)", logger); -verifyExists(options.systemid, "SystemID can not be undefined or empty! (--systemid)", logger); -verifyExists(options.password, "Password can not be undefined or empty! (--password)", logger); - -let inFlight = 0; -let sent = 0; -let success = 0; -let failed = 0; -const sendTimer = new NanoTimer(); -const metricManager = new MetricManager(options); - -// TODO: Currently bars are broken -// A major rework will need to happen before bars are able to play nice with multiple sessions -// TODO: Maybe add only receiver and only transmitter modes instead of transciever -// Instead just use the same timer but make a pool of connections; That way both problems will be solved -function startInterval(sessions, sessionLogger, rxMetrics) { - if (!options.messagecount > 0) { - sessionLogger.info("No messages to send"); - return; - } - let sessionPointer = 0; - sendTimer.setInterval( - async () => { - if (sent >= options.messagecount) { - sessionLogger.info(`Finished sending messages success:${success}, failed:${failed}, idling...`); - sendTimer.clearInterval(); - } else if (inFlight < options.window) { - sessionLogger.info(`Sending message ${sent + 1}/${options.messagecount}`); - const pdu = new smpp.PDU("deliver_sm", { - source_addr: options.source, - destination_addr: options.destination, - short_message: options.message, - }); - - if (sessionPointer >= sessions.length) { - sessionPointer = 0; - } - sendPdu(sessions[sessionPointer++], pdu, sessionLogger, options.longsms) - .then((resp) => { - inFlight--; - sessionLogger.info(`Received response with id ${resp.message_id}`); - success++; - }) - .catch((resp) => { - inFlight--; - sessionLogger.warn(`Message failed with id ${resp.message_id}`); - failed++; - }); - sent++; - inFlight++; - } else { - sessionLogger.warn( - `${inFlight}/${options.window} messages pending, waiting for a reply before sending more` - ); - sendTimer.clearInterval(); - setTimeout(() => startInterval(sessions, sessionLogger), options.windowsleep); - } - }, - "", - `${1 / options.mps} s` - ); -} - -logger.info(`Staring server on port ${options.port}...`); -let sessionid = 1; -let messageid = 0; -const sessions = []; -const server = smpp.createServer( - { - debug: options.debug, - }, - function (session) { - const id = sessionid++; - const sessionLogger = createSessionLogger(options, id); - const rxMetrics = metricManager.AddMetrics(`Session-${id}-RX`); - const txMetrics = metricManager.AddMetrics(`Session-${id}-TX`); - - session.on("bind_transceiver", function (pdu) { - if (pdu.system_id === options.systemid && pdu.password === options.password) { - sessions.push(session); - sessionLogger.info(`Client connected, currently: ${sessions.length}`); - session.send(pdu.response()); - startInterval(sessions, sessionLogger); - } else { - sessionLogger.warn( - `Client tried to connect with incorrect login ('${pdu.system_id}' '${pdu.password}')` - ); - pdu.response({ - command_status: smpp.ESME_RBINDFAIL, - }); - session.close(); - } - }); - session.on("bind_transmitter", function (pdu) { - if (pdu.system_id === options.systemid && pdu.password === options.password) { - sessionLogger.info("Client connected"); - session.send(pdu.response()); - startInterval(session, sessionLogger, rxMetrics); - } else { - sessionLogger.warn( - `Client tried to connect with incorrect login ('${pdu.system_id}' '${pdu.password}')` - ); - pdu.response({ - command_status: smpp.ESME_RBINDFAIL, - }); - session.close(); - } - }); - session.on("bind_receiver", function (pdu) { - if (pdu.system_id === options.systemid && pdu.password === options.password) { - sessionLogger.info("Client connected"); - session.send(pdu.response()); - startInterval(session, sessionLogger); - } else { - sessionLogger.warn( - `Client tried to connect with incorrect login ('${pdu.system_id}' '${pdu.password}')` - ); - pdu.response({ - command_status: smpp.ESME_RBINDFAIL, - }); - session.close(); - } - }); - session.on("enquire_link", function (pdu) { - session.send(pdu.response()); - }); - session.on("submit_sm", async function (pdu) { - if (!options.dr) { - sessionLogger.info("Replying to incoming submit_sm"); - if (options.bars) { - rxMetrics.AddEvent(); - } - // setTimeout(() => { - // session.send(pdu.response()); - // }, 200); - session.send(pdu.response()); - return; - } - - sessionLogger.info("Generating DR for incoming submit_sm"); - let response = pdu.response(); - - let smppid = messageid++; - if (options.randid) { - smppid = crypto.randomBytes(8).toString("hex"); - } - - response.message_id = smppid.toString(16); - session.send(response); - - let drMessage = ""; - let date = new Date() - .toISOString() - .replace(/T/, "") - .replace(/\..+/, "") - .replace(/-/g, "") - .replace(/:/g, "") - .substring(2, 12); - - drMessage += "id:" + response.message_id + " "; - drMessage += "sub:001 "; - drMessage += "dlvrd:001 "; - drMessage += "submit date:" + date + " "; - drMessage += "done date:" + date + " "; - drMessage += "stat:DELIVRD "; - drMessage += "err:000 "; - drMessage += "text:"; - - const DRPdu = { - source_addr: pdu.destination_addr, - destination_addr: pdu.source_addr, - short_message: drMessage, - esm_class: 4, - }; - sessionLogger.info(`Generated DR as ${drMessage}`); - session.deliver_sm(DRPdu); - if (txMetrics) { - txMetrics.AddEvent(); - } - }); - - session.on("close", function () { - sessions.splice(sessions.indexOf(session), 1); - sessionLogger.warn(`Session closed, now ${sessions.length}`); - session.close(); - if (sessions.length === 0) { - sessionLogger.info("No more sessions, stopping sending timer"); - sendTimer.clearInterval(); - } - }); - session.on("error", function (err) { - sessionLogger.error(`Fatal error ${err}`); - session.close(); - }); - } -); - -server.on("error", function (err) { - logger.error(`Fatal server error ${err}`); - server.close(); - process.exit(1); -}); - -server.listen(options.port); -logger.info(`SMPP Server listening on ${options.port}`); +const smpp = require("smpp"); +const commandLineArgs = require("command-line-args"); +const commandLineUsage = require("command-line-usage"); +const NanoTimer = require("nanotimer"); +const { createBaseLogger, createSessionLogger } = require("./logger"); +const { verifyDefaults, verifyExists, sendPdu } = require("./utils"); +const { centerOptions } = require("./cliOptions"); +const crypto = require("crypto"); +const { MetricManager } = require("./metrics/metricManager"); + +const options = commandLineArgs(centerOptions); +const logger = createBaseLogger(options); + +if (options.help) { + const usage = commandLineUsage([ + { + header: "CLI SMPP (Center)", + }, + { + header: "Options", + optionList: centerOptions, + }, + { + content: "Project home: {underline https://github.com/PhatDave/SMPP_CLI}", + }, + ]); + console.log(usage); + process.exit(0); +} + +verifyDefaults(options, centerOptions); +verifyExists(options.port, "Port can not be undefined or empty! (--port)", logger); +verifyExists(options.systemid, "SystemID can not be undefined or empty! (--systemid)", logger); +verifyExists(options.password, "Password can not be undefined or empty! (--password)", logger); + +let inFlight = 0; +let sent = 0; +let success = 0; +let failed = 0; +const sendTimer = new NanoTimer(); +const metricManager = new MetricManager(options); + +// TODO: Currently bars are broken +// A major rework will need to happen before bars are able to play nice with multiple sessions +// TODO: Maybe add only receiver and only transmitter modes instead of transciever +// Instead just use the same timer but make a pool of connections; That way both problems will be solved +function startInterval(sessions, sessionLogger, rxMetrics) { + if (!options.messagecount > 0) { + sessionLogger.info("No messages to send"); + return; + } + let sessionPointer = 0; + sendTimer.setInterval( + async () => { + if (sent >= options.messagecount) { + sessionLogger.info(`Finished sending messages success:${success}, failed:${failed}, idling...`); + sendTimer.clearInterval(); + } else if (inFlight < options.window) { + sessionLogger.info(`Sending message ${sent + 1}/${options.messagecount}`); + const pdu = new smpp.PDU("deliver_sm", { + source_addr: options.source, + destination_addr: options.destination, + short_message: options.message, + }); + + if (sessionPointer >= sessions.length) { + sessionPointer = 0; + } + sendPdu(sessions[sessionPointer++], pdu, sessionLogger, options.longsms) + .then((resp) => { + inFlight--; + sessionLogger.info(`Received response with id ${resp.message_id}`); + success++; + }) + .catch((resp) => { + inFlight--; + sessionLogger.warn(`Message failed with id ${resp.message_id}`); + failed++; + }); + sent++; + inFlight++; + } else { + sessionLogger.warn( + `${inFlight}/${options.window} messages pending, waiting for a reply before sending more` + ); + sendTimer.clearInterval(); + setTimeout(() => startInterval(sessions, sessionLogger), options.windowsleep); + } + }, + "", + `${1 / options.mps} s` + ); +} + +logger.info(`Staring server on port ${options.port}...`); +let sessionid = 1; +let messageid = 0; +const sessions = []; +const server = smpp.createServer( + { + debug: options.debug, + }, + function (session) { + const id = sessionid++; + const sessionLogger = createSessionLogger(options, id); + const rxMetrics = metricManager.AddMetrics(`Session-${id}-RX`); + const txMetrics = metricManager.AddMetrics(`Session-${id}-TX`); + + session.on("bind_transceiver", function (pdu) { + if (pdu.system_id === options.systemid && pdu.password === options.password) { + sessions.push(session); + sessionLogger.info(`Client connected, currently: ${sessions.length}`); + session.send(pdu.response()); + startInterval(sessions, sessionLogger); + } else { + sessionLogger.warn( + `Client tried to connect with incorrect login ('${pdu.system_id}' '${pdu.password}')` + ); + pdu.response({ + command_status: smpp.ESME_RBINDFAIL, + }); + session.close(); + } + }); + session.on("bind_transmitter", function (pdu) { + if (pdu.system_id === options.systemid && pdu.password === options.password) { + sessionLogger.info("Client connected"); + session.send(pdu.response()); + startInterval(session, sessionLogger, rxMetrics); + } else { + sessionLogger.warn( + `Client tried to connect with incorrect login ('${pdu.system_id}' '${pdu.password}')` + ); + pdu.response({ + command_status: smpp.ESME_RBINDFAIL, + }); + session.close(); + } + }); + session.on("bind_receiver", function (pdu) { + if (pdu.system_id === options.systemid && pdu.password === options.password) { + sessionLogger.info("Client connected"); + session.send(pdu.response()); + startInterval(session, sessionLogger); + } else { + sessionLogger.warn( + `Client tried to connect with incorrect login ('${pdu.system_id}' '${pdu.password}')` + ); + pdu.response({ + command_status: smpp.ESME_RBINDFAIL, + }); + session.close(); + } + }); + session.on("enquire_link", function (pdu) { + session.send(pdu.response()); + }); + session.on("submit_sm", async function (pdu) { + if (!options.dr) { + sessionLogger.info("Replying to incoming submit_sm"); + if (options.bars) { + rxMetrics.AddEvent(); + } + // setTimeout(() => { + // session.send(pdu.response()); + // }, 200); + session.send(pdu.response()); + return; + } + + sessionLogger.info("Generating DR for incoming submit_sm"); + let response = pdu.response(); + + let smppid = messageid++; + if (options.randid) { + smppid = crypto.randomBytes(8).toString("hex"); + } + + response.message_id = smppid.toString(16); + session.send(response); + + let drMessage = ""; + let date = new Date() + .toISOString() + .replace(/T/, "") + .replace(/\..+/, "") + .replace(/-/g, "") + .replace(/:/g, "") + .substring(2, 12); + + drMessage += "id:" + response.message_id + " "; + drMessage += "sub:001 "; + drMessage += "dlvrd:001 "; + drMessage += "submit date:" + date + " "; + drMessage += "done date:" + date + " "; + drMessage += "stat:DELIVRD "; + drMessage += "err:000 "; + drMessage += "text:"; + + const DRPdu = { + source_addr: pdu.destination_addr, + destination_addr: pdu.source_addr, + short_message: drMessage, + esm_class: 4, + }; + sessionLogger.info(`Generated DR as ${drMessage}`); + session.deliver_sm(DRPdu); + if (txMetrics) { + txMetrics.AddEvent(); + } + }); + + session.on("close", function () { + sessions.splice(sessions.indexOf(session), 1); + sessionLogger.warn(`Session closed, now ${sessions.length}`); + session.close(); + if (sessions.length === 0) { + sessionLogger.info("No more sessions, stopping sending timer"); + sendTimer.clearInterval(); + } + }); + session.on("error", function (err) { + sessionLogger.error(`Fatal error ${err}`); + session.close(); + }); + } +); + +server.on("error", function (err) { + logger.error(`Fatal server error ${err}`); + server.close(); + process.exit(1); +}); + +server.listen(options.port); +logger.info(`SMPP Server listening on ${options.port}`); diff --git a/cliOptions.js b/cliOptions.js index a396346..3eb809b 100644 --- a/cliOptions.js +++ b/cliOptions.js @@ -1,166 +1,166 @@ -const clientOptions = [ - { name: "help", type: Boolean, description: "Display this usage guide." }, - { name: "host", alias: "h", type: String, description: "The host (IP) to connect to." }, - { name: "port", alias: "p", type: Number, description: "The port to connect to." }, - { name: "systemid", alias: "s", type: String, description: "SMPP related login info." }, - { name: "password", alias: "w", type: String, description: "SMPP related login info." }, - { name: "sessions", type: Number, description: "Number of sessions to start, defaults to 1.", defaultOption: 1 }, - { - name: "messagecount", - type: Number, - description: "Number of messages to send; Optional, defaults to 1.", - defaultOption: 1, - }, - { - name: "window", - type: Number, - description: - "Defines the amount of messages that are allowed to be 'in flight'. The client no longer waits for a response before sending the next message for up to messages. Defaults to 100.", - defaultOption: 100, - }, - { - name: "windowsleep", - type: Number, - description: - "Defines the amount time (in ms) waited between retrying in the case of full window. Defaults to 100.", - defaultOption: 100, - }, - { - name: "mps", - type: Number, - description: "Number of messages to send per second", - defaultOption: 999999, - }, - { - name: "source", - type: String, - description: "Source field of the sent messages.", - defaultOption: "smppDebugClient", - }, - { - name: "destination", - type: String, - description: "Destination field of the sent messages.", - defaultOption: "smpp", - }, - { - name: "message", - type: String, - description: "Text content of the sent messages.", - defaultOption: "smpp debug message", - }, - { name: "debug", type: Boolean, description: "Display all traffic to and from the client; Debug mode." }, - { name: "logs", type: Boolean, description: "Write logs (to stdout), defaults to true." }, - { - name: "bars", - type: Boolean, - description: "Display TX and RX bars. Can be used with logs (although it will make a mess)." - }, - { - name: "metricsinterval", - type: Number, - defaultOption: 5, - description: "Interval for measuring metrics. A value of 5 considers the packets within the last 5 seconds. Defaults to 5." - }, - { - name: "defaultmaxrate", - type: Number, - defaultOption: 1000, - description: "Default max rate for metrics/bars." - }, - { - name: "longsms", - type: Boolean, - description: "Split messages into multiple parts. Applies only if message is too big for one packet." - }, -]; - -const centerOptions = [ - { name: "help", type: Boolean, description: "Display this usage guide." }, - { name: "port", alias: "p", type: Number, description: "The port to connect to." }, - { name: "systemid", alias: "s", type: String, description: "SMPP related login info." }, - { name: "password", alias: "w", type: String, description: "SMPP related login info." }, - { name: "dr", type: Boolean, description: "Whether or not to send Delivery Reports.", defaultOption: false }, - { - name: "randid", - type: Boolean, - description: "SMPP ID generation is entirely random instead of sequential.", - defaultOption: false, - }, - { - name: "sessions", - type: Number, - description: "Maximum number of client sessions to accept, defaults to 8.", - defaultOption: 8, - }, - { - name: "messagecount", - type: Number, - description: "Number of messages to send; Optional, defaults to 0.", - defaultOption: 0, - }, - { - name: "window", - type: Number, - description: - "Defines the amount of messages that are allowed to be 'in flight'. The client no longer waits for a response before sending the next message for up to messages. Defaults to 100.", - defaultOption: 100, - }, - { - name: "windowsleep", - type: Number, - description: - "Defines the amount time (in ms) waited between retrying in the case of full window. Defaults to 100.", - defaultOption: 100, - }, - { - name: "mps", - type: Number, - description: "Number of messages to send per second", - defaultOption: 999999, - }, - { - name: "source", - type: String, - description: "Source field of the sent messages.", - defaultOption: "smppDebugClient", - }, - { - name: "destination", - type: String, - description: "Destination field of the sent messages.", - defaultOption: "smpp", - }, - { - name: "message", - type: String, - description: "Text content of the sent messages.", - defaultOption: "smpp debug message", - }, - { name: "debug", type: Boolean, description: "Display all traffic to and from the center; Debug mode." }, - { name: "logs", type: Boolean, description: "Write logs (to stdout), defaults to true." }, - { - name: "bars", - type: Boolean, - description: "Display TX and RX bars. Can be used with logs (although it will make a mess)." - }, - { - name: "metricsinterval", - type: Number, - defaultOption: 5, - description: "Interval for measuring metrics. A value of 5 considers the packets within the last 5 seconds. Defaults to 5." - }, - { - name: "defaultmaxrate", - type: Number, - defaultOption: 1000, - description: "Default max rate for metrics/bars." - }, - { - name: "longsms", - type: Boolean, - description: "Split messages into multiple parts. Applies only if message is too big for one packet." - }, -]; - -module.exports = { clientOptions, centerOptions }; +const clientOptions = [ + { name: "help", type: Boolean, description: "Display this usage guide." }, + { name: "host", alias: "h", type: String, description: "The host (IP) to connect to." }, + { name: "port", alias: "p", type: Number, description: "The port to connect to." }, + { name: "systemid", alias: "s", type: String, description: "SMPP related login info." }, + { name: "password", alias: "w", type: String, description: "SMPP related login info." }, + { name: "sessions", type: Number, description: "Number of sessions to start, defaults to 1.", defaultOption: 1 }, + { + name: "messagecount", + type: Number, + description: "Number of messages to send; Optional, defaults to 1.", + defaultOption: 1, + }, + { + name: "window", + type: Number, + description: + "Defines the amount of messages that are allowed to be 'in flight'. The client no longer waits for a response before sending the next message for up to messages. Defaults to 100.", + defaultOption: 100, + }, + { + name: "windowsleep", + type: Number, + description: + "Defines the amount time (in ms) waited between retrying in the case of full window. Defaults to 100.", + defaultOption: 100, + }, + { + name: "mps", + type: Number, + description: "Number of messages to send per second", + defaultOption: 999999, + }, + { + name: "source", + type: String, + description: "Source field of the sent messages.", + defaultOption: "smppDebugClient", + }, + { + name: "destination", + type: String, + description: "Destination field of the sent messages.", + defaultOption: "smpp", + }, + { + name: "message", + type: String, + description: "Text content of the sent messages.", + defaultOption: "smpp debug message", + }, + { name: "debug", type: Boolean, description: "Display all traffic to and from the client; Debug mode." }, + { name: "logs", type: Boolean, description: "Write logs (to stdout), defaults to true." }, + { + name: "bars", + type: Boolean, + description: "Display TX and RX bars. Can be used with logs (although it will make a mess)." + }, + { + name: "metricsinterval", + type: Number, + defaultOption: 5, + description: "Interval for measuring metrics. A value of 5 considers the packets within the last 5 seconds. Defaults to 5." + }, + { + name: "defaultmaxrate", + type: Number, + defaultOption: 1000, + description: "Default max rate for metrics/bars." + }, + { + name: "longsms", + type: Boolean, + description: "Split messages into multiple parts. Applies only if message is too big for one packet." + }, +]; + +const centerOptions = [ + { name: "help", type: Boolean, description: "Display this usage guide." }, + { name: "port", alias: "p", type: Number, description: "The port to connect to." }, + { name: "systemid", alias: "s", type: String, description: "SMPP related login info." }, + { name: "password", alias: "w", type: String, description: "SMPP related login info." }, + { name: "dr", type: Boolean, description: "Whether or not to send Delivery Reports.", defaultOption: false }, + { + name: "randid", + type: Boolean, + description: "SMPP ID generation is entirely random instead of sequential.", + defaultOption: false, + }, + { + name: "sessions", + type: Number, + description: "Maximum number of client sessions to accept, defaults to 8.", + defaultOption: 8, + }, + { + name: "messagecount", + type: Number, + description: "Number of messages to send; Optional, defaults to 0.", + defaultOption: 0, + }, + { + name: "window", + type: Number, + description: + "Defines the amount of messages that are allowed to be 'in flight'. The client no longer waits for a response before sending the next message for up to messages. Defaults to 100.", + defaultOption: 100, + }, + { + name: "windowsleep", + type: Number, + description: + "Defines the amount time (in ms) waited between retrying in the case of full window. Defaults to 100.", + defaultOption: 100, + }, + { + name: "mps", + type: Number, + description: "Number of messages to send per second", + defaultOption: 999999, + }, + { + name: "source", + type: String, + description: "Source field of the sent messages.", + defaultOption: "smppDebugClient", + }, + { + name: "destination", + type: String, + description: "Destination field of the sent messages.", + defaultOption: "smpp", + }, + { + name: "message", + type: String, + description: "Text content of the sent messages.", + defaultOption: "smpp debug message", + }, + { name: "debug", type: Boolean, description: "Display all traffic to and from the center; Debug mode." }, + { name: "logs", type: Boolean, description: "Write logs (to stdout), defaults to true." }, + { + name: "bars", + type: Boolean, + description: "Display TX and RX bars. Can be used with logs (although it will make a mess)." + }, + { + name: "metricsinterval", + type: Number, + defaultOption: 5, + description: "Interval for measuring metrics. A value of 5 considers the packets within the last 5 seconds. Defaults to 5." + }, + { + name: "defaultmaxrate", + type: Number, + defaultOption: 1000, + description: "Default max rate for metrics/bars." + }, + { + name: "longsms", + type: Boolean, + description: "Split messages into multiple parts. Applies only if message is too big for one packet." + }, +]; + +module.exports = { clientOptions, centerOptions }; diff --git a/client.js b/client.js index e578333..889f514 100644 --- a/client.js +++ b/client.js @@ -1,164 +1,164 @@ -const smpp = require("smpp"); -const commandLineArgs = require("command-line-args"); -const commandLineUsage = require("command-line-usage"); -const NanoTimer = require("nanotimer"); -const { createBaseLogger, createSessionLogger } = require("./logger"); -const { verifyDefaults, verifyExists, sendPdu } = require("./utils"); -const { clientOptions } = require("./cliOptions"); -const { MetricManager } = require("./metrics/metricManager"); - -const options = commandLineArgs(clientOptions); -const logger = createBaseLogger(options); - -if (options.help) { - const usage = commandLineUsage([ - { - header: "CLI SMPP (Client)", - }, - { - header: "Options", - optionList: clientOptions, - }, - { - content: "Project home: {underline https://github.com/PhatDave/SMPP_CLI}", - }, - ]); - console.log(usage); - process.exit(0); -} - -verifyDefaults(options, clientOptions); -verifyExists(options.host, "Host can not be undefined or empty! (--host)", logger); -verifyExists(options.port, "Port can not be undefined or empty! (--port)", logger); -verifyExists(options.systemid, "SystemID can not be undefined or empty! (--systemid)", logger); -verifyExists(options.password, "Password can not be undefined or empty! (--password)", logger); - -let inFlight = 0; -let sent = 0; -let success = 0; -let failed = 0; -const sendTimer = new NanoTimer(); -const metricManager = new MetricManager(options); - -function startInterval(session, sessionLogger, metrics) { - if (!metrics.progress && options.bars === true) { - metrics.progress = metricManager.AddMetrics("Send progress", false); - metrics.progress.bar.total = options.messagecount; - metrics.window = metricManager.AddMetrics("Send window", false); - metrics.window.bar.total = options.window; - } - sendTimer.setInterval( - async () => { - if (sent >= options.messagecount) { - sessionLogger.info(`Finished sending messages success:${success}, failed:${failed}, idling...`); - sendTimer.clearInterval(); - } else if (inFlight < options.window) { - sessionLogger.info(`Sending message ${sent + 1}/${options.messagecount}`); - if (options.bars) { - metrics.progress.bar.increment(); - metrics.window.bar.increment(); - } - const pdu = new smpp.PDU("submit_sm", { - source_addr: options.source, - destination_addr: options.destination, - short_message: options.message, - }); - - sendPdu(session, pdu, sessionLogger, options.longsms) - .then((resp) => { - inFlight--; - sessionLogger.info(`Received response with id ${resp.message_id}`); - success++; - }) - .catch((resp) => { - inFlight--; - sessionLogger.warn(`Message failed with id ${resp.message_id}`); - failed++; - }); - - if (metrics.txMetrics) { - metrics.txMetrics.AddEvent(); - } - sent++; - inFlight++; - } else { - sessionLogger.warn( - `${inFlight}/${options.window} messages pending, waiting for a reply before sending more` - ); - sendTimer.clearInterval(); - setTimeout(() => startInterval(session, sessionLogger, metrics), options.windowsleep); - } - }, - "", - `${1 / options.mps} s` - ); -} - -for (let i = 0; i < options.sessions; i++) { - const sessionLogger = createSessionLogger(options, i); - sessionLogger.info(`Connecting to ${options.host}:${options.port}...`); - const session = smpp.connect( - { - url: `smpp://${options.host}:${options.port}`, - auto_enquire_link_period: 10000, - debug: options.debug, - }, - function () { - sessionLogger.info( - `Connected, sending bind_transciever with systemId '${options.systemid}' and password '${options.password}'...` - ); - session.on('close', function () { - sessionLogger.error(`Session closed`); - process.exit(1); - }); - session.bind_transceiver( - { - system_id: options.systemid, - password: options.password, - }, - function (pdu) { - if (pdu.command_status === 0) { - sessionLogger.info( - `Successfully bound, sending ${options.messagecount} messages '${options.source}'->'${options.destination}' ('${options.message}')` - ); - const rxMetrics = metricManager.AddMetrics(`Session-${i}-RX`); - const txMetrics = metricManager.AddMetrics(`Session-${i}-TX`); - startInterval(session, sessionLogger, { - rxMetrics, - txMetrics, - }); - - session.on("deliver_sm", function (pdu) { - if (rxMetrics) { - rxMetrics.AddEvent(); - } - sessionLogger.info("Got deliver_sm, replying..."); - // setTimeout(() => { - // session.send(pdu.response()); - // txMetrics.AddEvent(); - // }, 200); - session.send(pdu.response()); - if (txMetrics) { - txMetrics.AddEvent(); - } - }); - session.on("enquire_link", function (pdu) { - session.send(pdu.response()); - }); - session.on("close", function () { - sessionLogger.error(`Session closed`); - process.exit(1); - }); - session.on("error", function (err) { - sessionLogger.error(`Fatal error ${err}`); - process.exit(1); - }); - } else { - sessionLogger.error(`Failed to bind, status ${pdu.command_status}`); - process.exit(1); - } - } - ); - } - ); -} +const smpp = require("smpp"); +const commandLineArgs = require("command-line-args"); +const commandLineUsage = require("command-line-usage"); +const NanoTimer = require("nanotimer"); +const { createBaseLogger, createSessionLogger } = require("./logger"); +const { verifyDefaults, verifyExists, sendPdu } = require("./utils"); +const { clientOptions } = require("./cliOptions"); +const { MetricManager } = require("./metrics/metricManager"); + +const options = commandLineArgs(clientOptions); +const logger = createBaseLogger(options); + +if (options.help) { + const usage = commandLineUsage([ + { + header: "CLI SMPP (Client)", + }, + { + header: "Options", + optionList: clientOptions, + }, + { + content: "Project home: {underline https://github.com/PhatDave/SMPP_CLI}", + }, + ]); + console.log(usage); + process.exit(0); +} + +verifyDefaults(options, clientOptions); +verifyExists(options.host, "Host can not be undefined or empty! (--host)", logger); +verifyExists(options.port, "Port can not be undefined or empty! (--port)", logger); +verifyExists(options.systemid, "SystemID can not be undefined or empty! (--systemid)", logger); +verifyExists(options.password, "Password can not be undefined or empty! (--password)", logger); + +let inFlight = 0; +let sent = 0; +let success = 0; +let failed = 0; +const sendTimer = new NanoTimer(); +const metricManager = new MetricManager(options); + +function startInterval(session, sessionLogger, metrics) { + if (!metrics.progress && options.bars === true) { + metrics.progress = metricManager.AddMetrics("Send progress", false); + metrics.progress.bar.total = options.messagecount; + metrics.window = metricManager.AddMetrics("Send window", false); + metrics.window.bar.total = options.window; + } + sendTimer.setInterval( + async () => { + if (sent >= options.messagecount) { + sessionLogger.info(`Finished sending messages success:${success}, failed:${failed}, idling...`); + sendTimer.clearInterval(); + } else if (inFlight < options.window) { + sessionLogger.info(`Sending message ${sent + 1}/${options.messagecount}`); + if (options.bars) { + metrics.progress.bar.increment(); + metrics.window.bar.increment(); + } + const pdu = new smpp.PDU("submit_sm", { + source_addr: options.source, + destination_addr: options.destination, + short_message: options.message, + }); + + sendPdu(session, pdu, sessionLogger, options.longsms) + .then((resp) => { + inFlight--; + sessionLogger.info(`Received response with id ${resp.message_id}`); + success++; + }) + .catch((resp) => { + inFlight--; + sessionLogger.warn(`Message failed with id ${resp.message_id}`); + failed++; + }); + + if (metrics.txMetrics) { + metrics.txMetrics.AddEvent(); + } + sent++; + inFlight++; + } else { + sessionLogger.warn( + `${inFlight}/${options.window} messages pending, waiting for a reply before sending more` + ); + sendTimer.clearInterval(); + setTimeout(() => startInterval(session, sessionLogger, metrics), options.windowsleep); + } + }, + "", + `${1 / options.mps} s` + ); +} + +for (let i = 0; i < options.sessions; i++) { + const sessionLogger = createSessionLogger(options, i); + sessionLogger.info(`Connecting to ${options.host}:${options.port}...`); + const session = smpp.connect( + { + url: `smpp://${options.host}:${options.port}`, + auto_enquire_link_period: 10000, + debug: options.debug, + }, + function () { + sessionLogger.info( + `Connected, sending bind_transciever with systemId '${options.systemid}' and password '${options.password}'...` + ); + session.on('close', function () { + sessionLogger.error(`Session closed`); + process.exit(1); + }); + session.bind_transceiver( + { + system_id: options.systemid, + password: options.password, + }, + function (pdu) { + if (pdu.command_status === 0) { + sessionLogger.info( + `Successfully bound, sending ${options.messagecount} messages '${options.source}'->'${options.destination}' ('${options.message}')` + ); + const rxMetrics = metricManager.AddMetrics(`Session-${i}-RX`); + const txMetrics = metricManager.AddMetrics(`Session-${i}-TX`); + startInterval(session, sessionLogger, { + rxMetrics, + txMetrics, + }); + + session.on("deliver_sm", function (pdu) { + if (rxMetrics) { + rxMetrics.AddEvent(); + } + sessionLogger.info("Got deliver_sm, replying..."); + // setTimeout(() => { + // session.send(pdu.response()); + // txMetrics.AddEvent(); + // }, 200); + session.send(pdu.response()); + if (txMetrics) { + txMetrics.AddEvent(); + } + }); + session.on("enquire_link", function (pdu) { + session.send(pdu.response()); + }); + session.on("close", function () { + sessionLogger.error(`Session closed`); + process.exit(1); + }); + session.on("error", function (err) { + sessionLogger.error(`Fatal error ${err}`); + process.exit(1); + }); + } else { + sessionLogger.error(`Failed to bind, status ${pdu.command_status}`); + process.exit(1); + } + } + ); + } + ); +} diff --git a/logger.js b/logger.js index d909f6e..2a8c735 100644 --- a/logger.js +++ b/logger.js @@ -1,75 +1,75 @@ -const { createLogger, format, transports } = require("winston"); -const { combine, timestamp, label, printf } = format; - -const defaultFormat = printf(({ level, message, timestamp }) => { - return `${timestamp} ${level}: ${message}`; -}); -const sessionFormat = printf(({ level, message, label, timestamp }) => { - return `${timestamp} [Session ${label}] ${level}: ${message}`; -}); -function createBaseLogger(options) { - const logger = createLogger({ - format: combine(format.colorize({ all: true }), timestamp(), defaultFormat), - transports: [new transports.Console()], - }); - - const oldInfo = logger.info; - const oldWarn = logger.info; - const oldError = logger.error; - logger.info = function (input) { - if (!shouldLog(options)) { - return; - } - oldInfo(input); - }; - logger.error = function (input) { - if (!shouldLog(options)) { - return; - } - oldError(input); - }; - logger.warn = function (input) { - if (!shouldLog(options)) { - return; - } - oldWarn(input); - }; - - return logger; -} -function createSessionLogger(options, ilabel) { - const logger = createLogger({ - format: combine(label({ label: ilabel }), format.colorize({ all: true }), timestamp(), sessionFormat), - transports: [new transports.Console()], - }); - - const oldInfo = logger.info; - const oldWarn = logger.info; - const oldError = logger.error; - logger.info = function (input) { - if (!shouldLog(options)) { - return; - } - oldInfo(input); - }; - logger.error = function (input) { - if (!shouldLog(options)) { - return; - } - oldError(input); - }; - logger.warn = function (input) { - if (!shouldLog(options)) { - return; - } - oldWarn(input); - }; - - return logger; -} - -function shouldLog(options) { - return options.logs || !options.bars; -} - -module.exports = { createBaseLogger, createSessionLogger }; +const { createLogger, format, transports } = require("winston"); +const { combine, timestamp, label, printf } = format; + +const defaultFormat = printf(({ level, message, timestamp }) => { + return `${timestamp} ${level}: ${message}`; +}); +const sessionFormat = printf(({ level, message, label, timestamp }) => { + return `${timestamp} [Session ${label}] ${level}: ${message}`; +}); +function createBaseLogger(options) { + const logger = createLogger({ + format: combine(format.colorize({ all: true }), timestamp(), defaultFormat), + transports: [new transports.Console()], + }); + + const oldInfo = logger.info; + const oldWarn = logger.info; + const oldError = logger.error; + logger.info = function (input) { + if (!shouldLog(options)) { + return; + } + oldInfo(input); + }; + logger.error = function (input) { + if (!shouldLog(options)) { + return; + } + oldError(input); + }; + logger.warn = function (input) { + if (!shouldLog(options)) { + return; + } + oldWarn(input); + }; + + return logger; +} +function createSessionLogger(options, ilabel) { + const logger = createLogger({ + format: combine(label({ label: ilabel }), format.colorize({ all: true }), timestamp(), sessionFormat), + transports: [new transports.Console()], + }); + + const oldInfo = logger.info; + const oldWarn = logger.info; + const oldError = logger.error; + logger.info = function (input) { + if (!shouldLog(options)) { + return; + } + oldInfo(input); + }; + logger.error = function (input) { + if (!shouldLog(options)) { + return; + } + oldError(input); + }; + logger.warn = function (input) { + if (!shouldLog(options)) { + return; + } + oldWarn(input); + }; + + return logger; +} + +function shouldLog(options) { + return options.logs || !options.bars; +} + +module.exports = { createBaseLogger, createSessionLogger }; diff --git a/metrics/circularBuffer.js b/metrics/circularBuffer.js index 603617d..64047a4 100644 --- a/metrics/circularBuffer.js +++ b/metrics/circularBuffer.js @@ -1,46 +1,46 @@ -class CircularBuffer { - constructor(size) { - this.buffer = new Array(size); - this.size = size; - this.head = 0; - this.tail = 0; - } - - push(item) { - this.buffer[this.head] = item; - this.head = (this.head + 1) % this.size; - if (this.head === this.tail) { - this.tail = (this.tail + 1) % this.size; - } - } - - toArray() { - const result = []; - let current = this.tail; - for (let i = 0; i < this.size; i++) { - if (this.buffer[current] !== undefined) { - result.push(this.buffer[current]); - } - current = (current + 1) % this.size; - } - return result; - } - - toArrayRecent(n = 10) { - const result = []; - const threshold = Date.now() - n * 1000; - - let current = (this.head - 1 + this.size) % this.size; - while (current !== this.tail) { - if (this.buffer[current] !== undefined && this.buffer[current].timestamp > threshold) { - result.push(this.buffer[current]); - } else { - break; - } - current = (current - 1 + this.size) % this.size; - } - return result; - } -} - -module.exports = { CircularBuffer }; +class CircularBuffer { + constructor(size) { + this.buffer = new Array(size); + this.size = size; + this.head = 0; + this.tail = 0; + } + + push(item) { + this.buffer[this.head] = item; + this.head = (this.head + 1) % this.size; + if (this.head === this.tail) { + this.tail = (this.tail + 1) % this.size; + } + } + + toArray() { + const result = []; + let current = this.tail; + for (let i = 0; i < this.size; i++) { + if (this.buffer[current] !== undefined) { + result.push(this.buffer[current]); + } + current = (current + 1) % this.size; + } + return result; + } + + toArrayRecent(n = 10) { + const result = []; + const threshold = Date.now() - n * 1000; + + let current = (this.head - 1 + this.size) % this.size; + while (current !== this.tail) { + if (this.buffer[current] !== undefined && this.buffer[current].timestamp > threshold) { + result.push(this.buffer[current]); + } else { + break; + } + current = (current - 1 + this.size) % this.size; + } + return result; + } +} + +module.exports = { CircularBuffer }; diff --git a/metrics/metricManager.js b/metrics/metricManager.js index 33e2e21..d867353 100644 --- a/metrics/metricManager.js +++ b/metrics/metricManager.js @@ -1,30 +1,30 @@ -const cliProgress = require("cli-progress"); -const { Metric } = require("./metrics"); - -class MetricManager { - constructor(options) { - this.options = options; - if (options.bars) { - this.metricBufferSize = 1000; - this.multibar = new cliProgress.MultiBar( - { - clearOnComplete: false, - barCompleteChar: "\u2588", - barIncompleteChar: "\u2591", - format: " {bar} | {name} | {value}/{total}", - }, - cliProgress.Presets.shades_grey - ); - setInterval(() => this.multibar.update(), 200); - } - } - - AddMetrics(name, refresh = true) { - if (this.options.bars) { - const metric = new Metric(name, this.multibar, this.metricBufferSize, this.options, refresh); - return metric; - } - } -} - -module.exports = { MetricManager }; +const cliProgress = require("cli-progress"); +const { Metric } = require("./metrics"); + +class MetricManager { + constructor(options) { + this.options = options; + if (options.bars) { + this.metricBufferSize = 1000; + this.multibar = new cliProgress.MultiBar( + { + clearOnComplete: false, + barCompleteChar: "\u2588", + barIncompleteChar: "\u2591", + format: " {bar} | {name} | {value}/{total}", + }, + cliProgress.Presets.shades_grey + ); + setInterval(() => this.multibar.update(), 200); + } + } + + AddMetrics(name, refresh = true) { + if (this.options.bars) { + const metric = new Metric(name, this.multibar, this.metricBufferSize, this.options, refresh); + return metric; + } + } +} + +module.exports = { MetricManager }; diff --git a/metrics/metrics.js b/metrics/metrics.js index 7a13954..5e9d4ae 100644 --- a/metrics/metrics.js +++ b/metrics/metrics.js @@ -1,39 +1,39 @@ -const { CircularBuffer } = require("./circularBuffer"); - -class Metric { - constructor(barName, multibar, bufferSize, options, refresh = true) { - this.options = options; - this.multibar = multibar; - this.bar = multibar.create(0, 0); - this.bar.update(0, { name: barName }); - this.maxRate = this.options.defaultmaxrate; - this.bar.total = this.maxRate; - this.buffer = new CircularBuffer(bufferSize); - if (refresh) { - setInterval(this.UpdateBar.bind(this), 100); - } - } - - AddEvent() { - const timestamp = Date.now(); - this.buffer.push({ timestamp, count: 1 }); - } - - GetRate() { - const entries = this.buffer.toArrayRecent(this.options.metricsinterval); - - const totalRX = entries.reduce((sum, entry) => sum + entry.count, 0); - return Math.round((totalRX / this.options.metricsinterval) * 100) / 100; - } - - UpdateBar() { - const eps = this.GetRate(); - if (eps > this.maxRate) { - this.bar.total = eps; - this.maxRate = eps; - } - this.bar.update(eps); - } -} - -module.exports = { Metric }; +const { CircularBuffer } = require("./circularBuffer"); + +class Metric { + constructor(barName, multibar, bufferSize, options, refresh = true) { + this.options = options; + this.multibar = multibar; + this.bar = multibar.create(0, 0); + this.bar.update(0, { name: barName }); + this.maxRate = this.options.defaultmaxrate; + this.bar.total = this.maxRate; + this.buffer = new CircularBuffer(bufferSize); + if (refresh) { + setInterval(this.UpdateBar.bind(this), 100); + } + } + + AddEvent() { + const timestamp = Date.now(); + this.buffer.push({ timestamp, count: 1 }); + } + + GetRate() { + const entries = this.buffer.toArrayRecent(this.options.metricsinterval); + + const totalRX = entries.reduce((sum, entry) => sum + entry.count, 0); + return Math.round((totalRX / this.options.metricsinterval) * 100) / 100; + } + + UpdateBar() { + const eps = this.GetRate(); + if (eps > this.maxRate) { + this.bar.total = eps; + this.maxRate = eps; + } + this.bar.update(eps); + } +} + +module.exports = { Metric }; diff --git a/tests/utils.test.js b/tests/utils.test.js index 332e038..e4ff21f 100644 --- a/tests/utils.test.js +++ b/tests/utils.test.js @@ -1,94 +1,94 @@ -const smpp = require("smpp"); -const { splitToParts, verifyExists, getCharacterSizeForEncoding } = require("../utils"); - -describe("splitToParts", () => { - // A pdu is expected to be one part if it has less than 160 characters and is encoded using GSM7 (data_coding = null or 0) - // Given a pdu with short_message length less than 160 chars, it should return an array with a single pdu. - it("should return an array with a single pdu when short_message length is less than or equal to maxMessageSizeBits", () => { - const pdu = new smpp.PDU("deliver_sm", { - short_message: "test message", - }); - const result = splitToParts(pdu); - expect(result.length).toBe(1); - }); - - // Given a pdu with short_message length greater than 160 chars, it should return an array with 2 pdus. - it("should return an array with two pdus when short_message length is greater than maxMessageSizeBits and less than or equal to maxMessageSizeBits * 2", () => { - const pdu = new smpp.PDU("deliver_sm", { - short_message: "c".repeat(200), - }); - const result = splitToParts(pdu); - expect(result.length).toBe(2); - }); - - // Given a pdu with short_message length greater than 320 chars, it should return an array with 2 pdus. - it("should return an array with three pdus when short_message length is greater than maxMessageSizeBits * 2 and less than or equal to maxMessageSizeBits * 3", () => { - const pdu = new smpp.PDU("deliver_sm", { - short_message: "c".repeat(400), - }); - const result = splitToParts(pdu); - expect(result.length).toBe(3); - }); - - // Given a pdu with short_message length equal to 0, it should return an empty array. - it("should return an empty array when short_message length is equal to 0", () => { - const pdu = new smpp.PDU("deliver_sm", { - short_message: "", - }); - const result = splitToParts(pdu); - expect(result.length).toBe(0); - }); - - // Given a pdu with short_message length equal to 320, it should return an array with two pdus. - it("should return an array with two pdus when short_message length is equal to maxMessageSizeBits", () => { - const pdu = new smpp.PDU("deliver_sm", { - short_message: "c".repeat(320), - }); - const result = splitToParts(pdu); - expect(result.length).toBe(2); - }); -}); - -describe("getCharacterSizeForEncoding", () => { - // Returns 7 when data_coding is 0 - it("should return 7 when data_coding is 0", () => { - const pdu = { data_coding: 0 }; - const result = getCharacterSizeForEncoding(pdu); - expect(result).toBe(7); - }); - - // Returns 8 when data_coding is 1 - it("should return 8 when data_coding is 1", () => { - const pdu = { data_coding: 1 }; - const result = getCharacterSizeForEncoding(pdu); - expect(result).toBe(8); - }); - - // Returns 16 when data_coding is 8 - it("should return 16 when data_coding is 8", () => { - const pdu = { data_coding: 8 }; - const result = getCharacterSizeForEncoding(pdu); - expect(result).toBe(16); - }); - - // Returns 7 when data_coding is null - it("should return 0 when data_coding is null", () => { - const pdu = { data_coding: null }; - const result = getCharacterSizeForEncoding(pdu); - expect(result).toBe(7); - }); - - // Returns 0 when data_coding is not a number - it("should return 0 when data_coding is not a number", () => { - const pdu = { data_coding: "abc" }; - const result = getCharacterSizeForEncoding(pdu); - expect(result).toBe(0); - }); - - // Returns 0 when data_coding is negative - it("should return 0 when data_coding is negative", () => { - const pdu = { data_coding: -1 }; - const result = getCharacterSizeForEncoding(pdu); - expect(result).toBe(0); - }); -}); +const smpp = require("smpp"); +const { splitToParts, verifyExists, getCharacterSizeForEncoding } = require("../utils"); + +describe("splitToParts", () => { + // A pdu is expected to be one part if it has less than 160 characters and is encoded using GSM7 (data_coding = null or 0) + // Given a pdu with short_message length less than 160 chars, it should return an array with a single pdu. + it("should return an array with a single pdu when short_message length is less than or equal to maxMessageSizeBits", () => { + const pdu = new smpp.PDU("deliver_sm", { + short_message: "test message", + }); + const result = splitToParts(pdu); + expect(result.length).toBe(1); + }); + + // Given a pdu with short_message length greater than 160 chars, it should return an array with 2 pdus. + it("should return an array with two pdus when short_message length is greater than maxMessageSizeBits and less than or equal to maxMessageSizeBits * 2", () => { + const pdu = new smpp.PDU("deliver_sm", { + short_message: "c".repeat(200), + }); + const result = splitToParts(pdu); + expect(result.length).toBe(2); + }); + + // Given a pdu with short_message length greater than 320 chars, it should return an array with 2 pdus. + it("should return an array with three pdus when short_message length is greater than maxMessageSizeBits * 2 and less than or equal to maxMessageSizeBits * 3", () => { + const pdu = new smpp.PDU("deliver_sm", { + short_message: "c".repeat(400), + }); + const result = splitToParts(pdu); + expect(result.length).toBe(3); + }); + + // Given a pdu with short_message length equal to 0, it should return an empty array. + it("should return an empty array when short_message length is equal to 0", () => { + const pdu = new smpp.PDU("deliver_sm", { + short_message: "", + }); + const result = splitToParts(pdu); + expect(result.length).toBe(0); + }); + + // Given a pdu with short_message length equal to 320, it should return an array with two pdus. + it("should return an array with two pdus when short_message length is equal to maxMessageSizeBits", () => { + const pdu = new smpp.PDU("deliver_sm", { + short_message: "c".repeat(320), + }); + const result = splitToParts(pdu); + expect(result.length).toBe(2); + }); +}); + +describe("getCharacterSizeForEncoding", () => { + // Returns 7 when data_coding is 0 + it("should return 7 when data_coding is 0", () => { + const pdu = { data_coding: 0 }; + const result = getCharacterSizeForEncoding(pdu); + expect(result).toBe(7); + }); + + // Returns 8 when data_coding is 1 + it("should return 8 when data_coding is 1", () => { + const pdu = { data_coding: 1 }; + const result = getCharacterSizeForEncoding(pdu); + expect(result).toBe(8); + }); + + // Returns 16 when data_coding is 8 + it("should return 16 when data_coding is 8", () => { + const pdu = { data_coding: 8 }; + const result = getCharacterSizeForEncoding(pdu); + expect(result).toBe(16); + }); + + // Returns 7 when data_coding is null + it("should return 0 when data_coding is null", () => { + const pdu = { data_coding: null }; + const result = getCharacterSizeForEncoding(pdu); + expect(result).toBe(7); + }); + + // Returns 0 when data_coding is not a number + it("should return 0 when data_coding is not a number", () => { + const pdu = { data_coding: "abc" }; + const result = getCharacterSizeForEncoding(pdu); + expect(result).toBe(0); + }); + + // Returns 0 when data_coding is negative + it("should return 0 when data_coding is negative", () => { + const pdu = { data_coding: -1 }; + const result = getCharacterSizeForEncoding(pdu); + expect(result).toBe(0); + }); +}); diff --git a/utils.js b/utils.js index 81d6f70..760d704 100644 --- a/utils.js +++ b/utils.js @@ -1,104 +1,104 @@ -const smpp = require("smpp"); - -function verifyExists(value, err, logger) { - if (!value) { - logger.error(err); - process.exit(0); - } -} -function verifyDefaults(options, definitions) { - for (const optionDefinition of definitions) { - if (optionDefinition.defaultOption) { - if (!options[optionDefinition.name]) { - options[optionDefinition.name] = optionDefinition.defaultOption; - } - } - } -} - -function getCharacterSizeForEncoding(pdu) { - let encoding = pdu.data_coding; - if (!encoding) { - encoding = 0; - } - let characterSizeBits = 0; - switch (encoding) { - case 0: - characterSizeBits = 7; - break; - case 1: - characterSizeBits = 8; - break; - case 8: - characterSizeBits = 16; - break; - } - return characterSizeBits; -} - -const maxMessageSizeBits = 1120; -function splitToParts(pdu) { - const charSize = getCharacterSizeForEncoding(pdu); - const maxMessageLength = maxMessageSizeBits / charSize; - - const splitMessage = []; - const message = pdu.short_message; - const messageLength = message.length; - const messageCount = (messageLength / maxMessageLength) | 0; - for (let i = 0; i < messageCount; i++) { - splitMessage.push(message.slice(i * maxMessageLength, i * maxMessageLength + maxMessageLength)); - } - - const pdus = splitMessage.map((messagePart, index) => { - let udh = Buffer.from([0x05, 0x00, 0x03, this.iterator++, messageCount, index + 1]); - - let partPdu = new smpp.PDU(pdu.command, { ...pdu }); - partPdu.short_message = { - udh: udh, - message: messagePart, - }; - return partPdu; - }); - return pdus; -} - -// TODO: Add "uselongsms" switch to options; -async function sendPdu(session, pdu, logger, uselongsms) { - return new Promise((resolve, reject) => { - if (uselongsms) { - const pdus = splitToParts(pdu); - logger.info(`Sending long sms of ${pdus.length} parts`); - const total = pdus.length; - let success = 0; - let failed = 0; - const promises = pdus.map((pdu) => { - return new Promise((resolve, reject) => { - session.send(pdu, (respPdu) => { - if (respPdu.command_status === 0) { - resolve(respPdu); - } else { - reject(respPdu); - } - }); - }); - }); - Promise.all(promises) - .then((responses) => { - resolve(responses[0]); - }) - .catch((error) => { - reject(error); - }); - } else { - session.send(pdu, (respPdu) => { - if (respPdu.command_status === 0) { - resolve(respPdu); - } else { - reject(respPdu); - } - }); - } - }); -} - -module.exports = { verifyDefaults, verifyExists, sendPdu, splitToParts, getCharacterSizeForEncoding }; +const smpp = require("smpp"); + +function verifyExists(value, err, logger) { + if (!value) { + logger.error(err); + process.exit(0); + } +} +function verifyDefaults(options, definitions) { + for (const optionDefinition of definitions) { + if (optionDefinition.defaultOption) { + if (!options[optionDefinition.name]) { + options[optionDefinition.name] = optionDefinition.defaultOption; + } + } + } +} + +function getCharacterSizeForEncoding(pdu) { + let encoding = pdu.data_coding; + if (!encoding) { + encoding = 0; + } + let characterSizeBits = 0; + switch (encoding) { + case 0: + characterSizeBits = 7; + break; + case 1: + characterSizeBits = 8; + break; + case 8: + characterSizeBits = 16; + break; + } + return characterSizeBits; +} + +const maxMessageSizeBits = 1120; +function splitToParts(pdu) { + const charSize = getCharacterSizeForEncoding(pdu); + const maxMessageLength = maxMessageSizeBits / charSize; + + const splitMessage = []; + const message = pdu.short_message; + const messageLength = message.length; + const messageCount = (messageLength / maxMessageLength) | 0; + for (let i = 0; i < messageCount; i++) { + splitMessage.push(message.slice(i * maxMessageLength, i * maxMessageLength + maxMessageLength)); + } + + const pdus = splitMessage.map((messagePart, index) => { + let udh = Buffer.from([0x05, 0x00, 0x03, this.iterator++, messageCount, index + 1]); + + let partPdu = new smpp.PDU(pdu.command, { ...pdu }); + partPdu.short_message = { + udh: udh, + message: messagePart, + }; + return partPdu; + }); + return pdus; +} + +// TODO: Add "uselongsms" switch to options; +async function sendPdu(session, pdu, logger, uselongsms) { + return new Promise((resolve, reject) => { + if (uselongsms) { + const pdus = splitToParts(pdu); + logger.info(`Sending long sms of ${pdus.length} parts`); + const total = pdus.length; + let success = 0; + let failed = 0; + const promises = pdus.map((pdu) => { + return new Promise((resolve, reject) => { + session.send(pdu, (respPdu) => { + if (respPdu.command_status === 0) { + resolve(respPdu); + } else { + reject(respPdu); + } + }); + }); + }); + Promise.all(promises) + .then((responses) => { + resolve(responses[0]); + }) + .catch((error) => { + reject(error); + }); + } else { + session.send(pdu, (respPdu) => { + if (respPdu.command_status === 0) { + resolve(respPdu); + } else { + reject(respPdu); + } + }); + } + }); +} + +module.exports = { verifyDefaults, verifyExists, sendPdu, splitToParts, getCharacterSizeForEncoding };