[V3 RPC] Initial RPC library switch (#1634)

* Initial RPC library switch

* Use weak refs to the methods so cog unload works

* Add docs

* Black fixes

* Add jsonrpcserver to Pipfile.lock
This commit is contained in:
Will 2018-05-22 20:29:26 -04:00 committed by palmtree5
parent abfee70eb3
commit 73a427f6aa
8 changed files with 179 additions and 99 deletions

34
Pipfile.lock generated
View File

@ -32,13 +32,6 @@
],
"version": "==2.2.5"
},
"aiohttp-json-rpc": {
"hashes": [
"sha256:9ec69ea70ce49c4af445f0ac56ac728708ccfad8b214272d2cc7e75bc0b31327",
"sha256:e2b8b49779d5d9b811f3a94e98092b1fa14af6d9adbf71c3afa6b20c641fa5d5"
],
"version": "==0.8.7"
},
"appdirs": {
"hashes": [
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
@ -83,6 +76,13 @@
"editable": true,
"path": "."
},
"funcsigs": {
"hashes": [
"sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca",
"sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50"
],
"version": "==1.0.2"
},
"idna": {
"hashes": [
"sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f",
@ -90,6 +90,19 @@
],
"version": "==2.6"
},
"jsonrpcserver": {
"hashes": [
"sha256:ab8013cdee3f65d59c5d3f84c75be76a3492caa0b33ecaa3f0f69906cf3d9e92"
],
"version": "==3.5.4"
},
"jsonschema": {
"hashes": [
"sha256:000e68abd33c972a5248544925a0cae7d1125f9bf6c58280d37546b946769a08",
"sha256:6ff5f3180870836cae40f06fa10419f557208175f13ad7bc26caa77beb1f6e02"
],
"version": "==2.6.0"
},
"multidict": {
"hashes": [
"sha256:1a1d76374a1e7fe93acef96b354a03c1d7f83e7512e225a527d283da0d7ba5e0",
@ -140,6 +153,13 @@
],
"version": "==1.1.1"
},
"six": {
"hashes": [
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
],
"version": "==1.11.0"
},
"websockets": {
"hashes": [
"sha256:09dfec40e9b73e8808c39ecdbc1733e33915a2b26b90c54566afc0af546a9ec3",

View File

@ -4,5 +4,36 @@
RPC
===
.. automodule:: redbot.core.rpc
.. currentmodule:: redbot.core.rpc
V3 comes default with an internal RPC server that may be used to remotely control the bot in various ways.
Cogs must register functions to be exposed to RPC clients.
Each of those functions must only take JSON serializable parameters and must return JSON serializable objects.
To begin, register all methods using individual calls to the :func:`Methods.add` method.
********
Examples
********
Coming soon to a docs page near you!
*************
API Reference
*************
.. py:attribute:: redbot.core.rpc.methods
An instance of the :class:`Methods` class.
All attempts to register new RPC methods **MUST** use this object.
You should never create a new instance of the :class:`Methods` class!
RPC
^^^
.. autoclass:: redbot.core.rpc.RPC
:members:
Methods
^^^^^^^
.. autoclass:: redbot.core.rpc.Methods
:members:

View File

@ -160,7 +160,7 @@ def main():
sentry_log.critical("Fatal Exception", exc_info=e)
loop.run_until_complete(red.logout())
finally:
rpc.clean_up()
red.rpc.close()
if cleanup_tasks:
pending = asyncio.Task.all_tasks(loop=red.loop)
gathered = asyncio.gather(*pending, loop=red.loop, return_exceptions=True)

View File

@ -21,22 +21,9 @@ from .cog_manager import CogManager
from . import Config, i18n, commands, rpc
from .help_formatter import Help, help as help_
from .sentry import SentryManager
from .utils import TYPE_CHECKING
if TYPE_CHECKING:
from aiohttp_json_rpc import JsonRpc
# 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):
class RedBase(BotBase):
"""Mixin for the main bot class.
This exists because `Red` inherits from `discord.AutoShardedClient`, which
@ -104,10 +91,11 @@ class RedBase(BotBase, RpcMethodMixin):
self.cog_mgr = CogManager(paths=(str(self.main_dir / "cogs"),))
self.register_rpc_methods()
super().__init__(formatter=Help(), **kwargs)
if self.rpc_enabled:
self.rpc = rpc.RPC(self)
self.remove_command("help")
self.add_command(help_)
@ -275,10 +263,6 @@ class RedBase(BotBase, RpcMethodMixin):
if pkg_name.startswith("redbot.cogs"):
del sys.modules["redbot.cogs"].__dict__[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

@ -50,10 +50,6 @@ class Core:
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(hidden=True)
async def ping(self, ctx):
"""Pong."""

View File

@ -16,7 +16,6 @@ from discord.ext import commands
from . import __version__
from .data_manager import storage_type
from .utils.chat_formatting import inline, bordered
from .rpc import initialize
from colorama import Fore, Style, init
log = logging.getLogger("red")
@ -173,7 +172,7 @@ def init_events(bot, cli_flags):
print("\nInvite URL: {}\n".format(invite_url))
if bot.rpc_enabled:
await initialize(bot)
await bot.rpc.initialize()
@bot.event
async def on_error(event_method, *args, **kwargs):

View File

@ -1,85 +1,135 @@
import asyncio
import weakref
from aiohttp.web import Application
from aiohttp_json_rpc import JsonRpc
from aiohttp import web
import jsonrpcserver.aio
import inspect
import logging
from .utils import TYPE_CHECKING, NewType
if TYPE_CHECKING:
from .bot import Red
__all__ = ["methods", "RPC", "Methods"]
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):
class Methods(jsonrpcserver.aio.AsyncMethods):
"""
Adds a topic for clients to listen to.
Container class for all registered RPC methods, please use the existing `methods`
attribute rather than creating a new instance of this class.
Parameters
----------
topic_name
.. warning::
**NEVER** create a new instance of this class!
"""
_rpc.add_topics(topic_name)
def __init__(self):
super().__init__()
self._items = weakref.WeakValueDictionary()
def add(self, method, name: str = None):
"""
Registers a method to the internal RPC server making it available for
RPC users to call.
.. important::
Any method added here must take ONLY JSON serializable parameters and
MUST return a JSON serializable object.
Parameters
----------
method : function
A reference to the function to register.
name : str
Name of the function as seen by the RPC clients.
"""
if not inspect.iscoroutinefunction(method):
raise TypeError("Method must be a coroutine.")
if name is None:
name = method.__qualname__
self._items[str(name)] = method
def remove(self, *, name: str = None, method=None):
"""
Unregisters an RPC method. Either a name or reference to the method must
be provided and name will take priority.
Parameters
----------
name : str
method : function
"""
if name and name in self._items:
del self._items[name]
elif method and method in self._items.values():
to_remove = []
for name, val in self._items.items():
if method == val:
to_remove.append(name)
for name in to_remove:
del self._items[name]
def all_methods(self):
"""
Lists all available method names.
Returns
-------
list of str
"""
return self._items.keys()
def notify(topic_name: str, data: JsonSerializable):
methods = Methods()
class BaseRPCMethodMixin:
def __init__(self):
methods.add(self.all_methods, name="all_methods")
async def all_methods(self):
return list(methods.all_methods())
class RPC(BaseRPCMethodMixin):
"""
Publishes a notification for the given topic name to all listening clients.
data MUST be json serializable.
note::
This method will fail silently.
Parameters
----------
topic_name
data
RPC server manager.
"""
_rpc.notify(topic_name, data)
def __init__(self, bot):
self.app = web.Application(loop=bot.loop)
self.app.router.add_post("/rpc", self.handle)
def add_method(prefix, method):
"""
Makes a method available to RPC clients. The name given to clients will be as
follows::
self.app_handler = self.app.make_handler()
"{}__{}".format(prefix, method.__name__)
self.server = None
note::
super().__init__()
This method will fail silently.
async def initialize(self):
"""
Finalizes the initialization of the RPC server and allows it to begin
accepting queries.
"""
self.server = await self.app.loop.create_server(self.app_handler, "127.0.0.1", 6133)
log.debug("Created RPC server listener.")
Parameters
----------
prefix
method
MUST BE A COROUTINE OR OBJECT.
"""
_rpc.add_methods(("", method), prefix=prefix)
def close(self):
"""
Closes the RPC server.
"""
self.server.close()
def clean_up():
if _rpc_server is not None:
_rpc_server.close()
async def handle(self, request):
request = await request.text()
response = await methods.dispatch(request)
if response.is_notification:
return web.Response()
else:
return web.json_response(response, status=response.http_status)

View File

@ -3,7 +3,7 @@ aiohttp>=2.0.0,<2.3.0
appdirs==1.4.3
raven==6.5.0
colorama==0.3.9
aiohttp-json-rpc==0.8.7
jsonrpcserver
pyyaml==3.12
Red-Trivia>=1.1.1
async-timeout<3.0.0