mirror of
https://github.com/aljazceru/plugins.git
synced 2025-12-19 14:14:20 +01:00
This updates all plugins *which already have requirements.txt* to pyln-client from pylightning.
248 lines
7.6 KiB
Python
Executable File
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()
|