[V3 RPC] Add basic RPC functionality (#1017)

* Add basic RPC functionality

* Add load/unload/reload rpc commands

* Reorganize without rpc_ready event

* Remove rpc ready event

* Removed bot reference from rpc module

* Close RPC server cleanly

* refactor bot

* Refactor a bit and make RPC server initialization based on a cli flag

* Fix version resolver

* standardize version getters

* Pick a new port number
This commit is contained in:
Will 2017-10-27 20:55:41 -04:00 committed by GitHub
parent 8d8e1c61d8
commit f459a21bef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 175 additions and 8 deletions

12
docs/framework_events.rst Normal file
View File

@ -0,0 +1,12 @@
.. framework events list
=============
Custom Events
=============
RPC Server
^^^^^^^^^^
.. py:method:: Red.on_shutdown()
Dispatched when the bot begins it's shutdown procedures.

View File

@ -21,6 +21,7 @@ from redbot.core.sentry_setup import init_sentry_logging
from redbot.core.cli import interactive_config, confirm, parse_cli_flags, ask_sentry
from redbot.core.core_commands import Core
from redbot.core.dev_commands import Dev
from redbot.core import rpc
import asyncio
import logging.handlers
import logging
@ -96,7 +97,7 @@ def main():
red = Red(cli_flags, description=description, pm_help=None)
init_global_checks(red)
init_events(red, cli_flags)
red.add_cog(Core())
red.add_cog(Core(red))
red.add_cog(CogManagerUI())
if cli_flags.dev:
red.add_cog(Dev())
@ -141,6 +142,7 @@ def main():
sentry_log.critical("Fatal Exception", exc_info=e)
loop.run_until_complete(red.logout())
finally:
rpc.clean_up()
if cleanup_tasks:
pending = asyncio.Task.all_tasks(loop=red.loop)
gathered = asyncio.gather(*pending, loop=red.loop)

View File

@ -6,6 +6,6 @@ from .context import RedContext
__all__ = ["Config", "RedContext", "__version__"]
try:
__version__ = pkg_resources.require("Red-DiscordBot")[0].version
__version__ = pkg_resources.get_distribution("Red-DiscordBot").version
except pkg_resources.DistributionNotFound:
__version__ = "3.0.0"

View File

@ -10,10 +10,29 @@ from discord.ext.commands.bot import BotBase
from discord.ext.commands import GroupMixin
from .cog_manager import CogManager
from . import Config, i18n, RedContext
from . import (
Config,
i18n,
RedContext,
rpc
)
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from aiohttp_json_rpc import JsonRpc
class RedBase(BotBase):
# noinspection PyUnresolvedReferences
class RpcMethodMixin:
async def rpc__cogs(self, request):
return list(self.cogs.keys())
async def rpc__extensions(self, request):
return list(self.extensions.keys())
class RedBase(BotBase, RpcMethodMixin):
"""Mixin for the main bot class.
This exists because `Red` inherits from `discord.AutoShardedClient`, which
@ -26,6 +45,7 @@ class RedBase(BotBase):
self._shutdown_mode = ExitCodes.CRITICAL
self.db = Config.get_core_conf(force_registration=True)
self._co_owners = cli_flags.co_owner
self.rpc_enabled = cli_flags.rpc
self.db.register_global(
token=None,
@ -73,6 +93,8 @@ class RedBase(BotBase):
self.cog_mgr = CogManager(paths=(str(self.main_dir / 'cogs'),))
self.register_rpc_methods()
super().__init__(**kwargs)
async def _dict_abuse(self, indict):
@ -194,6 +216,10 @@ class RedBase(BotBase):
del self.extensions[name]
# del sys.modules[name]
def register_rpc_methods(self):
rpc.add_method('bot', self.rpc__cogs)
rpc.add_method('bot', self.rpc__extensions)
class Red(RedBase, discord.AutoShardedClient):
"""

View File

@ -102,6 +102,10 @@ def parse_cli_flags(args):
parser.add_argument("--dev",
action="store_true",
help="Enables developer mode")
parser.add_argument("--rpc",
action="store_true",
help="Enables the built-in RPC server. Please read the docs"
"prior to enabling this!")
parser.add_argument("instance_name",
help="Name of the bot instance created during `redbot-setup`.")

View File

@ -13,6 +13,12 @@ from discord.ext import commands
from redbot.core import checks
from redbot.core import i18n
from redbot.core import rpc
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from redbot.core.bot import Red
__all__ = ["Core"]
@ -29,6 +35,12 @@ _ = i18n.CogI18n("Core", __file__)
class Core:
"""Commands related to core functions"""
def __init__(self, bot):
self.bot = bot # type: Red
rpc.add_method('core', self.rpc_load)
rpc.add_method('core', self.rpc_unload)
rpc.add_method('core', self.rpc_reload)
@commands.command()
@checks.is_owner()
@ -410,3 +422,25 @@ class Core:
"to %s") % destination)
else:
await ctx.send(_("Message delivered to %s") % destination)
# RPC handlers
async def rpc_load(self, request):
cog_name = request.params[0]
spec = await self.bot.cog_mgr.find_cog(cog_name)
if spec is None:
raise LookupError("No such cog found.")
self.cleanup_and_refresh_modules(spec.name)
self.bot.load_extension(spec)
async def rpc_unload(self, request):
cog_name = request.params[0]
self.bot.unload_extension(cog_name)
async def rpc_reload(self, request):
await self.rpc_unload(request)
await self.rpc_load(request)

