mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-06 03:08:55 -05:00
[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:
parent
abfee70eb3
commit
73a427f6aa
34
Pipfile.lock
generated
34
Pipfile.lock
generated
@ -32,13 +32,6 @@
|
|||||||
],
|
],
|
||||||
"version": "==2.2.5"
|
"version": "==2.2.5"
|
||||||
},
|
},
|
||||||
"aiohttp-json-rpc": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:9ec69ea70ce49c4af445f0ac56ac728708ccfad8b214272d2cc7e75bc0b31327",
|
|
||||||
"sha256:e2b8b49779d5d9b811f3a94e98092b1fa14af6d9adbf71c3afa6b20c641fa5d5"
|
|
||||||
],
|
|
||||||
"version": "==0.8.7"
|
|
||||||
},
|
|
||||||
"appdirs": {
|
"appdirs": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
|
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
|
||||||
@ -83,6 +76,13 @@
|
|||||||
"editable": true,
|
"editable": true,
|
||||||
"path": "."
|
"path": "."
|
||||||
},
|
},
|
||||||
|
"funcsigs": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca",
|
||||||
|
"sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50"
|
||||||
|
],
|
||||||
|
"version": "==1.0.2"
|
||||||
|
},
|
||||||
"idna": {
|
"idna": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f",
|
"sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f",
|
||||||
@ -90,6 +90,19 @@
|
|||||||
],
|
],
|
||||||
"version": "==2.6"
|
"version": "==2.6"
|
||||||
},
|
},
|
||||||
|
"jsonrpcserver": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:ab8013cdee3f65d59c5d3f84c75be76a3492caa0b33ecaa3f0f69906cf3d9e92"
|
||||||
|
],
|
||||||
|
"version": "==3.5.4"
|
||||||
|
},
|
||||||
|
"jsonschema": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:000e68abd33c972a5248544925a0cae7d1125f9bf6c58280d37546b946769a08",
|
||||||
|
"sha256:6ff5f3180870836cae40f06fa10419f557208175f13ad7bc26caa77beb1f6e02"
|
||||||
|
],
|
||||||
|
"version": "==2.6.0"
|
||||||
|
},
|
||||||
"multidict": {
|
"multidict": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1a1d76374a1e7fe93acef96b354a03c1d7f83e7512e225a527d283da0d7ba5e0",
|
"sha256:1a1d76374a1e7fe93acef96b354a03c1d7f83e7512e225a527d283da0d7ba5e0",
|
||||||
@ -140,6 +153,13 @@
|
|||||||
],
|
],
|
||||||
"version": "==1.1.1"
|
"version": "==1.1.1"
|
||||||
},
|
},
|
||||||
|
"six": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
|
||||||
|
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
|
||||||
|
],
|
||||||
|
"version": "==1.11.0"
|
||||||
|
},
|
||||||
"websockets": {
|
"websockets": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:09dfec40e9b73e8808c39ecdbc1733e33915a2b26b90c54566afc0af546a9ec3",
|
"sha256:09dfec40e9b73e8808c39ecdbc1733e33915a2b26b90c54566afc0af546a9ec3",
|
||||||
|
|||||||
@ -4,5 +4,36 @@
|
|||||||
RPC
|
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:
|
:members:
|
||||||
|
|||||||
@ -160,7 +160,7 @@ def main():
|
|||||||
sentry_log.critical("Fatal Exception", exc_info=e)
|
sentry_log.critical("Fatal Exception", exc_info=e)
|
||||||
loop.run_until_complete(red.logout())
|
loop.run_until_complete(red.logout())
|
||||||
finally:
|
finally:
|
||||||
rpc.clean_up()
|
red.rpc.close()
|
||||||
if cleanup_tasks:
|
if cleanup_tasks:
|
||||||
pending = asyncio.Task.all_tasks(loop=red.loop)
|
pending = asyncio.Task.all_tasks(loop=red.loop)
|
||||||
gathered = asyncio.gather(*pending, loop=red.loop, return_exceptions=True)
|
gathered = asyncio.gather(*pending, loop=red.loop, return_exceptions=True)
|
||||||
|
|||||||
@ -21,22 +21,9 @@ from .cog_manager import CogManager
|
|||||||
from . import Config, i18n, commands, rpc
|
from . import Config, i18n, commands, rpc
|
||||||
from .help_formatter import Help, help as help_
|
from .help_formatter import Help, help as help_
|
||||||
from .sentry import SentryManager
|
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.
|
"""Mixin for the main bot class.
|
||||||
|
|
||||||
This exists because `Red` inherits from `discord.AutoShardedClient`, which
|
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.cog_mgr = CogManager(paths=(str(self.main_dir / "cogs"),))
|
||||||
|
|
||||||
self.register_rpc_methods()
|
|
||||||
|
|
||||||
super().__init__(formatter=Help(), **kwargs)
|
super().__init__(formatter=Help(), **kwargs)
|
||||||
|
|
||||||
|
if self.rpc_enabled:
|
||||||
|
self.rpc = rpc.RPC(self)
|
||||||
|
|
||||||
self.remove_command("help")
|
self.remove_command("help")
|
||||||
|
|
||||||
self.add_command(help_)
|
self.add_command(help_)
|
||||||
@ -275,10 +263,6 @@ class RedBase(BotBase, RpcMethodMixin):
|
|||||||
if pkg_name.startswith("redbot.cogs"):
|
if pkg_name.startswith("redbot.cogs"):
|
||||||
del sys.modules["redbot.cogs"].__dict__[name]
|
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):
|
class Red(RedBase, discord.AutoShardedClient):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -50,10 +50,6 @@ class Core:
|
|||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
self.bot = bot # type: Red
|
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)
|
@commands.command(hidden=True)
|
||||||
async def ping(self, ctx):
|
async def ping(self, ctx):
|
||||||
"""Pong."""
|
"""Pong."""
|
||||||
|
|||||||
@ -16,7 +16,6 @@ from discord.ext import commands
|
|||||||
from . import __version__
|
from . import __version__
|
||||||
from .data_manager import storage_type
|
from .data_manager import storage_type
|
||||||
from .utils.chat_formatting import inline, bordered
|
from .utils.chat_formatting import inline, bordered
|
||||||
from .rpc import initialize
|
|
||||||
from colorama import Fore, Style, init
|
from colorama import Fore, Style, init
|
||||||
|
|
||||||
log = logging.getLogger("red")
|
log = logging.getLogger("red")
|
||||||
@ -173,7 +172,7 @@ def init_events(bot, cli_flags):
|
|||||||
print("\nInvite URL: {}\n".format(invite_url))
|
print("\nInvite URL: {}\n".format(invite_url))
|
||||||
|
|
||||||
if bot.rpc_enabled:
|
if bot.rpc_enabled:
|
||||||
await initialize(bot)
|
await bot.rpc.initialize()
|
||||||
|
|
||||||
@bot.event
|
@bot.event
|
||||||
async def on_error(event_method, *args, **kwargs):
|
async def on_error(event_method, *args, **kwargs):
|
||||||
|
|||||||
@ -1,85 +1,135 @@
|
|||||||
import asyncio
|
import weakref
|
||||||
|
|
||||||
from aiohttp.web import Application
|
from aiohttp import web
|
||||||
from aiohttp_json_rpc import JsonRpc
|
import jsonrpcserver.aio
|
||||||
|
|
||||||
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from .utils import TYPE_CHECKING, NewType
|
__all__ = ["methods", "RPC", "Methods"]
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .bot import Red
|
|
||||||
|
|
||||||
log = logging.getLogger("red.rpc")
|
log = logging.getLogger("red.rpc")
|
||||||
JsonSerializable = NewType("JsonSerializable", dict)
|
|
||||||
|
|
||||||
_rpc = JsonRpc(logger=log)
|
|
||||||
|
|
||||||
_rpc_server = None # type: asyncio.AbstractServer
|
|
||||||
|
|
||||||
|
|
||||||
async def initialize(bot: "Red"):
|
class Methods(jsonrpcserver.aio.AsyncMethods):
|
||||||
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.
|
Container class for all registered RPC methods, please use the existing `methods`
|
||||||
|
attribute rather than creating a new instance of this class.
|
||||||
|
|
||||||
Parameters
|
.. warning::
|
||||||
----------
|
|
||||||
topic_name
|
**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.
|
RPC server manager.
|
||||||
|
|
||||||
data MUST be json serializable.
|
|
||||||
|
|
||||||
note::
|
|
||||||
|
|
||||||
This method will fail silently.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
topic_name
|
|
||||||
data
|
|
||||||
"""
|
"""
|
||||||
_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):
|
self.app_handler = self.app.make_handler()
|
||||||
"""
|
|
||||||
Makes a method available to RPC clients. The name given to clients will be as
|
|
||||||
follows::
|
|
||||||
|
|
||||||
"{}__{}".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
|
def close(self):
|
||||||
----------
|
"""
|
||||||
prefix
|
Closes the RPC server.
|
||||||
method
|
"""
|
||||||
MUST BE A COROUTINE OR OBJECT.
|
self.server.close()
|
||||||
"""
|
|
||||||
_rpc.add_methods(("", method), prefix=prefix)
|
|
||||||
|
|
||||||
|
async def handle(self, request):
|
||||||
def clean_up():
|
request = await request.text()
|
||||||
if _rpc_server is not None:
|
response = await methods.dispatch(request)
|
||||||
_rpc_server.close()
|
if response.is_notification:
|
||||||
|
return web.Response()
|
||||||
|
else:
|
||||||
|
return web.json_response(response, status=response.http_status)
|
||||||
|
|||||||
@ -3,7 +3,7 @@ aiohttp>=2.0.0,<2.3.0
|
|||||||
appdirs==1.4.3
|
appdirs==1.4.3
|
||||||
raven==6.5.0
|
raven==6.5.0
|
||||||
colorama==0.3.9
|
colorama==0.3.9
|
||||||
aiohttp-json-rpc==0.8.7
|
jsonrpcserver
|
||||||
pyyaml==3.12
|
pyyaml==3.12
|
||||||
Red-Trivia>=1.1.1
|
Red-Trivia>=1.1.1
|
||||||
async-timeout<3.0.0
|
async-timeout<3.0.0
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user