PyroLab 0.5.0rc2__tar.gz → 0.5.0rc3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyrolab-0.5.0rc2/pyrolab.egg-info → pyrolab-0.5.0rc3}/PKG-INFO +1 -1
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyproject.toml +1 -1
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/cli.py +2 -4
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/logs.py +34 -2
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/manager.py +18 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/pyrolabd.py +22 -2
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/utils.py +35 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3/pyrolab.egg-info}/PKG-INFO +1 -1
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/tests/test_cli.py +21 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/tests/test_logs.py +55 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/tests/test_pyrolabd.py +35 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/tests/test_registration.py +37 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/tests/test_utils.py +24 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/LICENSE +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/MANIFEST.in +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/NOTICE.txt +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/README.md +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/__init__.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/api.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/configure.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/data/wordlist.txt +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/drivers/VISAResourceExtensions.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/drivers/__init__.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/drivers/arduino/__init__.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/drivers/arduino/arduino.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/drivers/cameras/__init__.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/drivers/cameras/amcam.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/drivers/cameras/sciTSI.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/drivers/cameras/thorcam.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/drivers/cameras/uc480.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/drivers/fpgas/__init__.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/drivers/fpgas/analogdiscovery.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/drivers/function_generators/__init__.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/drivers/function_generators/smcv100b.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/drivers/lasers/__init__.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/drivers/lasers/ppcl55x.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/drivers/lasers/sachertec.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/drivers/lasers/tsl550.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/drivers/motion/KMTS25E.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/drivers/motion/__init__.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/drivers/motion/kinesis/__init__.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/drivers/motion/kinesis/bpc303.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/drivers/motion/kinesis/kdc101.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/drivers/motion/max31x.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/drivers/motion/prm1z8.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/drivers/motion/z8xx.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/drivers/sample.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/drivers/scopes/__init__.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/drivers/scopes/rohdeschwarz.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/drivers/smu/__init__.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/drivers/smu/keysight.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/locations.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/nameserver.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/server.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/service.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab/updates.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab.egg-info/SOURCES.txt +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab.egg-info/dependency_links.txt +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab.egg-info/entry_points.txt +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab.egg-info/requires.txt +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/pyrolab.egg-info/top_level.txt +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/setup.cfg +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/tests/test_configure.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/tests/test_imports.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/tests/test_lifecycle.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/tests/test_locations.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/tests/test_manager.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/tests/test_nameserver.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/tests/test_server.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/tests/test_service.py +0 -0
- {pyrolab-0.5.0rc2 → pyrolab-0.5.0rc3}/tests/test_updates.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyrolab
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.0rc3
|
|
4
4
|
Summary: A framework for using remote lab instruments as local resources built on Pyro5.
|
|
5
5
|
Author-email: Sequoia Ploeg <sequoiap4@gmail.com>, BYU CamachoLab <camacho@byu.edu>
|
|
6
6
|
Maintainer-email: Sequoia Ploeg <sequoiap4@gmail.com>, BYU CamachoLab <camacho@byu.edu>
|
|
@@ -796,10 +796,8 @@ def ns_list(
|
|
|
796
796
|
services = ns.list(return_metadata=True)
|
|
797
797
|
|
|
798
798
|
listing = []
|
|
799
|
-
for
|
|
800
|
-
name
|
|
801
|
-
uri, description = v
|
|
802
|
-
listing.append([name, uri, ": ".join(description)])
|
|
799
|
+
for name, (uri, description) in sorted(services.items()):
|
|
800
|
+
listing.append([name, uri, ": ".join(sorted(description))])
|
|
803
801
|
|
|
804
802
|
typer.echo(tabulate(listing, headers=["NAME", "URI", "DESCRIPTION"]))
|
|
805
803
|
|
|
@@ -29,6 +29,8 @@ import json
|
|
|
29
29
|
import logging
|
|
30
30
|
import logging.handlers
|
|
31
31
|
import os
|
|
32
|
+
import signal
|
|
33
|
+
import sys
|
|
32
34
|
import threading
|
|
33
35
|
import warnings
|
|
34
36
|
from datetime import datetime
|
|
@@ -117,8 +119,6 @@ def configure_process() -> None:
|
|
|
117
119
|
(unless ``PYROLAB_HUSH_DEPRECATION`` is set). Never called on import: a
|
|
118
120
|
library must not change these for the application that imports it.
|
|
119
121
|
"""
|
|
120
|
-
import sys
|
|
121
|
-
|
|
122
122
|
import Pyro5.errors
|
|
123
123
|
|
|
124
124
|
sys.excepthook = Pyro5.errors.excepthook
|
|
@@ -219,6 +219,38 @@ class PipeHandler(logging.Handler):
|
|
|
219
219
|
return data
|
|
220
220
|
|
|
221
221
|
|
|
222
|
+
def line_buffer_output() -> None:
|
|
223
|
+
"""
|
|
224
|
+
Write this process's stdout and stderr out line by line.
|
|
225
|
+
|
|
226
|
+
The daemon and its child processes have their output redirected to
|
|
227
|
+
``pyrolabd_output.log``, and Python block-buffers output to a file: a
|
|
228
|
+
driver's ``print()`` would only appear once 8 KB had built up, and would be
|
|
229
|
+
lost if the process were killed. Drivers often report problems that way.
|
|
230
|
+
"""
|
|
231
|
+
for stream in (sys.stdout, sys.stderr):
|
|
232
|
+
reconfigure = getattr(stream, "reconfigure", None) # None under pythonw
|
|
233
|
+
if reconfigure is not None:
|
|
234
|
+
try:
|
|
235
|
+
reconfigure(line_buffering=True)
|
|
236
|
+
except (ValueError, OSError): # e.g. a stream that's already closed
|
|
237
|
+
pass
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def on_sigterm(handler: Callable[[int, object], None]) -> None:
|
|
241
|
+
"""
|
|
242
|
+
Call ``handler`` when this process is asked to stop with SIGTERM.
|
|
243
|
+
|
|
244
|
+
Without a handler, Python exits on SIGTERM at once, skipping all cleanup.
|
|
245
|
+
That's what ``systemctl stop`` sends (to every process of the service), and
|
|
246
|
+
what ``multiprocessing.Process.terminate()`` sends on POSIX. Signal
|
|
247
|
+
handlers can only be installed from the main thread; elsewhere (as when a
|
|
248
|
+
runner's ``run()`` is called in a thread by the tests) this does nothing.
|
|
249
|
+
"""
|
|
250
|
+
if threading.current_thread() is threading.main_thread():
|
|
251
|
+
signal.signal(signal.SIGTERM, handler)
|
|
252
|
+
|
|
253
|
+
|
|
222
254
|
def log_to_pipe(conn) -> None:
|
|
223
255
|
"""
|
|
224
256
|
In a child process: send all logging to the parent over ``conn``.
|
|
@@ -276,6 +276,8 @@ class NameServerRunner(multiprocessing.Process):
|
|
|
276
276
|
goes there (see :py:mod:`pyrolab.logs`).
|
|
277
277
|
"""
|
|
278
278
|
|
|
279
|
+
KIND = "nameserver"
|
|
280
|
+
|
|
279
281
|
def __init__(
|
|
280
282
|
self,
|
|
281
283
|
*args,
|
|
@@ -336,6 +338,11 @@ class NameServerRunner(multiprocessing.Process):
|
|
|
336
338
|
"""
|
|
337
339
|
return not self.KILL_SIGNAL
|
|
338
340
|
|
|
341
|
+
def _stop_on_sigterm(self, signum, frame) -> None:
|
|
342
|
+
"""Treat SIGTERM like the stop message: shut down cleanly."""
|
|
343
|
+
log.info("SIGTERM received; stopping %s '%s'", self.KIND, self.name)
|
|
344
|
+
self.KILL_SIGNAL = True
|
|
345
|
+
|
|
339
346
|
def run(self) -> None:
|
|
340
347
|
"""
|
|
341
348
|
Creates and runs the child process.
|
|
@@ -345,6 +352,8 @@ class NameServerRunner(multiprocessing.Process):
|
|
|
345
352
|
"""
|
|
346
353
|
if self.log_conn is not None:
|
|
347
354
|
logs.log_to_pipe(self.log_conn)
|
|
355
|
+
logs.line_buffer_output()
|
|
356
|
+
logs.on_sigterm(self._stop_on_sigterm)
|
|
348
357
|
log.info("Starting nameserver '%s'", self.name)
|
|
349
358
|
try:
|
|
350
359
|
self.nsconfig.update_pyro_config()
|
|
@@ -398,6 +407,8 @@ class DaemonRunner(multiprocessing.Process):
|
|
|
398
407
|
goes there (see :py:mod:`pyrolab.logs`).
|
|
399
408
|
"""
|
|
400
409
|
|
|
410
|
+
KIND = "daemon"
|
|
411
|
+
|
|
401
412
|
def __init__(
|
|
402
413
|
self,
|
|
403
414
|
*args,
|
|
@@ -497,6 +508,11 @@ class DaemonRunner(multiprocessing.Process):
|
|
|
497
508
|
"""
|
|
498
509
|
return not self.KILL_SIGNAL
|
|
499
510
|
|
|
511
|
+
def _stop_on_sigterm(self, signum, frame) -> None:
|
|
512
|
+
"""Treat SIGTERM like the stop message: shut down cleanly."""
|
|
513
|
+
log.info("SIGTERM received; stopping %s '%s'", self.KIND, self.name)
|
|
514
|
+
self.KILL_SIGNAL = True
|
|
515
|
+
|
|
500
516
|
def plan_registrations(self, uris: Dict[str, URI]) -> Registrations:
|
|
501
517
|
"""Queue every nameserver registration this daemon should make."""
|
|
502
518
|
config = PyroLabConfiguration.from_file(RUNTIME_CONFIG)
|
|
@@ -526,6 +542,8 @@ class DaemonRunner(multiprocessing.Process):
|
|
|
526
542
|
"""
|
|
527
543
|
if self.log_conn is not None:
|
|
528
544
|
logs.log_to_pipe(self.log_conn)
|
|
545
|
+
logs.line_buffer_output()
|
|
546
|
+
logs.on_sigterm(self._stop_on_sigterm)
|
|
529
547
|
log.info("Starting daemon '%s'", self.name)
|
|
530
548
|
stop_registering = threading.Event()
|
|
531
549
|
registrar = None
|
|
@@ -12,6 +12,7 @@ Submodule defining the background PyroLab daemon.
|
|
|
12
12
|
import logging
|
|
13
13
|
import os
|
|
14
14
|
import shutil
|
|
15
|
+
import signal
|
|
15
16
|
import sys
|
|
16
17
|
import threading
|
|
17
18
|
from typing import NamedTuple, Optional
|
|
@@ -34,7 +35,7 @@ from pyrolab import (
|
|
|
34
35
|
from pyrolab.configure import GlobalConfiguration, describe_config_error
|
|
35
36
|
from pyrolab.locations import migrate_legacy_files
|
|
36
37
|
from pyrolab.manager import ProcessManager
|
|
37
|
-
from pyrolab.utils import atomic_write_text, pid_is_running
|
|
38
|
+
from pyrolab.utils import atomic_write_text, names_this_machine, pid_is_running
|
|
38
39
|
|
|
39
40
|
log = logging.getLogger("pyrolab.pyrolabd")
|
|
40
41
|
|
|
@@ -220,8 +221,12 @@ class PyroLabDaemon:
|
|
|
220
221
|
log.debug("Daemon process listing requested.")
|
|
221
222
|
|
|
222
223
|
listing = []
|
|
223
|
-
for ns in self.gconfig.get_config().nameservers.
|
|
224
|
+
for ns, nsconfig in self.gconfig.get_config().nameservers.items():
|
|
224
225
|
info = self.manager.get_nameserver_process_info(ns)
|
|
226
|
+
if info["status"] == "Stopped" and not names_this_machine(nsconfig.host):
|
|
227
|
+
# Configured so services can register with it, but it runs on
|
|
228
|
+
# another machine; "Stopped" would suggest something's wrong.
|
|
229
|
+
info["status"] = "Remote"
|
|
225
230
|
listing.append(NameServerInfo(name=ns, **_short_error(info)))
|
|
226
231
|
nsstring = tabulate(
|
|
227
232
|
listing, headers=["NAMESERVER", "CREATED", "STATUS", "URI", "LAST ERROR"]
|
|
@@ -401,6 +406,8 @@ def main(port: int = 0) -> int:
|
|
|
401
406
|
return 1
|
|
402
407
|
|
|
403
408
|
handler = logs.start_file_logging(PYROLAB_LOGFILE)
|
|
409
|
+
logs.line_buffer_output()
|
|
410
|
+
logs.on_sigterm(_exit_on_sigterm)
|
|
404
411
|
try:
|
|
405
412
|
for copied in migrate_legacy_files(
|
|
406
413
|
LEGACY_DATA_DIR,
|
|
@@ -414,6 +421,19 @@ def main(port: int = 0) -> int:
|
|
|
414
421
|
logs.stop_file_logging(handler)
|
|
415
422
|
|
|
416
423
|
|
|
424
|
+
def _exit_on_sigterm(signum, frame) -> None:
|
|
425
|
+
"""
|
|
426
|
+
Shut down cleanly on SIGTERM (``systemctl stop``) instead of dying at once.
|
|
427
|
+
|
|
428
|
+
Raising SystemExit unwinds out of the request loop, so ``_serve``'s
|
|
429
|
+
``finally`` stops every nameserver and daemon, removes the lockfile and
|
|
430
|
+
the runtime config, and the process exits with status 0.
|
|
431
|
+
"""
|
|
432
|
+
log.info("SIGTERM received; shutting down")
|
|
433
|
+
signal.signal(signal.SIGTERM, signal.SIG_IGN) # don't interrupt the cleanup
|
|
434
|
+
raise SystemExit(0)
|
|
435
|
+
|
|
436
|
+
|
|
417
437
|
def _serve(port: int) -> int:
|
|
418
438
|
daemon = None
|
|
419
439
|
published = False
|
|
@@ -9,11 +9,13 @@ Utils
|
|
|
9
9
|
Convenience functions for working with the pyrolab package.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
+
import ipaddress
|
|
12
13
|
import logging
|
|
13
14
|
import os
|
|
14
15
|
import secrets
|
|
15
16
|
import socket
|
|
16
17
|
import sys
|
|
18
|
+
from typing import Optional
|
|
17
19
|
|
|
18
20
|
try:
|
|
19
21
|
import importlib.resources as pkg_resources
|
|
@@ -74,6 +76,39 @@ def get_ip() -> str:
|
|
|
74
76
|
return "127.0.0.1"
|
|
75
77
|
|
|
76
78
|
|
|
79
|
+
def names_this_machine(host: Optional[str]) -> bool:
|
|
80
|
+
"""
|
|
81
|
+
Whether a configured ``host`` refers to this machine.
|
|
82
|
+
|
|
83
|
+
Decided without DNS, so it never waits on the network: wildcard and
|
|
84
|
+
loopback addresses, ``public``, this machine's hostname (or its first
|
|
85
|
+
label, so ``lab.example.edu`` matches a machine called ``lab``), and, for a
|
|
86
|
+
literal IP address, this machine's address. Anything else is taken to be
|
|
87
|
+
another machine.
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
host : str, optional
|
|
92
|
+
A host as written in the configuration.
|
|
93
|
+
|
|
94
|
+
Returns
|
|
95
|
+
-------
|
|
96
|
+
bool
|
|
97
|
+
False if ``host`` names another machine.
|
|
98
|
+
"""
|
|
99
|
+
if not host:
|
|
100
|
+
return True
|
|
101
|
+
host = host.strip().lower().strip("[]")
|
|
102
|
+
if host in ("localhost", "public", "0.0.0.0", "::", "::1"):
|
|
103
|
+
return True
|
|
104
|
+
try:
|
|
105
|
+
address = ipaddress.ip_address(host)
|
|
106
|
+
except ValueError:
|
|
107
|
+
hostname = socket.gethostname().lower()
|
|
108
|
+
return host == hostname or host.split(".")[0] == hostname.split(".")[0]
|
|
109
|
+
return address.is_loopback or address.is_unspecified or host == get_ip()
|
|
110
|
+
|
|
111
|
+
|
|
77
112
|
def generate_random_name(count: int = 3) -> str:
|
|
78
113
|
"""
|
|
79
114
|
Concatenates ``count`` random words as a hyphenated string.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyrolab
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.0rc3
|
|
4
4
|
Summary: A framework for using remote lab instruments as local resources built on Pyro5.
|
|
5
5
|
Author-email: Sequoia Ploeg <sequoiap4@gmail.com>, BYU CamachoLab <camacho@byu.edu>
|
|
6
6
|
Maintainer-email: Sequoia Ploeg <sequoiap4@gmail.com>, BYU CamachoLab <camacho@byu.edu>
|
|
@@ -838,3 +838,24 @@ def test_spawned_daemon_output_goes_to_log_file(data_dir, monkeypatch):
|
|
|
838
838
|
assert seen["stderr"] is cli.subprocess.STDOUT
|
|
839
839
|
assert seen["stdout"].name == str(data_dir.PYROLAB_LOGDIR / cli.DAEMON_OUTPUT_LOG)
|
|
840
840
|
assert seen["start_new_session"] is True
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
def test_nslist_is_sorted(monkeypatch):
|
|
844
|
+
# Regression: listed in whatever order the nameserver returned (reversed,
|
|
845
|
+
# in the lab).
|
|
846
|
+
class FakeNS:
|
|
847
|
+
def list(self, return_metadata=False):
|
|
848
|
+
return {
|
|
849
|
+
"zeta.service": ("PYRO:z@host:1", {"b-tag", "a-tag"}),
|
|
850
|
+
"alpha.service": ("PYRO:a@host:1", set()),
|
|
851
|
+
"Pyro.NameServer": ("PYRO:Pyro.NameServer@host:9090", set()),
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
import pyrolab.api
|
|
855
|
+
|
|
856
|
+
monkeypatch.setattr(pyrolab.api, "locate_ns", lambda host, port: FakeNS())
|
|
857
|
+
result = runner.invoke(cli.app, ["nslist", "--host", "lab", "--port", "9090"])
|
|
858
|
+
assert result.exit_code == 0, result.output
|
|
859
|
+
names = [line.split()[0] for line in result.output.splitlines()[2:] if line.strip()]
|
|
860
|
+
assert names == ["Pyro.NameServer", "alpha.service", "zeta.service"]
|
|
861
|
+
assert "a-tag: b-tag" in result.output
|
|
@@ -9,6 +9,10 @@ import json
|
|
|
9
9
|
import logging
|
|
10
10
|
import multiprocessing as mp
|
|
11
11
|
import pickle
|
|
12
|
+
import signal
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
import threading
|
|
12
16
|
import time
|
|
13
17
|
|
|
14
18
|
import pytest
|
|
@@ -331,3 +335,54 @@ def test_cli_logging_only_when_requested(monkeypatch):
|
|
|
331
335
|
def test_log_level_from_environment(monkeypatch, value, level):
|
|
332
336
|
monkeypatch.setenv(logs.LOGLEVEL_ENV_VAR, value)
|
|
333
337
|
assert logs.log_level() == level
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
###############################################################################
|
|
341
|
+
# Output buffering and SIGTERM
|
|
342
|
+
###############################################################################
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@pytest.mark.parametrize("line_buffered", [False, True])
|
|
346
|
+
def test_output_survives_a_hard_exit_only_when_line_buffered(tmp_path, line_buffered):
|
|
347
|
+
# Regression: driver print()s went to pyrolabd_output.log block-buffered,
|
|
348
|
+
# so they showed up late and were lost when the process was killed.
|
|
349
|
+
# os._exit skips flushing, like a kill does.
|
|
350
|
+
script = (
|
|
351
|
+
"import os\n"
|
|
352
|
+
"from pyrolab import logs\n"
|
|
353
|
+
+ ("logs.line_buffer_output()\n" if line_buffered else "")
|
|
354
|
+
+ "print('Arduino Failed: no such port')\n"
|
|
355
|
+
"os._exit(1)\n"
|
|
356
|
+
)
|
|
357
|
+
out = tmp_path / "pyrolabd_output.log"
|
|
358
|
+
with open(out, "w") as f:
|
|
359
|
+
subprocess.run([sys.executable, "-c", script], stdout=f, check=False)
|
|
360
|
+
printed = "Arduino Failed" in out.read_text()
|
|
361
|
+
assert printed == line_buffered
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def test_line_buffer_output_tolerates_missing_streams(monkeypatch):
|
|
365
|
+
monkeypatch.setattr(sys, "stdout", None) # as under pythonw
|
|
366
|
+
monkeypatch.setattr(sys, "stderr", object())
|
|
367
|
+
logs.line_buffer_output()
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def test_on_sigterm_installs_the_handler_in_the_main_thread(monkeypatch):
|
|
371
|
+
installed = {}
|
|
372
|
+
monkeypatch.setattr(signal, "signal", lambda sig, h: installed.update({sig: h}))
|
|
373
|
+
|
|
374
|
+
def handler(signum, frame):
|
|
375
|
+
pass
|
|
376
|
+
|
|
377
|
+
logs.on_sigterm(handler)
|
|
378
|
+
assert installed == {signal.SIGTERM: handler}
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def test_on_sigterm_does_nothing_outside_the_main_thread(monkeypatch):
|
|
382
|
+
# The tests run runners' run() in threads, where signal() would raise.
|
|
383
|
+
installed = {}
|
|
384
|
+
monkeypatch.setattr(signal, "signal", lambda sig, h: installed.update({sig: h}))
|
|
385
|
+
worker = threading.Thread(target=logs.on_sigterm, args=(print,))
|
|
386
|
+
worker.start()
|
|
387
|
+
worker.join()
|
|
388
|
+
assert installed == {}
|
|
@@ -345,3 +345,38 @@ def test_main_copies_legacy_files(run_main, data_dir, wait_for):
|
|
|
345
345
|
proxy.shutdown()
|
|
346
346
|
thread.join(timeout=10)
|
|
347
347
|
assert data_dir.USER_CONFIG_FILE.read_text() == "daemons: {old: {}}\n"
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def test_ps_shows_a_nameserver_on_another_machine_as_remote(
|
|
351
|
+
data_dir, global_config, fake_manager, monkeypatch
|
|
352
|
+
):
|
|
353
|
+
# Regression: a nameserver configured only so services can register with
|
|
354
|
+
# it (it runs elsewhere) was listed as "Stopped".
|
|
355
|
+
data_dir.USER_CONFIG_FILE.write_text(
|
|
356
|
+
"version: '1.0'\n"
|
|
357
|
+
"nameservers:\n"
|
|
358
|
+
" here: {host: localhost, ns_port: 9090}\n"
|
|
359
|
+
" elsewhere: {host: lab-nameserver.example.edu, ns_port: 9090}\n"
|
|
360
|
+
)
|
|
361
|
+
monkeypatch.setattr(
|
|
362
|
+
fake_manager,
|
|
363
|
+
"get_nameserver_process_info",
|
|
364
|
+
lambda name: {"created": "", "status": "Stopped", "uri": "", "error": ""},
|
|
365
|
+
)
|
|
366
|
+
rows = {line.split()[0]: line for line in PyroLabDaemon().ps().splitlines() if line}
|
|
367
|
+
assert "Remote" in rows["elsewhere"]
|
|
368
|
+
assert "Stopped" in rows["here"]
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def test_sigterm_exits_cleanly(monkeypatch):
|
|
372
|
+
# Regression: under systemd, SIGTERM killed the daemon at once, skipping
|
|
373
|
+
# the cleanup in _serve's finally (stopping children, the lockfile).
|
|
374
|
+
ignored = []
|
|
375
|
+
monkeypatch.setattr(
|
|
376
|
+
pyrolabd.signal, "signal", lambda sig, handler: ignored.append((sig, handler))
|
|
377
|
+
)
|
|
378
|
+
with pytest.raises(SystemExit) as exit_:
|
|
379
|
+
pyrolabd._exit_on_sigterm(15, None)
|
|
380
|
+
assert exit_.value.code == 0
|
|
381
|
+
# Further SIGTERMs are ignored so they can't interrupt the cleanup.
|
|
382
|
+
assert ignored == [(pyrolabd.signal.SIGTERM, pyrolabd.signal.SIG_IGN)]
|
|
@@ -12,10 +12,13 @@ be observed directly.
|
|
|
12
12
|
|
|
13
13
|
import gc
|
|
14
14
|
import logging
|
|
15
|
+
import multiprocessing
|
|
15
16
|
import queue
|
|
16
17
|
import socket
|
|
18
|
+
import sys
|
|
17
19
|
import threading
|
|
18
20
|
import time
|
|
21
|
+
from types import SimpleNamespace
|
|
19
22
|
|
|
20
23
|
import Pyro5
|
|
21
24
|
import pytest
|
|
@@ -355,3 +358,37 @@ def test_daemon_runner_closes_its_instruments_when_stopping(
|
|
|
355
358
|
assert len(closed) == 1
|
|
356
359
|
finally:
|
|
357
360
|
gc.enable()
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
@pytest.mark.skipif(sys.platform == "win32", reason="terminate() is not SIGTERM")
|
|
364
|
+
def test_runner_in_a_real_process_stops_cleanly_on_sigterm(wait_for):
|
|
365
|
+
# Regression: SIGTERM (what systemctl stop and Process.terminate() send)
|
|
366
|
+
# killed a runner at once, skipping its cleanup; it now stops the same
|
|
367
|
+
# way the stop message does.
|
|
368
|
+
with multiprocessing.Manager() as shared:
|
|
369
|
+
state = shared.dict()
|
|
370
|
+
runner = NameServerRunner(
|
|
371
|
+
name="ns",
|
|
372
|
+
nsconfig=NameServerConfiguration(host="127.0.0.1", ns_port=free_port()),
|
|
373
|
+
msg_queue=multiprocessing.Queue(),
|
|
374
|
+
msg_polling=0.05,
|
|
375
|
+
shared_state=state,
|
|
376
|
+
)
|
|
377
|
+
runner.start()
|
|
378
|
+
try:
|
|
379
|
+
assert wait_for(lambda: state.get("ready"), timeout=20)
|
|
380
|
+
runner.terminate()
|
|
381
|
+
runner.join(timeout=10)
|
|
382
|
+
assert runner.exitcode == 0 # -15 if the signal had killed it
|
|
383
|
+
finally:
|
|
384
|
+
if runner.is_alive():
|
|
385
|
+
runner.kill()
|
|
386
|
+
runner.join()
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def test_runners_treat_sigterm_like_the_stop_message():
|
|
390
|
+
for cls in (NameServerRunner, DaemonRunner):
|
|
391
|
+
runner = SimpleNamespace(KIND=cls.KIND, name="x", KILL_SIGNAL=False)
|
|
392
|
+
cls._stop_on_sigterm(runner, 15, None)
|
|
393
|
+
assert runner.KILL_SIGNAL is True
|
|
394
|
+
assert cls.stay_alive(runner) is False
|
|
@@ -137,3 +137,27 @@ def test_get_ip_falls_back_to_loopback(no_route, monkeypatch, caplog, resolved):
|
|
|
137
137
|
monkeypatch.setattr(utils.socket, "gethostbyname", resolve)
|
|
138
138
|
assert utils.get_ip() == "127.0.0.1"
|
|
139
139
|
assert "only clients on this machine can reach" in caplog.text
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@pytest.mark.parametrize(
|
|
143
|
+
"host, here",
|
|
144
|
+
[
|
|
145
|
+
(None, True),
|
|
146
|
+
("", True),
|
|
147
|
+
("localhost", True),
|
|
148
|
+
("public", True),
|
|
149
|
+
("0.0.0.0", True),
|
|
150
|
+
("::", True),
|
|
151
|
+
("[::1]", True),
|
|
152
|
+
("127.0.0.5", True),
|
|
153
|
+
("lab", True),
|
|
154
|
+
("LAB.example.edu", True), # this machine's name, fully qualified
|
|
155
|
+
("10.0.0.7", True), # this machine's address
|
|
156
|
+
("camacholab.ee.byu.edu", False),
|
|
157
|
+
("10.0.0.8", False),
|
|
158
|
+
],
|
|
159
|
+
)
|
|
160
|
+
def test_names_this_machine(monkeypatch, host, here):
|
|
161
|
+
monkeypatch.setattr(utils.socket, "gethostname", lambda: "lab")
|
|
162
|
+
monkeypatch.setattr(utils, "get_ip", lambda: "10.0.0.7")
|
|
163
|
+
assert utils.names_this_machine(host) is here
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|