View File

@ -9,9 +9,11 @@ import discord
from .sentry_setup import should_log
from discord.ext import commands
from . import __version__
from .data_manager import storage_type
from .utils.chat_formatting import inline, bordered
from colorama import Fore, Style
from .rpc import initialize
log = logging.getLogger("red")
sentry_log = logging.getLogger("red.sentry")
@ -70,12 +72,13 @@ def init_events(bot, cli_flags):
prefixes = await bot.db.prefix()
lang = await bot.db.locale()
red_pkg = pkg_resources.get_distribution('Red_DiscordBot')
dpy_version = pkg_resources.get_distribution('discord.py').version
red_version = __version__
red_pkg = pkg_resources.get_distribution("Red-DiscordBot")
dpy_version = discord.__version__
INFO = [str(bot.user), "Prefixes: {}".format(', '.join(prefixes)),
'Language: {}'.format(lang),
"Red Bot Version: {}".format(red_pkg.version),
"Red Bot Version: {}".format(red_version),
"Discord.py Version: {}".format(dpy_version),
"Shards: {}".format(bot.shard_count)]
@ -125,6 +128,9 @@ def init_events(bot, cli_flags):
if invite_url:
print("\nInvite URL: {}\n".format(invite_url))
if bot.rpc_enabled:
await initialize(bot)
@bot.event
async def on_command_error(ctx, error):
if isinstance(error, commands.MissingRequiredArgument):

82
redbot/core/rpc.py Normal file
View File

@ -0,0 +1,82 @@
from typing import NewType, TYPE_CHECKING
import asyncio
from aiohttp.web import Application
from aiohttp_json_rpc import JsonRpc
import logging
if TYPE_CHECKING:
from .bot import Red
log = logging.getLogger('red.rpc')
JsonSerializable = NewType('JsonSerializable', dict)
_rpc = JsonRpc(logger=log)
_rpc_server = None # type: asyncio.AbstractServer
async def initialize(bot: "Red"):
global _rpc_server
app = Application(loop=bot.loop)
app.router.add_route('*', '/rpc', _rpc)
handler = app.make_handler()
_rpc_server = await bot.loop.create_server(handler, '127.0.0.1', 6133)
log.debug('Created RPC _rpc_server listener.')
def add_topic(topic_name: str):
"""
Adds a topic for clients to listen to.
:param topic_name:
"""
_rpc.add_topics(topic_name)
def notify(topic_name: str, data: JsonSerializable):
"""
Publishes a notification for the given topic name to all listening clients.
data MUST be json serializable.
note::
This method will fail silently.
:param topic_name:
:param data:
"""
_rpc.notify(topic_name, data)
def add_method(prefix, method):
"""
Makes a method available to RPC clients. The name given to clients will be as
follows::
"{}__{}".format(prefix, method.__name__)
note::
This method will fail silently.
:param prefix:
:param method:
MUST BE A COROUTINE OR OBJECT.
:return:
"""
_rpc.add_methods(
('', method),
prefix=prefix
)
def clean_up():
_rpc_server.close()

View File

@ -1,4 +1,5 @@
appdirs
youtube_dl
raven
colorama
colorama
aiohttp-json-rpc