// Copyright 2020 Campbell Crowley. All rights reserved.
// Author: Campbell Crowley (web@campbellcrowley.com)
const socketIo = require('socket.io-client');
const common = require('../common.js');
const crypto = require('crypto');
const {fork, exec} = require('child_process');
const path = require('path');
const fs = require('fs');
const auth = require('../../auth.js');
const ShardingMaster = require('./ShardingMaster.js');
const configDir = path.resolve(__dirname + '/../../config/');
const botCWD = path.resolve(__dirname + '/../../');
/**
* @description The slave that is managed by {@link ShardingMaster}. This does
* nothing to communicate with Discord until the master has told it to do so.
* The main purpose of this is to connect and listen to the master for commands
* and messages. This class must have a config file generated by the master
* named similarly to `shard_abc_config.json` in the `./config/` directory.
* @class
*/
class ShardingSlave {
/**
* @description Starts the slave, and attempts to connect to the master
* immediately. Throws errors if setup is incorrect.
*/
constructor() {
common.begin(false, true);
const files = fs.readdirSync(configDir);
const file = files.find((el) => el.match(common.shardConfigRegex));
if (!file) {
throw new Error('Failed to find shard config file required for boot.');
}
const data = fs.readFileSync(`${configDir}/${file}`);
/**
* @description Parsed config file from disk.
* @private
* @type {object}
* @constant
*/
this._config = JSON.parse(data);
/**
* @description The settings the master has told us to operate with. This
* includes the botName, heartbeat settings, as well as shard ID and count.
* Null until a connection is established.
* @private
* @type {?object}
*/
this._settings = null;
/**
* @description This slave's ID/Name.
* @public
* @type {string}
* @constant
*/
this.id = this._config.id;
common.log(`Shard ${this.id} booting up...`, this.id);
/**
* @description The public key of this shard.
* @public
* @type {string}
* @constant
*/
this.pubKey = this._config.pubKey;
/**
* @description The private key identifying this shard.
* @private
* @type {string}
* @constant
*/
this._privKey = this._config.privKey;
/**
* @description The current status information about this shard. This is
* sent to the master as a heartbeat.
* @private
* @type {ShardingMaster.ShardStatus}
*/
this._status = new ShardingMaster.ShardStatus(this.id);
/**
* @description Timeout for respawn request.
* @private
* @type {?number}
* @default
*/
this._respawnTimeout = null;
/**
* @description Timeout to attempt reconnection if no heartbeat request was
* received from the master (pull), or the timeout until the next heartbeat
* will be sent (push).
* @private
* @type {?number}
* @default
*/
this._hbTimeout = null;
/**
* @description The timestamp at which the last message from the master was
* received.
* @private
* @type {number}
* @default
*/
this._lastSeen = 0;
/**
* @description Has the connection to the master been verified. If false,
* the current connection has not been established to be the correct
* endpoint. This may not necessarily be a security vulnerability however,
* as this is a redundant check in addition to the HTTPS websocket
* connection.
* @private
* @type {boolean}
* @default
*/
this._verified = false;
/**
* @description Ongoing promises for calls to
* {@link Discord}'s Shard#eval, mapped by the script they were
* called with.
* @type {Map<string, Promise>}
* @private
*/
this._evals = new Map();
const host = this._config.host;
const authHeader = this._generateAuthHeader();
/**
* @description The socket.io socket used to communicate with the master.
* @private
* @type {socketIo.Socket}
* @constant
*/
this._socket = socketIo(`${host.protocol}//${host.host}:${host.port}`, {
path: `${host.path}master/`,
extraHeaders: {authorization: authHeader},
});
this._socket.on('connect', () => this._socketConnected());
this._socket.on(
'reconnecting', (...args) => this._socketReconnecting(...args));
this._socket.on(
'disconnect', (...args) => this._socketDisconnected(...args));
this._socket.on(
'masterVerification', (...args) => this._masterVerification(...args));
this._socket.on('evalRequest', (...args) => this._evalRequest(...args));
this._socket.on('update', (...args) => this._updateRequest(...args));
this._socket.on('respawn', (...args) => this._respawnChild(...args));
this._socket.on('writeFile', (...args) => this._receiveMasterFile(...args));
this._socket.on('getFile', (...args) => this._sendMasterFile(...args));
this._socket.on(
'connect_error', (...args) => this._socketConnectError(...args));
this._socket.on(
'connect_timeout', (...args) => this._socketConnectError(...args));
this._socket.on('error', (...args) => this._socketConnectError(...args));
}
/**
* @description Socket connected fail event handler.
* @private
* @param {...*} [args] Error arguments.
*/
_socketConnectError(...args) {
common.error('Failed to connect to master.', this.id);
console.error(...args);
this._socket.io.opts.extraHeaders.authorization =
this._generateAuthHeader();
// this._socket.connect();
}
/**
* @description Socket connected event handler.
* @private
* @param {number} attempt The reconnection attempt number.
*/
_socketReconnecting(attempt) {
common.log(
`Socket reconnecting to master... (Attempt: #${attempt})`, this.id);
this._socket.io.opts.extraHeaders.authorization =
this._generateAuthHeader();
}
/**
* @description Socket connected event handler.
* @private
*/
_socketConnected() {
this._lastSeen = Date.now();
common.log('Socket connected to master', this.id);
clearTimeout(this._reconnectTimeout);
this._reconnectTimeout = null;
}
/**
* @description Socket disconnected event handler.
* @private
* @param {string} reason Either ‘io server disconnect’, ‘io client
* disconnect’, or ‘ping timeout’.
*/
_socketDisconnected(reason) {
common.log(`Socket disconnected from master (${reason})`, this.id);
this._socket.io.opts.extraHeaders.authorization =
this._generateAuthHeader();
// console.log(reason, typeof reason, this._verified,
// this._reconnectTimeout);
if (this._verified && !this._reconnectTimeout &&
reason === 'io server disconnect') {
this._reconnectTimeout = setTimeout(() => {
common.log('Attempting reconnect after io disconnect.', this.id);
this._socket.io.opts.extraHeaders.authorization =
this._generateAuthHeader();
if (!this._socket.connected) {
this._socket.disconnect();
this._socket.connect();
}
this._socket.reconnection && this._socket.reconnection(true);
}, 5000);
}
}
/**
* @description Verify that we are connecting to the master we expect.
* @private
* @param {string} sig The signature.
* @param {string} data The message sent that was signed.
*/
_masterVerification(sig, data) {
this._lastSeen = Date.now();
const verify = crypto.createVerify(this._config.signAlgorithm);
verify.update(data);
verify.end();
if (!verify.verify(this._config.masterPubKey, sig, 'base64')) {
common.logWarning('Failed to verify signature from Master!', this.id);
} else {
this._verified = true;
common.log('Verified signature from master successfully.', this.id);
}
}
/**
* @description Master has requested shard evaluates a script.
* @private
* @param {string} script Script to evaluate on the shard.
* @param {Function} cb Callback function with optional error, otherwise
* success message is second parameter.
*/
_evalRequest(script, cb) {
if (!this._child) {
cb('Not Running');
return;
}
if (this._evals.has(script)) {
this._evals.get(script)
.then((res) => {
cb(null, res);
return res;
})
.catch((err) => cb(err));
return;
}
const promise = new Promise((resolve, reject) => {
const listener = (message) => {
if (!message || message._eval !== script) return;
this._child.removeListener('message', listener);
this._evals.delete(script);
if (!message._error) {
resolve(message._result);
} else {
reject(message._error);
}
};
this._child.on('message', listener);
this._child.send({_eval: script}, (err) => {
if (!err) return;
this._child.removeListener('message', listener);
this._evals.delete(script);
reject(err);
});
});
this._evals.set(script, promise);
promise
.then((res) => {
cb(null, res);
return res;
})
.catch((err) => cb(err));
}
/**
* @description Trigger the child process to be killed and restarted.
* @private
* @param {number} [delay] Time to wait before actually respawning in
* milliseconds.
*/
_respawnChild(delay) {
if (!this._respawnTimeout && delay) {
this._respawnTimeout = setTimeout(() => this._respawnChild(), delay);
return;
}
clearTimeout(this._respawnTimeout);
if (this._child && this._status.stopTime > this._status.startTime &&
Date.now() - this._status.stopTime > 30000) {
common.logWarning('Child failed to shutdown! Forcefully killing...');
this._child.kill('SIGKILL');
} else if (this._child) {
this._child.kill('SIGTERM');
this._status.stopTime = Date.now();
} else if (this._status.goalShardId >= 0) {
this._spawnChild();
}
}
/**
* @description Master has sent a status update, and potentially expects a
* response.
* @private
* @param {string} settings Current settings for operation as JSON parsable
* string.
*/
_updateRequest(settings) {
this._lastSeen = Date.now();
const newStr = JSON.stringify(settings);
const oldStr = JSON.stringify(this._settings);
if (newStr != oldStr) {
common.logDebug(
'New settings received from master: ' + JSON.stringify(settings),
this.id);
}
if (!settings || typeof settings !== 'object') return;
this._settings = settings;
const s = this._status;
s.goalShardId = settings.id;
s.goalShardCount = settings.count;
s.isMaster = settings.master || false;
if (s.currentShardId != s.goalShardId ||
s.currentShardCount != s.goalShardCount) {
if (s.goalShardId < -1) {
this.exit();
return;
} else if (s.currentShardId >= 0) {
this._respawnChild();
} else {
this._spawnChild();
}
}
if (this._settings.config.heartbeat.updateStyle === 'pull') {
this._generateHeartbeat();
}
this._hbTimeoutHandler();
// TODO: Implement 'push' update style event loop.
}
/**
* @description Handler for {@link _hbTimeout}.
* @private
*/
_hbTimeoutHandler() {
clearTimeout(this._hbTimeout);
const style = this._settings.config.heartbeat.updateStyle;
const extend = style === 'pull';
const delay =
this._settings.config.heartbeat.interval * (extend ? 1.5 : 1.0);
this._hbTimeout = setTimeout(() => this._hbTimeoutHandler(), delay);
const deathDelta = this._settings.config.heartbeat.assumeDeadAfter;
const rebootDelta = this._settings.config.heartbeat.requestRebootAfter;
if (style === 'push') {
this._generateHeartbeat();
} else if (
style === 'pull' && Date.now() - this._lastSeen > rebootDelta &&
this._status.goalShardId >= 0 && this._verified) {
this._socket.disconnect();
this._socket.connect();
} else if (
style === 'pull' && Date.now() - this._lastSeen > deathDelta &&
this._status.goalShardId >= 0) {
common.logWarning(
'No message has been received from ShardingMaster for too ' +
'long, rebooting.',
this.id);
this.exit();
}
}
/**
* @description Spawn the child process with the current settings available.
* @private
*/
_spawnChild() {
if (this._status.goalShardId < 0) return;
if (this._child) return;
common.log('Spawning child shard #' + this._status.goalShardId, this.id);
this._status.reset();
const botFullName =
this._settings.master ? 'master' : this._settings.config.botName;
const botName =
['release', 'dev'].includes(botFullName) ? null : botFullName;
const env = Object.assign({}, process.env, {
SHARDING_MANAGER: true,
SHARDING_MANAGER_MODE: 'process',
SHARDING_SLAVE: !this._settings.master,
SHARDING_MASTER: this._settings.master,
SHARDING_NAME: this.id,
SHARDS: this._status.goalShardId,
SHARD_COUNT: this._status.goalShardCount,
DISCORD_TOKEN: auth[botFullName],
});
this._status.currentShardId = this._status.goalShardId;
this._status.currentShardCount = this._status.goalShardCount;
if (!this._settings.config.botArgs) this._settings.config.botArgs = [];
if (botName) {
const index = this._settings.config.botArgs.findIndex(
(el) => el.match(/--botname=(\w+)$/));
if (index >= 0) {
this._settings.config.botArgs[index] = `--botname=${botName}`;
} else {
this._settings.config.botArgs.push(`--botname=${botName}`);
}
}
if (this._settings.master &&
!this._settings.config.botArgs.includes('--nologin')) {
this._settings.config.botArgs.push('--nologin');
}
this._child = fork('src/SpikeyBot.js', this._settings.config.botArgs, {
execArgv: this._settings.config.nodeArgs || [],
env: env,
cwd: botCWD,
detached: false,
});
this._child.on('error', (...args) => this._handleError(...args));
this._child.on('exit', (...args) => this._handleExit(...args));
this._child.on('message', (...args) => this._childMessage(...args));
this._status.startTime = Date.now();
}
/**
* @description We received a file from the sharding master that it intends
* for us to write to disk at the given filename relative to the project root.
*
* @private
* @param {string|object.<string>} req Filename relative to project directory,
* or object with filename and modified time.
* @param {?string|Buffer} data The data to write to the file, or null to
* delete the file, or modified time is older on master.
* @param {Function} cb Callback once completed with optional error.
*/
_receiveMasterFile(req, data, cb) {
if (typeof cb !== 'function') cb = () => {};
this._lastSeen = Date.now();
const filename = req.filename || req;
const file = path.resolve(`${botCWD}/${filename}`);
if (typeof filename != 'string' || !file.startsWith(botCWD)) {
common.logWarning(
'Master sent file outside of project directory: ' + file);
cb('File path unacceptable');
return;
}
if (req.mtime && !data) {
// common.logDebug(
// `Skipping write for file from master: ${file} (${req.mtime})`);
cb(null);
} else if (!data) {
common.unlink(file, (err) => {
if (err) {
common.error(`Failed to unlink file from master from disk: ${file}`);
console.error(err);
cb('Failed to unlink');
} else {
common.logDebug(`Unlinked file from master from disk: ${file}`);
cb(null);
}
});
} else {
common.mkAndWrite(file, null, data, (err) => {
if (err) {
common.error(`Failed to write file from master to disk: ${file}`);
console.error(err);
cb('Failed to write');
} else {
common.logDebug(`Wrote file from master to disk: ${file}`);
cb(null);
}
});
}
}
/**
* @description Send the specified file to the ShardingMaster.
*
* @private
* @param {string} filename Filename relative to project directory.
* @param {Function} cb Callback with optional error argument.
*/
_sendMasterFile(filename, cb) {
if (typeof cb !== 'function') cb = () => {};
this._lastSeen = Date.now();
const file = path.resolve(`${botCWD}/${filename}`);
if (typeof filename != 'string' || !file.startsWith(botCWD)) {
common.error(
`Attempted to send file outside of project directory: ${file}`);
cb('File path unacceptable');
return;
}
// Send original filename, as ShardingMaster expects the same format.
fs.readFile(filename, (err, data) => {
if (err) {
if (err.code === 'ENOENT') {
this._socket.emit('writeFile', filename, null, (err) => {
if (err) {
common.error(`Failed to unlink file on master: ${file}`);
console.error(err);
cb('Failed to unlink');
} else {
cb(null);
}
});
} else {
common.error(`Failed to read file for master: ${file}`);
console.error(err);
cb('Failed to read');
}
} else {
this._socket.emit('writeFile', filename, data, (err) => {
if (err) {
common.error(`Failed to write file on master: ${file}`);
console.error(err);
cb('Failed to write');
} else {
cb(null);
}
});
}
});
}
/**
* @description Handle an error during spawning of child.
* @private
* @param {Error} err Error emitted by EventEmitter.
*/
_handleError(err) {
this._child = null;
common.error('Failed to fork child process!', this.id);
console.error(err);
this._status.goalShardId = -1;
this._status.goalShardCount = -1;
this._status.currentShardId = -1;
this._status.currentShardCount = -1;
}
/**
* @description Handle the child processes exiting.
* @private
* @param {number} code Process exit code.
* @param {string} signal Process kill signal.
*/
_handleExit(code, signal) {
common.log('Child exited with code ' + code + ' (' + signal + ')', this.id);
this._child = null;
this._status.currentShardId = -1;
this._status.currentShardCount = -1;
this._evals.clear();
if (this._status.goalShardId >= 0) this._spawnChild();
}
/**
* @description Handle a message from the child.
* @private
* @param {object} message A parsed JSON object or primitive value.
*/
_childMessage(message) {
if (message) {
if (message._ready) {
// Shard became ready.
return;
} else if (message._disconnect) {
// Shard disconnected.
return;
} else if (message._reconnecting) {
// Shard attempting to reconnect.
return;
} else if (message._sFetchProp) {
// Shard is requesting a property fetch. I don't use this so I haven't
// bothered to implement it.
return;
} else if (message._sEval) {
this.broadcastEval(message._sEval, (err, res) => {
if (!this._child) return;
if (err) {
this._child.send({_sEval: message._sEval, _error: err});
} else {
this._child.send({_sEval: message._sEval, _result: res});
}
});
return;
} else if (message._sRespawnAll) {
this.respawnAll(() => {});
return;
} else if (message._sSQL) {
// Shard has requested to send a query to our primary database.
this.sendSQL(message._sSQL, (err, res) => {
if (!this._child) return;
if (err) {
this._child.send({_sSQL: message._sSQL, _error: err});
} else {
this._child.send({_sSQL: message._sSQL, _result: res});
}
});
return;
} else if (message._sWriteFile) {
// Shard has requested to send a file to our primary node.
this._sendMasterFile(message._sWriteFile, (err, res) => {
if (!this._child) return;
if (err) {
this._child.send({_sWriteFile: message._sWriteFile, _error: err});
} else {
this._child.send({_sWriteFile: message._sWriteFile, _result: res});
}
});
return;
} else if (message._sGetFile) {
// Shard has requested to get a file from our primary node.
const req = {filename: message._sGetFile, mtime: message._sGetFileM};
this._socket.emit('getFile', req, (err, res) => {
if (!this._child) return;
if (err) {
this._child.send({_sGetFile: message._sGetFile, _error: err});
} else {
this._child.send({_sGetFile: message._sGetFile, _result: res});
}
});
return;
} else if (typeof message === 'string' && message.startsWith('reboot')) {
common.log(`Reboot requested: ${JSON.stringify(message)}`);
if (!this._socket.connected) {
common.logWarning(
'Requested reboot broadcast while disconnected from master!',
this.id);
} else {
this._socket.emit('reboot', message);
}
return;
}
}
// common.logDebug(`Shard Message: ${JSON.stringify(message)}`, this.id);
}
/**
* @description Fire a broadcast to all shards requesting eval of given
* script.
* @see {@link ShardingMaster~broadcastEvalToShards}
* @public
* @param {string} script The script to evaluate.
* @param {Function} cb Callback once all shards have completed or there was
* an error. First argument is optional error, second will otherwise be array
* of responses indexed by shard IDs.
*/
broadcastEval(script, cb) {
if (!this._socket.connected) {
common.logWarning(
'Requested eval broadcast while disconnected from master!', this.id);
cb('Disconnected from master!');
// TODO: Resend this request once reconnected instead of failing.
} else {
this._socket.emit('broadcastEval', script, cb);
}
}
/**
* @description Send an SQL query to the master to run on our database.
* @see {@link ShardingMaster~sendSQL}
* @public
* @param {string} query The query to evaluate.
* @param {Function} cb First argument is optional error, second will
* otherwise be response from query.
*/
sendSQL(query, cb) {
if (!this._socket.connected) {
common.logWarning(
'Requested SQL broadcast while disconnected from master!', this.id);
cb('Disconnected from master!');
// TODO: Resend this request once reconnected instead of failing.
} else {
this._socket.emit('sendSQL', query, cb);
}
}
/**
* @description Kills all running shards and respawns them.
* @see {@link ShardingMaster.respawnAll}
* @param {Function} [cb] Callback once all shards have been rebooted or an
* error has occurred.
*/
respawnAll(cb) {
if (!this._socket.connected) {
common.logWarning(
'Requested Respawn All while disconnected from master!', this.id);
cb('Disconnected from master!');
// TODO: Resend this request once reconnected instead of failing.
} else {
this._socket.emit('respawnAll', cb);
}
}
/**
* @description Fetch stats necessary for heartbeat message to the master,
* then sends the message.
* @private
*/
_generateHeartbeat() {
if (!this._socket.connected) {
common.logWarning(
'Heartbeat generation requested, but socket is not connected!',
this.id);
return;
}
if (Date.now() - this._status.startTime < 100) {
this._hbEvalResHandler(null, null);
return;
}
const hbEvalReq = 'this.getStats(true)';
// common.logDebug('Attempting to fetch stats for heartbeat...');
const timeout =
setTimeout(() => this._hbEvalResHandler('Stats IPC timeout'), 10000);
this._evalRequest(hbEvalReq, (...args) => {
clearTimeout(timeout);
this._hbEvalResHandler(...args);
});
}
/**
* @description Handler for response to status fetching for a heartbeat
* request.
* @private
* @param {?Error|string} err Optional error message.
* @param {*} res Response from eval.
*/
_hbEvalResHandler(err, res) {
const now = Date.now();
const s = this._status;
if (err || (!res && Date.now() - this._status.startTime < 100)) {
common.error('Failed to fetch stats for heartbeat!', this.id);
if (err) console.error(err);
// this._socket.emit('status', s);
// common.logDebug(`Status Message: ${JSON.stringify(s)}`);
// return;
}
this._fetchDiskStats((err, stats) => {
const delta = (s.timestamp > s.startTime) ? now - s.timestamp : 0;
s.timestamp = now;
s.timeDelta = delta;
s.memHeapUsed = res && res.memory.heapUsed;
s.memHeapTotal = res && res.memory.heapTotal;
s.memRSS = res && res.memory.rss;
s.memExternal = res && res.memory.external;
if (!res) {
s.cpuLoad.forEach((_, i) => s.cpuLoad[i] = null);
} else if (s.cpuLoad.length !== res.cpus.length) {
s.cpuLoad = new Array(res.cpus.length);
}
if (res) {
res.cpus.forEach((el, i) => {
const t = el.times;
let total = 0;
let prevTotal = 0;
for (const c in t) {
if (!c) continue;
total += t[c];
prevTotal += (s.cpus[i] || el).times[c];
}
const totalDiff = total - prevTotal;
s.cpuLoad[i] = t.user / totalDiff;
});
s.cpus = res.cpus;
} else {
s.cpus.forEach((_, i) => s.cpus[i] = null);
}
const prevDelta = s.messageCountDelta || 0;
if (res) {
s.messageCountDelta =
(res.numMessages || 0) - (s.messageCountTotal || 0);
} else {
s.messageCountDelta = 0;
}
s.messageCountTotal = res && res.numMessages || 0;
s.storageUsedTotal = stats.root;
s.storageUsedUsers = stats.save;
this._socket.emit('status', s);
// common.logDebug(`Status Message: ${JSON.stringify(s)}`);
if (this._settings.config.heartbeat.useMessageStats &&
now - s.startTime > this._settings.config.heartbeat.interval) {
// common.logDebug(
// `Message delta: ${s.messageCountDelta}, Prev: ${prevDelta}`);
if (prevDelta === 0 && s.messageCountDelta === 0 && !s.isMaster) {
common.error('No messages received for last two heartbeats!');
this._respawnChild();
}
/* } else {
common.logDebug('Heartbeat Sent'); */
}
});
}
/**
* @description Fetch disk storage information about the bot. If a value was
* unable to be fetched, it will return a `null` value instead of a string.
* @private
* @param {Function} cb Callback with first argument as optional error,
* otherwise the second is an object containing stats about different
* directories.
*/
_fetchDiskStats(cb) {
// cb(null, {});
// return;
// // Resolve the absolute path to the project root.
const root = path.resolve(`${__dirname}/../..`);
// // Paths relative to project root.
// const dirs = [
// ['save', './save/'],
// // ['docs', './docs/'],
// // ['img', './img/'],
// // ['sounds', './sounds/'],
// // ['node_modules', './node_modules/'],
// ['root', './'],
// ];
const opts = {
env: null,
timeout: 5000,
cwd: root,
};
const regex =
's/^\\S+\\s+([0-9]+\\w)\\s+([0-9]+\\w)\\s+([0-9]+\\w)\\s+([0-9]+%).*' +
'/\\2\\/\\1 \\4/p';
exec(`df -h | grep G | sed -rn '${regex}'`, opts, (err, stdout) => {
if (err) {
common.logWarning('Failed to fetch save directory size.', this.id);
console.error(err);
cb(err);
} else {
cb(null, stdout.trim());
}
});
// let numDone = 0;
// const out = {};
// /**
// * @description Fired at completion of obtaining directory information
// for
// * each in the `dir` array. Fires callback once all are complete.
// * @private
// */
// function done() {
// if (++numDone === dirs.length) return;
// cb(null, out);
// }
// dirs.forEach((el) => {
// const name = el[0];
// const dir = el[1];
// exec(`du -sh ${dir}`, opts, (err, stdout) => {
// if (err) {
// common.logWarning('Failed to fetch save directory size.', this.id);
// console.error(err);
// out[name] = null;
// } else {
// out[name] = stdout.toString().trim().split('\t')[0];
// }
// done();
// });
// });
}
/**
* @description Generate the string to pass as the `authorization` header
* during the connection request to the master.
* @private
* @returns {string} The string to pass directly to the auth header.
*/
_generateAuthHeader() {
const now = Date.now();
const sign = crypto.createSign(this._config.signAlgorithm);
const signData = `${this.id}${now}`;
sign.update(signData);
sign.end();
const signature = sign.sign(this._privKey, 'base64');
return `${this.id},${signature},${now}`;
}
/**
* @description Cleanup and fully shutdown gracefully.
* @public
*/
exit() {
if (this._socket) this._socket.close();
if (this._status.goalShardId >= 0) {
this._status.goalShardId = -2;
this._status.goalShardCount = -2;
}
if (this._child) this._child.kill('SIGTERM');
process.exit(0);
}
}
if (require.main === module) {
console.log('Started via CLI, booting up...');
const slave = new ShardingSlave();
process.on('SIGINT', (...args) => slave.exit(...args));
process.on('SIGTERM', (...args) => slave.exit(...args));
}
module.exports = ShardingSlave;