mirror of
https://github.com/aljazceru/lightning.git
synced 2025-12-19 23:24:27 +01:00
pyln: Plugin methods and hooks refuse to set results twice
We had a couple of instances where a plugin would be killed by `lightningd` because we were returning a result of an exception twice, and it was hard to trace down the logic error in the user plugin that caused that. This patch adds a traceback the first time we return a result/exception, and raise an exception with a stacktrace of the first termination when a second one comes in. This can still terminate the plugin, but the programmer gets a clear indication where the result was set, and can potentially even recover from it. Changelog-Added: pyln: Plugin method and hook requests prevent the plugin developer from accidentally setting the result multiple times, and will raise an exception detailing where the result was first set.
This commit is contained in:
@@ -70,6 +70,7 @@ class Request(dict):
|
||||
self.plugin = plugin
|
||||
self.state = RequestState.PENDING
|
||||
self.id = req_id
|
||||
self.termination_tb: Optional[str] = None
|
||||
|
||||
def getattr(self, key: str) -> Union[Method, Any, int]:
|
||||
if key == "params":
|
||||
@@ -85,21 +86,27 @@ class Request(dict):
|
||||
|
||||
def set_result(self, result: Any) -> None:
|
||||
if self.state != RequestState.PENDING:
|
||||
assert(self.termination_tb is not None)
|
||||
raise ValueError(
|
||||
"Cannot set the result of a request that is not pending, "
|
||||
"current state is {state}".format(state=self.state))
|
||||
"current state is {state}. Request previously terminated at\n"
|
||||
"{tb}".format(state=self.state, tb=self.termination_tb))
|
||||
self.result = result
|
||||
self._write_result({
|
||||
'jsonrpc': '2.0',
|
||||
'id': self.id,
|
||||
'result': self.result
|
||||
})
|
||||
self.state = RequestState.FINISHED
|
||||
self.termination_tb = "".join(traceback.extract_stack().format()[:-1])
|
||||
|
||||
def set_exception(self, exc: Exception) -> None:
|
||||
if self.state != RequestState.PENDING:
|
||||
assert(self.termination_tb is not None)
|
||||
raise ValueError(
|
||||
"Cannot set the exception of a request that is not pending, "
|
||||
"current state is {state}".format(state=self.state))
|
||||
"current state is {state}. Request previously terminated at\n"
|
||||
"{tb}".format(state=self.state, tb=self.termination_tb))
|
||||
self.exc = exc
|
||||
self._write_result({
|
||||
'jsonrpc': '2.0',
|
||||
@@ -112,6 +119,8 @@ class Request(dict):
|
||||
"traceback": traceback.format_exc(),
|
||||
},
|
||||
})
|
||||
self.state = RequestState.FAILED
|
||||
self.termination_tb = "".join(traceback.extract_stack().format()[:-1])
|
||||
|
||||
def _write_result(self, result: dict) -> None:
|
||||
self.plugin._write_locked(result)
|
||||
|
||||
@@ -362,3 +362,43 @@ def test_argument_coercion():
|
||||
|
||||
ba = p._bind_pos(test1, ["100msat"], None)
|
||||
test1(*ba.args, **ba.kwargs)
|
||||
|
||||
|
||||
def test_duplicate_result():
|
||||
p = Plugin(autopatch=False)
|
||||
|
||||
def test1(request):
|
||||
request.set_result(1) # MARKER1
|
||||
request.set_result(1)
|
||||
|
||||
req = Request(p, req_id=1, method="test1", params=[])
|
||||
ba = p._bind_kwargs(test1, {}, req)
|
||||
with pytest.raises(ValueError, match=r'current state is RequestState\.FINISHED(.*\n.*)*MARKER1'):
|
||||
test1(*ba.args)
|
||||
|
||||
def test2(request):
|
||||
request.set_exception(1) # MARKER2
|
||||
request.set_exception(1)
|
||||
|
||||
req = Request(p, req_id=2, method="test2", params=[])
|
||||
ba = p._bind_kwargs(test2, {}, req)
|
||||
with pytest.raises(ValueError, match=r'current state is RequestState\.FAILED(.*\n*.*)*MARKER2'):
|
||||
test2(*ba.args)
|
||||
|
||||
def test3(request):
|
||||
request.set_exception(1) # MARKER3
|
||||
request.set_result(1)
|
||||
|
||||
req = Request(p, req_id=3, method="test3", params=[])
|
||||
ba = p._bind_kwargs(test3, {}, req)
|
||||
with pytest.raises(ValueError, match=r'current state is RequestState\.FAILED(.*\n*.*)*MARKER3'):
|
||||
test3(*ba.args)
|
||||
|
||||
def test4(request):
|
||||
request.set_result(1) # MARKER4
|
||||
request.set_exception(1)
|
||||
|
||||
req = Request(p, req_id=4, method="test4", params=[])
|
||||
ba = p._bind_kwargs(test4, {}, req)
|
||||
with pytest.raises(ValueError, match=r'current state is RequestState\.FINISHED(.*\n*.*)*MARKER4'):
|
||||
test4(*ba.args)
|
||||
|
||||
Reference in New Issue
Block a user