From 7bd6a1b6d43894cc4cb01248870d209b93c1e5fd Mon Sep 17 00:00:00 2001 From: Shukri Adams Date: Sun, 21 Jan 2018 16:52:54 +0100 Subject: [PATCH] added automatic timeout to bot. removed ssh key copy from vagrant script. --- README.md | 28 +++++++++++++++++++--------- lib/bot.js | 27 ++++++++++++++++++++++++++- lib/daemon.js | 16 +++++++++++++++- lib/settings.js | 4 ++++ lib/store.js | 12 ++++++++++++ package.json | 2 +- vagrant/Vagrantfile | 1 - 7 files changed, 77 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 7309e8c..3a66aa0 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # discord-giveawaybot -A demo version of the bot lives on Discord: https://discord.gg/gMEGQBj - [![Build Status](https://travis-ci.org/shukriadams/discord-giveawaybot.svg?branch=master)](https://travis-ci.org/shukriadams/discord-giveawaybot) Gives away Steam games on Discord. Heavily inspired by https://github.com/jagrosh/GiveawayBot +A demo version can be seen on Discord: https://discord.gg/gMEGQBj + ## Requirements - NodeJS 7 or greater. @@ -20,9 +20,11 @@ Gives away Steam games on Discord. Heavily inspired by https://github.com/jagros ## Host your bot -There are three ways to host your bot. In all cases, first create a sub folder called "discord-giveawaybot" in the root folder you're working from, to this sub folder copy exampleSettings.json, rename to settings.json, and add your bot token from Discord. +There are three ways to host your bot. In all cases, your should create a folder (this will be your bot's root folder). +In this create a sub folder called "discord-giveawaybot", in this sub folder copy exampleSettings.json, rename it +settings.json, open it with your favorite text editor, and add your Discord bot token (see above) to the "token" field. -### From Docker image +### 1) From Docker image Image on Docker hub : https://hub.docker.com/r/shukriadams/discord-giveawaybot/ @@ -44,20 +46,20 @@ Image on Docker hub : https://hub.docker.com/r/shukriadams/discord-giveawaybot/ These settings can of course be tweaked to suite your host setup, only npm start and the volume map are required. -### From NPM +### 2) From NPM - Install npm install discord-giveawaybot --save -- Run +- Run let Bot = require('discord-giveawaybot'), bot = new Bot(); bot.start(); -### From source +### 3) From source - Clone this repo. - Run @@ -65,7 +67,6 @@ These settings can of course be tweaked to suite your host setup, only npm start npm install npm start - ## Add your bot to your Discord server - back on your app's Discord config page (from the first section above), copy your bot's client id and paste it into @@ -79,6 +80,16 @@ should see your bot as a user on your server. "@BOTNAME channel" where BOTNAME is whatever name you gave your bot. - That's it, you're set to go. +## Known issues + +The bot has a memory leak, and will hang periodically. I don't have a fix for this, a workaround is to enable self-timeout +in settings.json + + "processLifetime" : 480 + +This value is in minutes, and after it has elapsed, the bot will cleanly exit and flush used memory. If you're hosting +with docker or pm2, they should automatically bring the bot back up, and you're good to run again. + ## Additional config By default, only admins can create and manage giveaways. If you want to delegate giveaway responsibilities to non-admins @@ -207,7 +218,6 @@ If you want to run it directly on your machine, install Node 7 or higher. Then r npm install node index - ## Tests npm test diff --git a/lib/bot.js b/lib/bot.js index fa4acb0..b38f4c7 100644 --- a/lib/bot.js +++ b/lib/bot.js @@ -9,6 +9,7 @@ let codes = require('./codes'), Trace = require('./trace'), hi = require('./highlight'), process = require('process'), + Store = require('./store'), path = require('path'), fs = require('fs'), State = require('./state'), @@ -19,6 +20,8 @@ class Bot{ constructor(options){ options = options || {}; this.cronEnabled = true; + this.busyProcessingMessage = false; + this.willShutdown = false; if(options.cronEnabled === false) this.cronEnabled = false; @@ -68,6 +71,12 @@ class Bot{ try { this.daemon = Daemon.instance(); + this.daemon.onProcessExpired = async function(){ + this.willShutdown = true; + if (!this.busyProcessingMessage) + await this.shutdown(); + }.bind(this); + if (this.cronEnabled) await this.daemon.start(); } catch (ex){ @@ -105,7 +114,7 @@ class Bot{ * Called after bot connects to discord and is ready to receive messages. */ _onReady(){ - console.log('Giveawaybot is ready.'); + console.log('discord-giveawaybot is now running.'); // todo : validate settings @@ -113,6 +122,16 @@ class Bot{ // permissions etc } + async shutdown(){ + + // close Loki down gracefully + let store = await Store.instance(); + await store.close(); + + console.log(`Bot lifetime (${this.settings.values.processLifetime}m) expired. Exiting now, this is not an error.`); + process.exitCode = 0; + process.exit(); + } /** * Entry point for all incoming user messages. This is where all user-driven interaction (as opposed to daemon) @@ -122,6 +141,8 @@ class Bot{ return new Promise(async function(resolve, reject){ try { + this.busyProcessingMessage = true; + // reject messages @bot (these are public messages), unless message is to set "channel" if (message.content.indexOf(`<@${message.client.user.id}>`) === 0 && message.content.toLowerCase().trim() !== `<@${message.client.user.id}> channel`) { await message.reply('Please message me directly.'); @@ -182,6 +203,10 @@ class Bot{ } catch (ex) { this._handleUnexpectedError(ex, message); return reject(ex); + } finally { + this.busyProcessingMessage = false; + if (this.willShutdown) + await this.shutdown(); } }.bind(this)); } diff --git a/lib/daemon.js b/lib/daemon.js index fa173d1..0bdecc2 100644 --- a/lib/daemon.js +++ b/lib/daemon.js @@ -21,6 +21,11 @@ let winston = require('winston'), class Daemon { + constructor(){ + this.started = new Date(); + this.willShutdown = false; + } + /** * Starts the timer loop that calls .tick() */ @@ -35,7 +40,7 @@ class Daemon { try { // use busy flag to prevent the daemon from running over itself - if (busy) + if (busy || this.willShutdown) return; busy = true; @@ -259,6 +264,15 @@ class Daemon { // clean old giveaways store.clean(); + // check if lifetime has passed + if (settings.values.processLifetime && this.onProcessExpired){ + let shouldExpire = timeHelper.minutesSince(this.started) >= settings.values.processLifetime; + if (shouldExpire){ + this.willShutdown = true; + this.onProcessExpired(); + } + } + return codes.DAEMON_FINISHED; } diff --git a/lib/settings.js b/lib/settings.js index c0bd4bc..7132eab 100644 --- a/lib/settings.js +++ b/lib/settings.js @@ -38,6 +38,10 @@ class Settings { if (this.values.joinGiveawayResponseCharacter === undefined) this.values.joinGiveawayResponseCharacter = '🎉'; + + // int, and in minutes + if (this.values.processLifetime && !Number.isInteger(this.values.processLifetime)) + throw new Error ('settings.json processLifetime value must an integer'); } save(){ diff --git a/lib/store.js b/lib/store.js index 7f695d4..99b2589 100644 --- a/lib/store.js +++ b/lib/store.js @@ -48,6 +48,18 @@ class Store { } } + async close(){ + return new Promise(async function(resolve, reject) { + try { + this.database.close(function(){ + resolve(); + }); + } catch (ex){ + reject(ex); + } + }.bind(this)); + } + static _convert (record){ return { id : record.$loki.toString(), diff --git a/package.json b/package.json index 034c64e..7221ae2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "discord-giveawaybot", - "version": "0.1.2", + "version": "0.1.3", "description": "A Discord bot that gives games away.", "private": false, "author": "shukri.adams@gmail.com", diff --git a/vagrant/Vagrantfile b/vagrant/Vagrantfile index 9743384..64c02b2 100644 --- a/vagrant/Vagrantfile +++ b/vagrant/Vagrantfile @@ -13,6 +13,5 @@ Vagrant.configure("2") do |config| v.customize ["modifyvm", :id, "--memory", 1048] end - config.vm.provision "file", source: "~/.ssh/id_rsa", destination: "~/.ssh/id_rsa" config.vm.provision :shell, path: "provision.sh" end