Merge pull request #1306 from meejah/4036.option-for-closing-stdin

Add an option for not exiting `tahoe run` when stdin closes.

Fixes: ticket:4036
This commit is contained in:
Jean-Paul Calderone 2023-06-20 08:48:53 -04:00 committed by GitHub
commit 2304f77dfd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 109 additions and 13 deletions

View File

@ -0,0 +1 @@
tahoe run now accepts --allow-stdin-close to mean "keep running if stdin closes"

View File

@ -104,6 +104,11 @@ class RunOptions(BasedirOptions):
" [default: %s]" % quote_local_unicode_path(_default_nodedir)),
]
optFlags = [
("allow-stdin-close", None,
'Do not exit when stdin closes ("tahoe run" otherwise will exit).'),
]
def parseArgs(self, basedir=None, *twistd_args):
# This can't handle e.g. 'tahoe run --reactor=foo', since
# '--reactor=foo' looks like an option to the tahoe subcommand, not to
@ -156,6 +161,7 @@ class DaemonizeTheRealService(Service, HookMixin):
"running": None,
}
self.stderr = options.parent.stderr
self._close_on_stdin_close = False if options["allow-stdin-close"] else True
def startService(self):
@ -199,9 +205,11 @@ class DaemonizeTheRealService(Service, HookMixin):
d = service_factory()
def created(srv):
if self.parent is not None:
srv.setServiceParent(self.parent)
# exiting on stdin-closed facilitates cleanup when run
# as a subprocess
if self._close_on_stdin_close:
on_stdin_close(reactor, reactor.stop)
d.addCallback(created)
d.addErrback(handle_config_error)
@ -213,11 +221,13 @@ class DaemonizeTheRealService(Service, HookMixin):
class DaemonizeTahoeNodePlugin(object):
tapname = "tahoenode"
def __init__(self, nodetype, basedir):
def __init__(self, nodetype, basedir, allow_stdin_close):
self.nodetype = nodetype
self.basedir = basedir
self.allow_stdin_close = allow_stdin_close
def makeService(self, so):
so["allow-stdin-close"] = self.allow_stdin_close
return DaemonizeTheRealService(self.nodetype, self.basedir, so)
@ -304,7 +314,9 @@ def run(reactor, config, runApp=twistd.runApp):
print(config, file=err)
print("tahoe %s: usage error from twistd: %s\n" % (config.subcommand_name, ue), file=err)
return 1
twistd_config.loadedPlugins = {"DaemonizeTahoeNode": DaemonizeTahoeNodePlugin(nodetype, basedir)}
twistd_config.loadedPlugins = {
"DaemonizeTahoeNode": DaemonizeTahoeNodePlugin(nodetype, basedir, config["allow-stdin-close"])
}
# our own pid-style file contains PID and process creation time
pidfile = FilePath(get_pidfile(config['basedir']))

View File

@ -1,16 +1,8 @@
"""
Tests for ``allmydata.scripts.tahoe_run``.
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 __future__ import annotations
import re
from six.moves import (
@ -31,6 +23,12 @@ from twisted.python.filepath import (
from twisted.internet.testing import (
MemoryReactor,
)
from twisted.python.failure import (
Failure,
)
from twisted.internet.error import (
ConnectionDone,
)
from twisted.internet.test.modulehelpers import (
AlternateReactor,
)
@ -147,6 +145,91 @@ class DaemonizeTheRealServiceTests(SyncTestCase):
)
class DaemonizeStopTests(SyncTestCase):
"""
Tests relating to stopping the daemon
"""
def setUp(self):
self.nodedir = FilePath(self.mktemp())
self.nodedir.makedirs()
config = ""
self.nodedir.child("tahoe.cfg").setContent(config.encode("ascii"))
self.nodedir.child("tahoe-client.tac").touch()
# arrange to know when reactor.stop() is called
self.reactor = MemoryReactor()
self.stop_calls = []
def record_stop():
self.stop_calls.append(object())
self.reactor.stop = record_stop
super().setUp()
def _make_daemon(self, extra_argv: list[str]) -> DaemonizeTheRealService:
"""
Create the daemonization service.
:param extra_argv: Extra arguments to pass between ``run`` and the
node path.
"""
options = parse_options(["run"] + extra_argv + [self.nodedir.path])
options.stdout = StringIO()
options.stderr = StringIO()
options.stdin = StringIO()
run_options = options.subOptions
return DaemonizeTheRealService(
"client",
self.nodedir.path,
run_options,
)
def _run_daemon(self) -> None:
"""
Simulate starting up the reactor so the daemon plugin can do its
stuff.
"""
# We happen to know that the service uses reactor.callWhenRunning
# to schedule all its work (though I couldn't tell you *why*).
# Make sure those scheduled calls happen.
waiting = self.reactor.whenRunningHooks[:]
del self.reactor.whenRunningHooks[:]
for f, a, k in waiting:
f(*a, **k)
def _close_stdin(self) -> None:
"""
Simulate closing the daemon plugin's stdin.
"""
# there should be a single reader: our StandardIO process
# reader for stdin. Simulate it closing.
for r in self.reactor.getReaders():
r.connectionLost(Failure(ConnectionDone()))
def test_stop_on_stdin_close(self):
"""
We stop when stdin is closed.
"""
with AlternateReactor(self.reactor):
service = self._make_daemon([])
service.startService()
self._run_daemon()
self._close_stdin()
self.assertEqual(len(self.stop_calls), 1)
def test_allow_stdin_close(self):
"""
If --allow-stdin-close is specified then closing stdin doesn't
stop the process
"""
with AlternateReactor(self.reactor):
service = self._make_daemon(["--allow-stdin-close"])
service.startService()
self._run_daemon()
self._close_stdin()
self.assertEqual(self.stop_calls, [])
class RunTests(SyncTestCase):
"""
Tests for ``run``.