Merge branch 'master' into 3899.failed-server

This commit is contained in:
meejah 2022-12-02 10:36:53 -07:00
commit 518d6fbbf7
9 changed files with 144 additions and 61 deletions

0
newsfragments/3874.minor Normal file
View File

View File

@ -0,0 +1,5 @@
`tahoe run ...` will now exit when its stdin is closed.
This facilitates subprocess management, specifically cleanup.
When a parent process is running tahoe and exits without time to do "proper" cleanup at least the stdin descriptor will be closed.
Subsequently "tahoe run" notices this and exits.

View File

@ -21,7 +21,11 @@ from twisted.scripts import twistd
from twisted.python import usage from twisted.python import usage
from twisted.python.filepath import FilePath from twisted.python.filepath import FilePath
from twisted.python.reflect import namedAny from twisted.python.reflect import namedAny
from twisted.internet.defer import maybeDeferred from twisted.python.failure import Failure
from twisted.internet.defer import maybeDeferred, Deferred
from twisted.internet.protocol import Protocol
from twisted.internet.stdio import StandardIO
from twisted.internet.error import ReactorNotRunning
from twisted.application.service import Service from twisted.application.service import Service
from allmydata.scripts.default_nodedir import _default_nodedir from allmydata.scripts.default_nodedir import _default_nodedir
@ -155,6 +159,8 @@ class DaemonizeTheRealService(Service, HookMixin):
def startService(self): def startService(self):
from twisted.internet import reactor
def start(): def start():
node_to_instance = { node_to_instance = {
u"client": lambda: maybeDeferred(namedAny("allmydata.client.create_client"), self.basedir), u"client": lambda: maybeDeferred(namedAny("allmydata.client.create_client"), self.basedir),
@ -194,12 +200,14 @@ class DaemonizeTheRealService(Service, HookMixin):
def created(srv): def created(srv):
srv.setServiceParent(self.parent) srv.setServiceParent(self.parent)
# exiting on stdin-closed facilitates cleanup when run
# as a subprocess
on_stdin_close(reactor, reactor.stop)
d.addCallback(created) d.addCallback(created)
d.addErrback(handle_config_error) d.addErrback(handle_config_error)
d.addBoth(self._call_hook, 'running') d.addBoth(self._call_hook, 'running')
return d return d
from twisted.internet import reactor
reactor.callWhenRunning(start) reactor.callWhenRunning(start)
@ -213,6 +221,46 @@ class DaemonizeTahoeNodePlugin(object):
return DaemonizeTheRealService(self.nodetype, self.basedir, so) return DaemonizeTheRealService(self.nodetype, self.basedir, so)
def on_stdin_close(reactor, fn):
"""
Arrange for the function `fn` to run when our stdin closes
"""
when_closed_d = Deferred()
class WhenClosed(Protocol):
"""
Notify a Deferred when our connection is lost .. as this is passed
to twisted's StandardIO class, it is used to detect our parent
going away.
"""
def connectionLost(self, reason):
when_closed_d.callback(None)
def on_close(arg):
try:
fn()
except ReactorNotRunning:
pass
except Exception:
# for our "exit" use-case failures will _mostly_ just be
# ReactorNotRunning (because we're already shutting down
# when our stdin closes) but no matter what "bad thing"
# happens we just want to ignore it .. although other
# errors might be interesting so we'll log those
print(Failure())
return arg
when_closed_d.addBoth(on_close)
# we don't need to do anything with this instance because it gets
# hooked into the reactor and thus remembered .. but we return it
# for Windows testing purposes.
return StandardIO(
proto=WhenClosed(),
reactor=reactor,
)
def run(reactor, config, runApp=twistd.runApp): def run(reactor, config, runApp=twistd.runApp):
""" """
Runs a Tahoe-LAFS node in the foreground. Runs a Tahoe-LAFS node in the foreground.

View File

@ -47,6 +47,9 @@ from twisted.internet.defer import (
inlineCallbacks, inlineCallbacks,
DeferredList, DeferredList,
) )
from twisted.internet.testing import (
MemoryReactorClock,
)
from twisted.python.filepath import FilePath from twisted.python.filepath import FilePath
from allmydata.util import fileutil, pollmixin from allmydata.util import fileutil, pollmixin
from allmydata.util.encodingutil import unicode_to_argv from allmydata.util.encodingutil import unicode_to_argv
@ -60,6 +63,9 @@ import allmydata
from allmydata.scripts.runner import ( from allmydata.scripts.runner import (
parse_options, parse_options,
) )
from allmydata.scripts.tahoe_run import (
on_stdin_close,
)
from .common import ( from .common import (
PIPE, PIPE,
@ -624,6 +630,64 @@ class RunNode(common_util.SignalMixin, unittest.TestCase, pollmixin.PollMixin):
yield client_running yield client_running
def _simulate_windows_stdin_close(stdio):
"""
on Unix we can just close all the readers, correctly "simulating"
a stdin close .. of course, Windows has to be difficult
"""
stdio.writeConnectionLost()
stdio.readConnectionLost()
class OnStdinCloseTests(SyncTestCase):
"""
Tests for on_stdin_close
"""
def test_close_called(self):
"""
our on-close method is called when stdin closes
"""
reactor = MemoryReactorClock()
called = []
def onclose():
called.append(True)
transport = on_stdin_close(reactor, onclose)
self.assertEqual(called, [])
if platform.isWindows():
_simulate_windows_stdin_close(transport)
else:
for reader in reactor.getReaders():
reader.loseConnection()
reactor.advance(1) # ProcessReader does a callLater(0, ..)
self.assertEqual(called, [True])
def test_exception_ignored(self):
"""
An exception from our on-close function is discarded.
"""
reactor = MemoryReactorClock()
called = []
def onclose():
called.append(True)
raise RuntimeError("unexpected error")
transport = on_stdin_close(reactor, onclose)
self.assertEqual(called, [])
if platform.isWindows():
_simulate_windows_stdin_close(transport)
else:
for reader in reactor.getReaders():
reader.loseConnection()
reactor.advance(1) # ProcessReader does a callLater(0, ..)
self.assertEqual(called, [True])
class PidFileLocking(SyncTestCase): class PidFileLocking(SyncTestCase):
""" """
Direct tests for allmydata.util.pid functions Direct tests for allmydata.util.pid functions

View File

@ -9,18 +9,7 @@
""" """
Tests for the allmydata.testing helpers Tests for the allmydata.testing helpers
Ported to Python 3.
""" """
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from future.utils import PY2
if PY2:
from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min # noqa: F401
from twisted.internet.defer import ( from twisted.internet.defer import (
inlineCallbacks, inlineCallbacks,
@ -56,10 +45,12 @@ from testtools.matchers import (
IsInstance, IsInstance,
MatchesStructure, MatchesStructure,
AfterPreprocessing, AfterPreprocessing,
Contains,
) )
from testtools.twistedsupport import ( from testtools.twistedsupport import (
succeeded, succeeded,
) )
from twisted.web.http import GONE
class FakeWebTest(SyncTestCase): class FakeWebTest(SyncTestCase):
@ -144,7 +135,8 @@ class FakeWebTest(SyncTestCase):
def test_download_missing(self): def test_download_missing(self):
""" """
Error if we download a capability that doesn't exist The response to a request to download a capability that doesn't exist
is 410 (GONE).
""" """
http_client = create_tahoe_treq_client() http_client = create_tahoe_treq_client()
@ -157,7 +149,11 @@ class FakeWebTest(SyncTestCase):
resp, resp,
succeeded( succeeded(
MatchesStructure( MatchesStructure(
code=Equals(500) code=Equals(GONE),
content=AfterPreprocessing(
lambda m: m(),
succeeded(Contains(b"No data for")),
),
) )
) )
) )

View File

@ -6,20 +6,12 @@
# This file is part of Tahoe-LAFS. # This file is part of Tahoe-LAFS.
# #
# See the docs/about.rst file for licensing information. # See the docs/about.rst file for licensing information.
"""Test-helpers for clients that use the WebUI.
Ported to Python 3.
""" """
from __future__ import absolute_import Test-helpers for clients that use the WebUI.
from __future__ import division """
from __future__ import print_function
from __future__ import unicode_literals
from future.utils import PY2
if PY2:
from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min # noqa: F401
from __future__ import annotations
import hashlib import hashlib
@ -54,6 +46,7 @@ import allmydata.uri
from allmydata.util import ( from allmydata.util import (
base32, base32,
) )
from ..util.dictutil import BytesKeyDict
__all__ = ( __all__ = (
@ -147,7 +140,7 @@ class _FakeTahoeUriHandler(Resource, object):
isLeaf = True isLeaf = True
data = attr.ib(default=attr.Factory(dict)) data: BytesKeyDict = attr.ib(default=attr.Factory(BytesKeyDict))
capability_generators = attr.ib(default=attr.Factory(dict)) capability_generators = attr.ib(default=attr.Factory(dict))
def _generate_capability(self, kind): def _generate_capability(self, kind):
@ -209,7 +202,7 @@ class _FakeTahoeUriHandler(Resource, object):
capability = None capability = None
for arg, value in uri.query: for arg, value in uri.query:
if arg == u"uri": if arg == u"uri":
capability = value capability = value.encode("utf-8")
# it's legal to use the form "/uri/<capability>" # it's legal to use the form "/uri/<capability>"
if capability is None and request.postpath and request.postpath[0]: if capability is None and request.postpath and request.postpath[0]:
capability = request.postpath[0] capability = request.postpath[0]
@ -221,10 +214,9 @@ class _FakeTahoeUriHandler(Resource, object):
# the user gave us a capability; if our Grid doesn't have any # the user gave us a capability; if our Grid doesn't have any
# data for it, that's an error. # data for it, that's an error.
capability = capability.encode('ascii')
if capability not in self.data: if capability not in self.data:
request.setResponseCode(http.BAD_REQUEST) request.setResponseCode(http.GONE)
return u"No data for '{}'".format(capability.decode('ascii')) return u"No data for '{}'".format(capability.decode('ascii')).encode("utf-8")
return self.data[capability] return self.data[capability]

View File

@ -1,21 +1,6 @@
""" """
Tools to mess with dicts. Tools to mess with dicts.
Ported to Python 3.
""" """
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from future.utils import PY2
if PY2:
# IMPORTANT: We deliberately don't import dict. The issue is that we're
# subclassing dict, so we'd end up exposing Python 3 dict APIs to lots of
# code that doesn't support it.
from builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, list, object, range, str, max, min # noqa: F401
from six import ensure_str
class DictOfSets(dict): class DictOfSets(dict):
def add(self, key, value): def add(self, key, value):
@ -104,7 +89,7 @@ def _make_enforcing_override(K, method_name):
raise TypeError("{} must be of type {}".format( raise TypeError("{} must be of type {}".format(
repr(key), self.KEY_TYPE)) repr(key), self.KEY_TYPE))
return getattr(dict, method_name)(self, key, *args, **kwargs) return getattr(dict, method_name)(self, key, *args, **kwargs)
f.__name__ = ensure_str(method_name) f.__name__ = method_name
setattr(K, method_name, f) setattr(K, method_name, f)
for _method_name in ["__setitem__", "__getitem__", "setdefault", "get", for _method_name in ["__setitem__", "__getitem__", "setdefault", "get",
@ -113,18 +98,13 @@ for _method_name in ["__setitem__", "__getitem__", "setdefault", "get",
del _method_name del _method_name
if PY2: class BytesKeyDict(_TypedKeyDict):
# No need for enforcement, can use either bytes or unicode as keys and it's
# fine.
BytesKeyDict = UnicodeKeyDict = dict
else:
class BytesKeyDict(_TypedKeyDict):
"""Keys should be bytes.""" """Keys should be bytes."""
KEY_TYPE = bytes KEY_TYPE = bytes
class UnicodeKeyDict(_TypedKeyDict): class UnicodeKeyDict(_TypedKeyDict):
"""Keys should be unicode strings.""" """Keys should be unicode strings."""
KEY_TYPE = str KEY_TYPE = str

View File

@ -86,7 +86,6 @@ commands =
coverage: python -b -m coverage run -m twisted.trial {env:TAHOE_LAFS_TRIAL_ARGS:--rterrors --reporter=timing} {posargs:{env:TEST_SUITE}} coverage: python -b -m coverage run -m twisted.trial {env:TAHOE_LAFS_TRIAL_ARGS:--rterrors --reporter=timing} {posargs:{env:TEST_SUITE}}
coverage: coverage combine coverage: coverage combine
coverage: coverage xml coverage: coverage xml
coverage: coverage report
[testenv:integration] [testenv:integration]
basepython = python3 basepython = python3
@ -99,7 +98,6 @@ commands =
# NOTE: 'run with "py.test --keep-tempdir -s -v integration/" to debug failures' # NOTE: 'run with "py.test --keep-tempdir -s -v integration/" to debug failures'
py.test --timeout=1800 --coverage -s -v {posargs:integration} py.test --timeout=1800 --coverage -s -v {posargs:integration}
coverage combine coverage combine
coverage report
[testenv:codechecks] [testenv:codechecks]