2019-01-05 16:55:03 +01:00
|
|
|
from collections import OrderedDict
|
2018-12-18 17:32:16 +01:00
|
|
|
from enum import Enum
|
2019-01-22 19:25:00 +01:00
|
|
|
from lightning import LightningRpc
|
|
|
|
from threading import RLock
|
2019-01-05 16:55:03 +01:00
|
|
|
|
2018-12-10 15:38:21 +01:00
|
|
|
import inspect
|
2019-01-03 20:23:02 +01:00
|
|
|
import json
|
|
|
|
import os
|
2018-12-29 12:28:52 +01:00
|
|
|
import re
|
2019-01-03 20:23:02 +01:00
|
|
|
import sys
|
2018-12-10 15:38:21 +01:00
|
|
|
import traceback
|
|
|
|
|
|
|
|
|
2018-12-18 17:32:16 +01:00
|
|
|
class MethodType(Enum):
|
|
|
|
RPCMETHOD = 0
|
|
|
|
HOOK = 1
|
|
|
|
|
|
|
|
|
2019-01-22 19:25:00 +01:00
|
|
|
class RequestState(Enum):
|
|
|
|
PENDING = 'pending'
|
|
|
|
FINISHED = 'finished'
|
|
|
|
FAILED = 'failed'
|
|
|
|
|
|
|
|
|
2019-01-22 17:45:50 +01:00
|
|
|
class Method(object):
|
|
|
|
"""Description of methods that are registered with the plugin.
|
|
|
|
|
|
|
|
These can be one of the following:
|
|
|
|
|
|
|
|
- RPC exposed by RPC passthrough
|
|
|
|
- HOOK registered to be called synchronously by lightningd
|
|
|
|
"""
|
|
|
|
def __init__(self, name, func, mtype=MethodType.RPCMETHOD):
|
|
|
|
self.name = name
|
|
|
|
self.func = func
|
|
|
|
self.mtype = mtype
|
2019-01-22 19:25:00 +01:00
|
|
|
self.background = False
|
|
|
|
|
|
|
|
|
|
|
|
class Request(dict):
|
|
|
|
"""A request object that wraps params and allows async return
|
|
|
|
"""
|
|
|
|
def __init__(self, plugin, req_id, method, params, background=False):
|
|
|
|
self.method = method
|
|
|
|
self.params = params
|
|
|
|
self.background = background
|
|
|
|
self.plugin = plugin
|
|
|
|
self.state = RequestState.PENDING
|
|
|
|
self.id = req_id
|
|
|
|
|
|
|
|
def getattr(self, key):
|
|
|
|
if key == "params":
|
|
|
|
return self.params
|
|
|
|
elif key == "id":
|
|
|
|
return self.id
|
|
|
|
elif key == "method":
|
|
|
|
return self.method
|
|
|
|
|
|
|
|
def set_result(self, result):
|
|
|
|
if self.state != RequestState.PENDING:
|
|
|
|
raise ValueError(
|
|
|
|
"Cannot set the result of a request that is not pending, "
|
|
|
|
"current state is {state}".format(self.state))
|
|
|
|
self.result = result
|
|
|
|
self._write_result({
|
|
|
|
'jsonrpc': '2.0',
|
|
|
|
'id': self.id,
|
|
|
|
'result': self.result
|
|
|
|
})
|
|
|
|
|
|
|
|
def set_exception(self, exc):
|
|
|
|
if self.state != RequestState.PENDING:
|
|
|
|
raise ValueError(
|
|
|
|
"Cannot set the exception of a request that is not pending, "
|
|
|
|
"current state is {state}".format(self.state))
|
|
|
|
self.exc = exc
|
|
|
|
self._write_result({
|
|
|
|
'jsonrpc': '2.0',
|
|
|
|
'id': self.id,
|
|
|
|
"error": "Error while processing {method}: {exc}".format(
|
|
|
|
method=self.method, exc=repr(exc)
|
|
|
|
),
|
|
|
|
})
|
|
|
|
|
|
|
|
def _write_result(self, result):
|
|
|
|
with self.plugin.write_lock:
|
|
|
|
json.dump(result, fp=self.plugin.stdout)
|
|
|
|
self.plugin.stdout.write('\n\n')
|
|
|
|
self.plugin.stdout.flush()
|
2019-01-22 17:45:50 +01:00
|
|
|
|
|
|
|
|
2018-12-10 15:38:21 +01:00
|
|
|
class Plugin(object):
|
|
|
|
"""Controls interactions with lightningd, and bundles functionality.
|
|
|
|
|
|
|
|
The Plugin class serves two purposes: it collects RPC methods and
|
|
|
|
options, and offers a control loop that dispatches incoming RPC
|
|
|
|
calls and hooks.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, stdout=None, stdin=None, autopatch=True):
|
2019-01-22 17:45:50 +01:00
|
|
|
self.methods = {'init': Method('init', self._init, MethodType.RPCMETHOD)}
|
2018-12-10 15:38:21 +01:00
|
|
|
self.options = {}
|
|
|
|
|
2018-12-11 18:24:55 +01:00
|
|
|
# A dict from topics to handler functions
|
|
|
|
self.subscriptions = {}
|
|
|
|
|
2018-12-10 15:38:21 +01:00
|
|
|
if not stdout:
|
|
|
|
self.stdout = sys.stdout
|
|
|
|
if not stdin:
|
|
|
|
self.stdin = sys.stdin
|
|
|
|
|
|
|
|
if os.getenv('LIGHTNINGD_PLUGIN') and autopatch:
|
|
|
|
monkey_patch(self, stdout=True, stderr=True)
|
|
|
|
|
2019-01-22 23:13:08 +01:00
|
|
|
self.add_method("getmanifest", self._getmanifest, background=False)
|
2018-12-10 15:38:21 +01:00
|
|
|
self.rpc_filename = None
|
|
|
|
self.lightning_dir = None
|
2019-01-03 20:23:02 +01:00
|
|
|
self.rpc = None
|
2019-02-04 11:55:42 +01:00
|
|
|
self.child_init = None
|
2018-12-10 15:38:21 +01:00
|
|
|
|
2019-01-22 19:25:00 +01:00
|
|
|
self.write_lock = RLock()
|
|
|
|
|
2019-01-22 23:13:08 +01:00
|
|
|
def add_method(self, name, func, background=False):
|
2018-12-10 15:38:21 +01:00
|
|
|
"""Add a plugin method to the dispatch table.
|
|
|
|
|
|
|
|
The function will be expected at call time (see `_dispatch`)
|
|
|
|
and the parameters from the JSON-RPC call will be mapped to
|
|
|
|
the function arguments. In addition to the parameters passed
|
|
|
|
from the JSON-RPC call we add a few that may be useful:
|
|
|
|
|
|
|
|
- `plugin`: gets a reference to this plugin.
|
|
|
|
|
|
|
|
- `request`: gets a reference to the raw request as a
|
|
|
|
dict. This corresponds to the JSON-RPC message that is
|
|
|
|
being dispatched.
|
|
|
|
|
|
|
|
Notice that due to the python binding logic we may be mapping
|
|
|
|
the arguments wrongly if we inject the plugin and/or request
|
|
|
|
in combination with positional binding. To prevent issues the
|
|
|
|
plugin and request argument should always be the last two
|
|
|
|
arguments and have a default on None.
|
|
|
|
|
2019-01-22 23:13:08 +01:00
|
|
|
The `background` argument can be used to specify whether the method is
|
|
|
|
going to return a result that should be sent back to the lightning
|
|
|
|
daemon (`background=False`) or whether the method will return without
|
|
|
|
sending back a result. In the latter case the method MUST use
|
|
|
|
`request.set_result` or `result.set_exception` to return a result or
|
|
|
|
raise an exception for the call.
|
|
|
|
|
2018-12-10 15:38:21 +01:00
|
|
|
"""
|
|
|
|
if name in self.methods:
|
|
|
|
raise ValueError(
|
|
|
|
"Name {} is already bound to a method.".format(name)
|
|
|
|
)
|
|
|
|
|
|
|
|
# Register the function with the name
|
2019-01-22 17:45:50 +01:00
|
|
|
method = Method(name, func, MethodType.RPCMETHOD)
|
2019-01-22 23:13:08 +01:00
|
|
|
method.background = background
|
2019-01-22 17:45:50 +01:00
|
|
|
self.methods[name] = method
|
2018-12-10 15:38:21 +01:00
|
|
|
|
2018-12-11 18:24:55 +01:00
|
|
|
def add_subscription(self, topic, func):
|
|
|
|
"""Add a subscription to our list of subscriptions.
|
|
|
|
|
|
|
|
A subscription is an association between a topic and a handler
|
|
|
|
function. Adding a subscription means that we will
|
|
|
|
automatically subscribe to events from that topic with
|
|
|
|
`lightningd` and, upon receiving a matching notification, we
|
|
|
|
will call the associated handler. Notice that in order for the
|
|
|
|
automatic subscriptions to work, the handlers need to be
|
|
|
|
registered before we send our manifest, hence before
|
|
|
|
`Plugin.run` is called.
|
|
|
|
|
|
|
|
"""
|
|
|
|
if topic in self.subscriptions:
|
|
|
|
raise ValueError(
|
|
|
|
"Topic {} already has a handler".format(topic)
|
|
|
|
)
|
|
|
|
self.subscriptions[topic] = func
|
|
|
|
|
|
|
|
def subscribe(self, topic):
|
|
|
|
"""Function decorator to register a notification handler.
|
|
|
|
"""
|
|
|
|
def decorator(f):
|
|
|
|
self.add_subscription(topic, f)
|
|
|
|
return f
|
|
|
|
return decorator
|
|
|
|
|
2018-12-10 15:59:31 +01:00
|
|
|
def add_option(self, name, default, description):
|
|
|
|
"""Add an option that we'd like to register with lightningd.
|
|
|
|
|
|
|
|
Needs to be called before `Plugin.run`, otherwise we might not
|
|
|
|
end up getting it set.
|
|
|
|
|
|
|
|
"""
|
|
|
|
if name in self.options:
|
|
|
|
raise ValueError(
|
|
|
|
"Name {} is already used by another option".format(name)
|
|
|
|
)
|
|
|
|
self.options[name] = {
|
|
|
|
'name': name,
|
|
|
|
'default': default,
|
|
|
|
'description': description,
|
|
|
|
'type': 'string',
|
2018-12-14 14:01:37 +01:00
|
|
|
'value': None,
|
2018-12-10 15:59:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
def get_option(self, name):
|
2018-12-14 14:01:37 +01:00
|
|
|
if name not in self.options:
|
2018-12-10 15:59:31 +01:00
|
|
|
raise ValueError("No option with name {} registered".format(name))
|
|
|
|
|
2018-12-14 14:01:37 +01:00
|
|
|
if self.options[name]['value'] is not None:
|
|
|
|
return self.options[name]['value']
|
|
|
|
else:
|
|
|
|
return self.options[name]['default']
|
|
|
|
|
2019-01-22 23:13:08 +01:00
|
|
|
def method(self, method_name, background=True):
|
2018-12-10 15:38:21 +01:00
|
|
|
"""Decorator to add a plugin method to the dispatch table.
|
|
|
|
|
|
|
|
Internally uses add_method.
|
|
|
|
"""
|
|
|
|
def decorator(f):
|
2019-01-22 23:13:08 +01:00
|
|
|
self.add_method(method_name, f, background=background)
|
2018-12-10 15:38:21 +01:00
|
|
|
return f
|
|
|
|
return decorator
|
|
|
|
|
2019-01-22 23:13:08 +01:00
|
|
|
def add_hook(self, name, func, background=False):
|
2018-12-18 17:32:16 +01:00
|
|
|
"""Register a hook that is called synchronously by lightningd on events
|
|
|
|
"""
|
|
|
|
if name in self.methods:
|
|
|
|
raise ValueError(
|
|
|
|
"Method {} was already registered".format(name, self.methods[name])
|
|
|
|
)
|
2019-01-22 17:45:50 +01:00
|
|
|
method = Method(name, func, MethodType.HOOK)
|
2019-01-22 23:13:08 +01:00
|
|
|
method.background = background
|
2019-01-22 17:45:50 +01:00
|
|
|
self.methods[name] = method
|
2018-12-18 17:32:16 +01:00
|
|
|
|
2019-01-22 23:13:08 +01:00
|
|
|
def hook(self, method_name, background=False):
|
2018-12-18 17:32:16 +01:00
|
|
|
"""Decorator to add a plugin hook to the dispatch table.
|
|
|
|
|
|
|
|
Internally uses add_hook.
|
|
|
|
"""
|
|
|
|
def decorator(f):
|
2019-01-22 23:13:08 +01:00
|
|
|
self.add_hook(method_name, f, background=background)
|
2018-12-18 17:32:16 +01:00
|
|
|
return f
|
|
|
|
return decorator
|
|
|
|
|
2019-02-04 11:55:42 +01:00
|
|
|
def init(self, *args, **kwargs):
|
|
|
|
"""Decorator to add a function called after plugin initialization
|
|
|
|
"""
|
|
|
|
def decorator(f):
|
|
|
|
if self.child_init is not None:
|
|
|
|
raise ValueError('The @plugin.init decorator should only be used once')
|
|
|
|
self.child_init = f
|
|
|
|
return f
|
|
|
|
return decorator
|
|
|
|
|
2018-12-11 18:24:55 +01:00
|
|
|
def _exec_func(self, func, request):
|
2019-01-22 19:25:00 +01:00
|
|
|
params = request.params
|
2018-12-10 15:38:21 +01:00
|
|
|
sig = inspect.signature(func)
|
|
|
|
|
2019-01-05 16:55:03 +01:00
|
|
|
arguments = OrderedDict()
|
|
|
|
for name, value in sig.parameters.items():
|
2019-01-22 23:09:42 +01:00
|
|
|
arguments[name] = inspect._empty
|
2018-12-10 15:38:21 +01:00
|
|
|
|
2019-01-05 16:55:03 +01:00
|
|
|
# Fill in any injected parameters
|
|
|
|
if 'plugin' in arguments:
|
|
|
|
arguments['plugin'] = self
|
2018-12-10 15:38:21 +01:00
|
|
|
|
2019-01-05 16:55:03 +01:00
|
|
|
if 'request' in arguments:
|
|
|
|
arguments['request'] = request
|
|
|
|
|
2019-02-21 18:30:15 +01:00
|
|
|
args = []
|
|
|
|
kwargs = {}
|
2019-01-05 16:55:03 +01:00
|
|
|
# Now zip the provided arguments and the prefilled a together
|
|
|
|
if isinstance(params, dict):
|
2019-02-21 18:30:15 +01:00
|
|
|
for k, v in params.items():
|
|
|
|
if k in arguments:
|
|
|
|
arguments[k] = v
|
|
|
|
else:
|
|
|
|
kwargs[k] = v
|
2019-01-05 16:55:03 +01:00
|
|
|
else:
|
|
|
|
pos = 0
|
|
|
|
for k, v in arguments.items():
|
2019-02-21 18:30:15 +01:00
|
|
|
# Skip already assigned args and special catch-all args
|
|
|
|
if v != inspect._empty or k in ['args', 'kwargs']:
|
2019-01-05 16:55:03 +01:00
|
|
|
continue
|
2019-02-21 18:30:15 +01:00
|
|
|
|
2019-01-05 16:55:03 +01:00
|
|
|
if pos < len(params):
|
|
|
|
# Apply positional args if we have them
|
|
|
|
arguments[k] = params[pos]
|
2019-02-21 13:37:05 +01:00
|
|
|
elif sig.parameters[k].default is inspect.Signature.empty:
|
|
|
|
# This is a positional arg with no value passed
|
|
|
|
raise TypeError("Missing required parameter: %s" % sig.parameters[k])
|
2019-01-05 16:55:03 +01:00
|
|
|
else:
|
|
|
|
# For the remainder apply default args
|
|
|
|
arguments[k] = sig.parameters[k].default
|
|
|
|
pos += 1
|
2019-02-21 18:30:15 +01:00
|
|
|
if len(arguments) < len(params):
|
|
|
|
args = params[len(arguments):]
|
|
|
|
|
|
|
|
if 'kwargs' in arguments:
|
|
|
|
arguments['kwargs'] = kwargs
|
|
|
|
elif len(kwargs) > 0:
|
|
|
|
raise TypeError("Extra arguments given: {kwargs}".format(kwargs=kwargs))
|
|
|
|
|
|
|
|
if 'args' in arguments:
|
|
|
|
arguments['args'] = args
|
|
|
|
elif len(args) > 0:
|
|
|
|
raise TypeError("Extra arguments given: {args}".format(args=args))
|
2019-01-05 16:55:03 +01:00
|
|
|
|
2019-01-22 23:09:42 +01:00
|
|
|
missing = [k for k, v in arguments.items() if v == inspect._empty]
|
|
|
|
if missing:
|
|
|
|
raise TypeError("Missing positional arguments ({given} given, "
|
|
|
|
"expected {expected}): {missing}".format(
|
|
|
|
missing=", ".join(missing),
|
|
|
|
given=len(arguments) - len(missing),
|
|
|
|
expected=len(arguments)
|
|
|
|
))
|
|
|
|
|
2019-01-05 16:55:03 +01:00
|
|
|
ba = sig.bind(**arguments)
|
2018-12-10 15:38:21 +01:00
|
|
|
ba.apply_defaults()
|
|
|
|
return func(*ba.args, **ba.kwargs)
|
|
|
|
|
2018-12-11 18:24:55 +01:00
|
|
|
def _dispatch_request(self, request):
|
2019-01-22 19:25:00 +01:00
|
|
|
name = request.method
|
2018-12-11 18:24:55 +01:00
|
|
|
|
|
|
|
if name not in self.methods:
|
|
|
|
raise ValueError("No method {} found.".format(name))
|
2019-01-22 17:45:50 +01:00
|
|
|
method = self.methods[name]
|
2019-01-22 19:25:00 +01:00
|
|
|
request.background = method.background
|
2018-12-11 18:24:55 +01:00
|
|
|
|
|
|
|
try:
|
2019-01-22 19:25:00 +01:00
|
|
|
result = self._exec_func(method.func, request)
|
|
|
|
if not method.background:
|
|
|
|
# Only if this is not an async (background) call do we need to
|
|
|
|
# return the result, otherwise the callee will eventually need
|
|
|
|
# to call request.set_result or request.set_exception to
|
|
|
|
# return a result or raise an exception.
|
|
|
|
request.set_result(result)
|
2018-12-11 18:24:55 +01:00
|
|
|
except Exception as e:
|
2019-01-22 19:25:00 +01:00
|
|
|
request.set_exception(e)
|
2018-12-11 18:24:55 +01:00
|
|
|
self.log(traceback.format_exc())
|
|
|
|
|
|
|
|
def _dispatch_notification(self, request):
|
2019-01-22 19:25:00 +01:00
|
|
|
if request.method not in self.subscriptions:
|
|
|
|
raise ValueError("No subscription for {name} found.".format(
|
|
|
|
name=request.method))
|
|
|
|
func = self.subscriptions[request.method]
|
2018-12-11 18:24:55 +01:00
|
|
|
|
|
|
|
try:
|
|
|
|
self._exec_func(func, request)
|
2019-01-25 03:51:54 +01:00
|
|
|
except Exception:
|
2018-12-11 18:24:55 +01:00
|
|
|
self.log(traceback.format_exc())
|
|
|
|
|
2018-12-10 15:38:21 +01:00
|
|
|
def notify(self, method, params):
|
|
|
|
payload = {
|
|
|
|
'jsonrpc': '2.0',
|
|
|
|
'method': method,
|
|
|
|
'params': params,
|
|
|
|
}
|
2019-01-22 19:25:00 +01:00
|
|
|
with self.write_lock:
|
|
|
|
json.dump(payload, self.stdout)
|
|
|
|
self.stdout.write("\n\n")
|
|
|
|
self.stdout.flush()
|
2018-12-10 15:38:21 +01:00
|
|
|
|
|
|
|
def log(self, message, level='info'):
|
2018-12-10 16:41:42 +01:00
|
|
|
# Split the log into multiple lines and print them
|
|
|
|
# individually. Makes tracebacks much easier to read.
|
|
|
|
for line in message.split('\n'):
|
|
|
|
self.notify('log', {'level': level, 'message': line})
|
2018-12-10 15:38:21 +01:00
|
|
|
|
|
|
|
def _multi_dispatch(self, msgs):
|
|
|
|
"""We received a couple of messages, now try to dispatch them all.
|
|
|
|
|
|
|
|
Returns the last partial message that was not complete yet.
|
|
|
|
"""
|
|
|
|
for payload in msgs[:-1]:
|
|
|
|
request = json.loads(payload)
|
2019-01-22 19:25:00 +01:00
|
|
|
request = Request(
|
|
|
|
plugin=self,
|
|
|
|
req_id=request.get('id', None),
|
|
|
|
method=request['method'],
|
|
|
|
params=request['params'],
|
|
|
|
background=False,
|
|
|
|
)
|
2018-12-10 15:38:21 +01:00
|
|
|
|
2018-12-11 18:24:55 +01:00
|
|
|
# If this has an 'id'-field, it's a request and returns a
|
|
|
|
# result. Otherwise it's a notification and it doesn't
|
|
|
|
# return anything.
|
2019-01-22 19:25:00 +01:00
|
|
|
if request.id is not None:
|
2018-12-11 18:24:55 +01:00
|
|
|
self._dispatch_request(request)
|
|
|
|
else:
|
|
|
|
self._dispatch_notification(request)
|
|
|
|
|
2018-12-10 15:38:21 +01:00
|
|
|
return msgs[-1]
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
partial = ""
|
|
|
|
for l in self.stdin:
|
|
|
|
partial += l
|
|
|
|
|
|
|
|
msgs = partial.split('\n\n')
|
|
|
|
if len(msgs) < 2:
|
|
|
|
continue
|
|
|
|
|
|
|
|
partial = self._multi_dispatch(msgs)
|
|
|
|
|
2019-02-20 12:31:34 +01:00
|
|
|
def _getmanifest(self, **kwargs):
|
2018-12-10 15:38:21 +01:00
|
|
|
methods = []
|
2018-12-18 17:32:16 +01:00
|
|
|
hooks = []
|
2019-01-22 17:45:50 +01:00
|
|
|
for method in self.methods.values():
|
2018-12-10 15:38:21 +01:00
|
|
|
# Skip the builtin ones, they don't get reported
|
2019-01-22 17:45:50 +01:00
|
|
|
if method.name in ['getmanifest', 'init']:
|
2018-12-10 15:38:21 +01:00
|
|
|
continue
|
|
|
|
|
2019-01-22 17:45:50 +01:00
|
|
|
if method.mtype == MethodType.HOOK:
|
|
|
|
hooks.append(method.name)
|
2018-12-18 17:32:16 +01:00
|
|
|
continue
|
|
|
|
|
2019-01-22 17:45:50 +01:00
|
|
|
doc = inspect.getdoc(method.func)
|
2018-12-10 15:38:21 +01:00
|
|
|
if not doc:
|
|
|
|
self.log(
|
2019-01-22 17:45:50 +01:00
|
|
|
'RPC method \'{}\' does not have a docstring.'.format(method.name)
|
2018-12-10 15:38:21 +01:00
|
|
|
)
|
|
|
|
doc = "Undocumented RPC method from a plugin."
|
2019-01-08 19:24:33 +01:00
|
|
|
doc = re.sub('\n+', ' ', doc)
|
2018-12-10 15:38:21 +01:00
|
|
|
|
2019-02-07 04:14:53 +01:00
|
|
|
# Handles out-of-order use of parameters like:
|
|
|
|
# def hello_obfus(arg1, arg2, plugin, thing3, request=None, thing5='at', thing6=21)
|
2019-01-22 17:45:50 +01:00
|
|
|
argspec = inspect.getargspec(method.func)
|
2019-02-05 01:16:47 +01:00
|
|
|
defaults = argspec.defaults
|
2019-02-07 04:14:53 +01:00
|
|
|
num_defaults = len(defaults) if defaults else 0
|
|
|
|
start_kwargs_idx = len(argspec.args) - num_defaults
|
|
|
|
args = []
|
|
|
|
for idx, arg in enumerate(argspec.args):
|
|
|
|
if arg in ('plugin', 'request'):
|
|
|
|
continue
|
|
|
|
# Positional arg
|
|
|
|
if idx < start_kwargs_idx:
|
|
|
|
args.append("%s" % arg)
|
|
|
|
# Keyword arg
|
|
|
|
else:
|
|
|
|
args.append("[%s]" % arg)
|
2019-02-05 01:16:47 +01:00
|
|
|
|
2018-12-10 15:38:21 +01:00
|
|
|
methods.append({
|
2019-01-22 17:45:50 +01:00
|
|
|
'name': method.name,
|
2019-02-05 01:16:47 +01:00
|
|
|
'usage': " ".join(args),
|
|
|
|
'description': doc
|
2018-12-10 15:38:21 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
return {
|
2018-12-10 15:59:31 +01:00
|
|
|
'options': list(self.options.values()),
|
2018-12-10 15:38:21 +01:00
|
|
|
'rpcmethods': methods,
|
2018-12-11 18:24:55 +01:00
|
|
|
'subscriptions': list(self.subscriptions.keys()),
|
2018-12-18 17:32:16 +01:00
|
|
|
'hooks': hooks,
|
2018-12-10 15:38:21 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
def _init(self, options, configuration, request):
|
2018-12-10 15:59:31 +01:00
|
|
|
self.rpc_filename = configuration['rpc-file']
|
|
|
|
self.lightning_dir = configuration['lightning-dir']
|
2019-01-03 20:23:02 +01:00
|
|
|
path = os.path.join(self.lightning_dir, self.rpc_filename)
|
|
|
|
self.rpc = LightningRpc(path)
|
2018-12-10 15:59:31 +01:00
|
|
|
for name, value in options.items():
|
2018-12-14 14:01:37 +01:00
|
|
|
self.options[name]['value'] = value
|
2018-12-10 15:59:31 +01:00
|
|
|
|
2019-02-04 11:55:42 +01:00
|
|
|
# Dispatch the plugin's init handler if any
|
|
|
|
if self.child_init:
|
|
|
|
return self._exec_func(self.child_init, request)
|
2018-12-10 15:38:21 +01:00
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
class PluginStream(object):
|
|
|
|
"""Sink that turns everything that is written to it into a notification.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, plugin, level="info"):
|
|
|
|
self.plugin = plugin
|
|
|
|
self.level = level
|
|
|
|
self.buff = ''
|
|
|
|
|
|
|
|
def write(self, payload):
|
|
|
|
self.buff += payload
|
|
|
|
|
2019-01-21 18:28:33 +01:00
|
|
|
if len(payload) > 0 and payload[-1] == '\n':
|
2018-12-10 15:38:21 +01:00
|
|
|
self.flush()
|
|
|
|
|
|
|
|
def flush(self):
|
|
|
|
lines = self.buff.split('\n')
|
|
|
|
if len(lines) < 2:
|
|
|
|
return
|
|
|
|
|
|
|
|
for l in lines[:-1]:
|
|
|
|
self.plugin.log(l, self.level)
|
|
|
|
|
|
|
|
# lines[-1] is either an empty string or a partial line
|
|
|
|
self.buff = lines[-1]
|
|
|
|
|
|
|
|
|
|
|
|
def monkey_patch(plugin, stdout=True, stderr=False):
|
|
|
|
"""Monkey patch stderr and stdout so we use notifications instead.
|
|
|
|
|
|
|
|
A plugin commonly communicates with lightningd over its stdout and
|
|
|
|
stdin filedescriptor, so if we use them in some other way
|
|
|
|
(printing, logging, ...) we're breaking our communication
|
|
|
|
channel. This function monkey patches these streams in the `sys`
|
|
|
|
module to be redirected to a `PluginStream` which wraps anything
|
|
|
|
that would get written to these streams into valid log
|
|
|
|
notifications that can be interpreted and printed by `lightningd`.
|
|
|
|
|
|
|
|
"""
|
|
|
|
if stdout:
|
|
|
|
setattr(sys, "stdout", PluginStream(plugin, level="info"))
|
|
|
|
if stderr:
|
|
|
|
setattr(sys, "stderr", PluginStream(plugin, level="warn"))
|