mirror of
https://github.com/ElementsProject/lightning.git
synced 2025-03-02 18:35:00 +01:00
pyln: Work around the socket path length on Linux OSs
Some Linux OSs impose a length limit on the path a Unix socket may have. This is not an issue in `lightningd` since we `chdir()` into that directory before opening the socket, however in pyln this became a problem for some tests, since we use absolute paths in the testing framework. It's also a rather strange quirk to expose to users. This patch introduces a `UnixSocket` abstraction that attempts to work around these limitations by aliasing the directory containing the socket into `/proc/self/fd` and then connecting using that alias. It was inspired by Open vSwitch code here https://github.com/openvswitch/ovs/blob/master/python/ovs/socket_util.py Signed-off-by: Christian Decker <@cdecker>
This commit is contained in:
parent
de619b1754
commit
8cad3ffeac
2 changed files with 92 additions and 8 deletions
|
@ -1,9 +1,10 @@
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import socket
|
|
||||||
import warnings
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from math import floor, log10
|
from math import floor, log10
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
|
||||||
class RpcError(ValueError):
|
class RpcError(ValueError):
|
||||||
|
@ -159,6 +160,73 @@ class Millisatoshi:
|
||||||
return Millisatoshi(int(self) + int(other))
|
return Millisatoshi(int(self) + int(other))
|
||||||
|
|
||||||
|
|
||||||
|
class UnixSocket(object):
|
||||||
|
"""A wrapper for socket.socket that is specialized to unix sockets.
|
||||||
|
|
||||||
|
Some OS implementations impose restrictions on the Unix sockets.
|
||||||
|
|
||||||
|
- On linux OSs the socket path must be shorter than the in-kernel buffer
|
||||||
|
size (somewhere around 100 bytes), thus long paths may end up failing
|
||||||
|
the `socket.connect` call.
|
||||||
|
|
||||||
|
This is a small wrapper that tries to work around these limitations.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, path):
|
||||||
|
self.path = path
|
||||||
|
self.sock = None
|
||||||
|
self.connect()
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
try:
|
||||||
|
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
return self.sock.connect(self.path)
|
||||||
|
except OSError as e:
|
||||||
|
self.sock.close()
|
||||||
|
|
||||||
|
if (e.args[0] == "AF_UNIX path too long" and os.uname()[0] == "Linux"):
|
||||||
|
# If this is a Linux system we may be able to work around this
|
||||||
|
# issue by opening our directory and using `/proc/self/fd/` to
|
||||||
|
# get a short alias for the socket file.
|
||||||
|
#
|
||||||
|
# This was heavily inspired by the Open vSwitch code see here:
|
||||||
|
# https://github.com/openvswitch/ovs/blob/master/python/ovs/socket_util.py
|
||||||
|
|
||||||
|
dirname = os.path.dirname(self.path)
|
||||||
|
basename = os.path.basename(self.path)
|
||||||
|
|
||||||
|
# Open an fd to our home directory, that we can then find
|
||||||
|
# through `/proc/self/fd` and access the contents.
|
||||||
|
dirfd = os.open(dirname, os.O_DIRECTORY | os.O_RDONLY)
|
||||||
|
short_path = "/proc/self/fd/%d/%s" % (dirfd, basename)
|
||||||
|
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
return self.sock.connect(short_path)
|
||||||
|
else:
|
||||||
|
# There is no good way to recover from this.
|
||||||
|
raise
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self.sock is not None:
|
||||||
|
self.sock.close()
|
||||||
|
self.sock = None
|
||||||
|
|
||||||
|
def sendall(self, b):
|
||||||
|
if self.sock is None:
|
||||||
|
raise socket.error("not connected")
|
||||||
|
|
||||||
|
self.sock.sendall(b)
|
||||||
|
|
||||||
|
def recv(self, length):
|
||||||
|
if self.sock is None:
|
||||||
|
raise socket.error("not connected")
|
||||||
|
|
||||||
|
return self.sock.recv(length)
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
|
||||||
class UnixDomainSocketRpc(object):
|
class UnixDomainSocketRpc(object):
|
||||||
def __init__(self, socket_path, executor=None, logger=logging, encoder_cls=json.JSONEncoder, decoder=json.JSONDecoder()):
|
def __init__(self, socket_path, executor=None, logger=logging, encoder_cls=json.JSONEncoder, decoder=json.JSONDecoder()):
|
||||||
self.socket_path = socket_path
|
self.socket_path = socket_path
|
||||||
|
@ -215,8 +283,7 @@ class UnixDomainSocketRpc(object):
|
||||||
payload = {k: v for k, v in payload.items() if v is not None}
|
payload = {k: v for k, v in payload.items() if v is not None}
|
||||||
|
|
||||||
# FIXME: we open a new socket for every readobj call...
|
# FIXME: we open a new socket for every readobj call...
|
||||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
sock = UnixSocket(self.socket_path)
|
||||||
sock.connect(self.socket_path)
|
|
||||||
self._writeobj(sock, {
|
self._writeobj(sock, {
|
||||||
"method": method,
|
"method": method,
|
||||||
"params": payload,
|
"params": payload,
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
from bitcoin.rpc import RawProxy
|
from bitcoin.rpc import RawProxy
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from fixtures import * # noqa: F401,F403
|
from fixtures import * # noqa: F401,F403
|
||||||
from fixtures import TEST_NETWORK
|
from fixtures import LightningNode, TEST_NETWORK
|
||||||
from flaky import flaky # noqa: F401
|
from flaky import flaky # noqa: F401
|
||||||
from lightning import RpcError
|
from lightning import RpcError
|
||||||
from threading import Event
|
from threading import Event
|
||||||
from utils import DEVELOPER, TIMEOUT, VALGRIND, sync_blockheight, only_one, wait_for, TailableProc, env
|
from pyln.testing.utils import DEVELOPER, TIMEOUT, VALGRIND, sync_blockheight, only_one, wait_for, TailableProc, env
|
||||||
from ephemeral_port_reserve import reserve
|
from ephemeral_port_reserve import reserve
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
@ -2002,3 +2002,20 @@ def test_unicode_rpc(node_factory):
|
||||||
assert(len(invoices) == 1)
|
assert(len(invoices) == 1)
|
||||||
assert(invoices[0]['description'] == desc)
|
assert(invoices[0]['description'] == desc)
|
||||||
assert(invoices[0]['label'] == desc)
|
assert(invoices[0]['label'] == desc)
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipIf(VALGRIND, "Testing pyln doesn't exercise anything interesting in the c code.")
|
||||||
|
def test_unix_socket_path_length(node_factory, bitcoind, directory, executor, db_provider, test_base_dir):
|
||||||
|
lightning_dir = os.path.join(directory, "anode" + "far" * 30 + "away")
|
||||||
|
os.makedirs(lightning_dir)
|
||||||
|
db = db_provider.get_db(lightning_dir, "test_unix_socket_path_length", 1)
|
||||||
|
|
||||||
|
l1 = LightningNode(1, lightning_dir, bitcoind, executor, db=db, port=node_factory.get_next_port())
|
||||||
|
|
||||||
|
# `LightningNode.start()` internally calls `LightningRpc.getinfo()` which
|
||||||
|
# exercises the socket logic, and raises an issue if it fails.
|
||||||
|
l1.start()
|
||||||
|
|
||||||
|
# Let's just call it again to make sure it really works.
|
||||||
|
l1.rpc.listconfigs()
|
||||||
|
l1.stop()
|
||||||
|
|
Loading…
Add table
Reference in a new issue