Files
plugins/autoreload/autoreload.py
darosior e426f6738e Update to pyln-client
This updates all plugins *which already have requirements.txt* to
pyln-client from pylightning.
2019-12-27 17:17:58 +01:00

248 lines
7.6 KiB
Python
Executable File

#!/usr/bin/env python3
from pyln.client import Plugin
import json
import psutil
import subprocess
import threading
import time
import os
try:
# C-lightning v0.7.2
plugin = Plugin(dynamic=False)
except:
plugin = Plugin()
class ChildPlugin(object):
def __init__(self, path, plugin):
self.path = path
self.plugin = plugin
self.status = 'stopped'
self.proc = None
self.iolock = threading.Lock()
self.decoder = json.JSONDecoder()
self.manifest = None
self.init = None
self.reader = None
def watch(self):
last = os.path.getmtime(self.path)
while True:
time.sleep(1)
now = os.path.getmtime(self.path)
if last != now:
print("Detected a change in the child plugin, restarting...")
last = now
try:
self.restart()
except:
self.plugin.log("Failed to start plugin, will wait for next change and try again.", level='warn')
def handle_init(self, request):
"""Lightningd has sent us its first init message, clean and forward.
"""
params = request.params.copy()
# These may have been added by the plugin framework and we won't be
# able to serialize them when forwarding, so delete them.
for key in ['plugin', 'request']:
if key in params:
del params[key]
self.init = {
'jsonrpc': '2.0',
'method': request.method,
'params': params,
'id': request.id,
}
print("Forwarding", self.init)
# Now remove any options that we registered on behalf of the child
# plugin. It'd not understand them if we forward them.
opts = self.init['params']['options']
self.init['params']['options'] = {k: v for k, v in opts.items() if not k.startswith('autoreload')}
plugin.child.send(self.init)
print("Sent init to child plugin")
plugin.child.passthru()
def _readobj(self, sock):
buff=b''
while True:
try:
b = sock.readline()
buff += b
if len(b) == 0:
return None
if b'}\n' not in buff:
continue
# Convert late to UTF-8 so glyphs split across recvs do not
# impact us
buff = buff.decode("UTF-8")
objs, len_used = self.decoder.raw_decode(buff)
buff = buff[len_used:].lstrip().encode("UTF-8")
return objs
except ValueError:
# Probably didn't read enough
buff = buff.lstrip().encode("UTF-8")
def start(self):
assert(self.status == 'stopped')
try:
self.proc = subprocess.Popen([self.path], stdout=subprocess.PIPE, stdin=subprocess.PIPE)
self.status = 'started'
self.getmanifest()
return True
except Exception as e:
self.plugin.log(e, level='warn')
return False
def stop(self):
assert(self.status == 'started')
self.proc.kill()
self.proc.wait()
reader = self.reader
if reader:
reader.join()
self.status = 'stopped'
def restart(self):
print('Restarting child plugin')
self.stop()
self.start()
plugin.child.send(self.init)
print("Sent init to child plugin")
plugin.child.passthru()
def getmanifest(self):
assert(self.status == 'started')
self.send({'jsonrpc': '2.0', 'id': 0, 'method': 'getmanifest', 'params': []})
while True:
msg = self._readobj(self.proc.stdout)
if msg is None:
print("Child plugin does not seem to be sending valid JSON: {}".format(buff.strip()))
self.stop()
raise ValueError()
if 'id' in msg and msg['id'] == 0:
self.manifest = msg['result']
break
self.plugin._write_locked(msg)
return self.manifest
def passthru(self):
# First read the init reply, and then we can switch to passthru
while True:
msg = self._readobj(self.proc.stdout)
if 'id' in msg and msg['id'] == self.init['id']:
break
self.plugin._write_locked(msg)
def read_loop():
while True:
line = self.proc.stdout.readline()
if line == b'':
break
self.plugin.stdout.buffer.write(line)
self.plugin.stdout.flush()
self.reader = None
print("Child plugin exited")
self.reader = threading.Thread(target=read_loop)
self.reader.daemon = True
self.reader.start()
def send(self, msg):
self.proc.stdin.write(json.dumps(msg).encode('UTF-8'))
self.proc.stdin.write(b'\n\n')
self.proc.stdin.flush()
def proxy_method(self, request, *args, **kwargs):
raw = {
'jsonrpc': '2.0',
'method': request.method,
'params': request.params,
'id': request.id,
}
self.send(raw)
def proxy_subscription(self, request, *args, **kwargs):
raw = {
'jsonrpc': '2.0',
'method': request.method,
'params': request.params,
}
self.send(raw)
@plugin.init()
def init(options, configuration, plugin, request):
if options['autoreload-plugin'] in ['null', None]:
print("Cannot run the autoreload plugin on its own, please specify --autoreload-plugin")
plugin.rpc.stop()
return
watch_thread = threading.Thread(target=plugin.child.watch)
watch_thread.daemon = True
watch_thread.start()
plugin.child.handle_init(request)
def inject_manifest(plugin, manifest):
"""Once we have the manifest from the child plugin, inject it into our own.
"""
for opt in manifest.get("options", []):
plugin.add_option(opt['name'], opt['default'], opt['description'])
for m in manifest.get("rpcmethods", []):
plugin.add_method(m['name'], plugin.child.proxy_method, background=True)
for s in manifest.get("subscriptions", []):
plugin.add_subscription(s, plugin.child.proxy_subscription)
for h in manifest.get("hooks", []):
plugin.add_hook(h, plugin.child.proxy_method, background=True)
@plugin.method('autoreload-restart')
def restart(plugin):
"""Manually triggers a restart of the plugin controlled by autoreload.
"""
child = plugin.child
child.restart()
# We can't rely on @plugin.init to tell us the plugin we need to watch and
# reload since we need to start it to pass through its manifest before we get
# any cli options. So we're doomed to get our parent cmdline and parse out the
# argument by hand.
parent = psutil.Process().parent()
cmdline = parent.cmdline()
plugin.path = None
prefix = '--autoreload-plugin='
for c in cmdline:
if c.startswith(prefix):
plugin.path = c[len(prefix):]
break
if plugin.path:
plugin.child = ChildPlugin(plugin.path, plugin)
# If we can't start on the first attempt we can't inject into the
# manifest, no point in continuing.
if not plugin.child.start():
raise Exception("Could not start the plugin under development, can't continue")
inject_manifest(plugin, plugin.child.manifest)
# Now we can run the actual plugin
plugin.add_option("autoreload-plugin", None, "Path to the plugin that we should be watching and reloading.")
plugin.run()