Skip to content

Commit 2d4d255

Browse files
committed
device_server: pass options to DeviceServer to enable logging (#180)
When the multiprocessing start method is not fork (Windows only has spawn, and macOS defaults to spawn), then configuration done in the main process is not copied to the child process, i.e., the actual device process. This means that the logging level set on the main process is not inherited by the DeviceServer process. This commit passes the device server options to the DeviceServer so that it can redo the required logging configuration on the new process. It does this by passing all options since we expect to expand those. This commit also adds dependency on the dataclasses module which means being dependent on Python 3.7 (which is now almost 3 years old).
1 parent fea16e0 commit 2d4d255

File tree

6 files changed

+66
-13
lines changed

6 files changed

+66
-13
lines changed

NEWS.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ python-microscope releases.
44
Version 0.7.0 (upcoming)
55
------------------------
66

7+
* The device server logging was broken in version 0.6.0 for Windows
8+
and macOS (systems not using fork for multiprocessing). This
9+
version fixes that issue.
10+
11+
* Microscope is now dependent on Python 3.7 or later.
12+
713

814
Version 0.6.0 (2021/01/14)
915
--------------------------

microscope/device_server.py

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import time
4545
import typing
4646
from collections.abc import Iterable
47+
from dataclasses import dataclass
4748
from logging import StreamHandler
4849
from logging.handlers import RotatingFileHandler
4950
from threading import Thread
@@ -184,6 +185,19 @@ def filter(self, record):
184185
return False
185186

186187

188+
@dataclass(frozen=True)
189+
class DeviceServerOptions:
190+
"""Class to define configuration for a device server.
191+
192+
The different fields map to the different ``device-server``
193+
command line options.
194+
195+
"""
196+
197+
config_fpath: str
198+
logging_level: int
199+
200+
187201
def _check_autoproxy_feature() -> None:
188202
# AUTOPROXY is enabled by default. If it is disabled there must
189203
# be a reason so raise an error instead of silently enabling it.
@@ -228,6 +242,7 @@ class DeviceServer(multiprocessing.Process):
228242
229243
Args:
230244
device_def: definition of the device.
245+
options: configuration for the device server.
231246
id_to_host: host or mapping of device identifiers to hostname.
232247
id_to_port: map or mapping of device identifiers to port
233248
number.
@@ -239,12 +254,14 @@ class DeviceServer(multiprocessing.Process):
239254
def __init__(
240255
self,
241256
device_def,
257+
options: DeviceServerOptions,
242258
id_to_host: typing.Mapping[str, str],
243259
id_to_port: typing.Mapping[str, int],
244260
exit_event: typing.Optional[multiprocessing.Event] = None,
245261
):
246262
# The device to serve.
247263
self._device_def = device_def
264+
self._options = options
248265
self._devices: typing.Dict[str, microscope.abc.Device] = {}
249266
# Where to serve it.
250267
self._id_to_host = id_to_host
@@ -262,6 +279,7 @@ def clone(self):
262279
"""
263280
return DeviceServer(
264281
self._device_def,
282+
self._options,
265283
self._id_to_host,
266284
self._id_to_port,
267285
exit_event=self.exit_event,
@@ -283,6 +301,8 @@ def run(self):
283301
for handler in list(root_logger.handlers):
284302
root_logger.removeHandler(handler)
285303

304+
root_logger.setLevel(self._options.logging_level)
305+
286306
# Later, we'll log to one file per server, with a filename
287307
# based on a unique identifier for the device. Some devices
288308
# don't have UIDs available until after initialization, so
@@ -369,7 +389,7 @@ def run(self):
369389
_logger.error("Failure to shutdown device %s", device, ex)
370390

371391

372-
def serve_devices(devices, exit_event=None):
392+
def serve_devices(devices, options: DeviceServerOptions, exit_event=None):
373393
root_logger = logging.getLogger()
374394

375395
log_handler = RotatingFileHandler("__MAIN__.log")
@@ -443,7 +463,11 @@ def term_func(sig, frame):
443463
for dev in devs:
444464
servers.append(
445465
DeviceServer(
446-
dev, uid_to_host, uid_to_port, exit_event=exit_event
466+
dev,
467+
options,
468+
uid_to_host,
469+
uid_to_port,
470+
exit_event=exit_event,
447471
)
448472
)
449473
servers[-1].start()
@@ -517,7 +541,7 @@ def keep_alive():
517541
return
518542

519543

520-
def _parse_cmd_line_args(args: typing.Sequence[str]) -> argparse.Namespace:
544+
def _parse_cmd_line_args(args: typing.Sequence[str]) -> DeviceServerOptions:
521545
parser = argparse.ArgumentParser(prog="device-server")
522546
parser.add_argument(
523547
"--logging-level",
@@ -534,7 +558,11 @@ def _parse_cmd_line_args(args: typing.Sequence[str]) -> argparse.Namespace:
534558
metavar="CONFIG-FILEPATH",
535559
help="Path to the configuration file",
536560
)
537-
return parser.parse_args(args)
561+
parsed = parser.parse_args(args)
562+
return DeviceServerOptions(
563+
config_fpath=parsed.config_fpath,
564+
logging_level=getattr(logging, parsed.logging_level.upper()),
565+
)
538566

539567

540568
def _load_source(filepath):
@@ -557,20 +585,20 @@ def validate_devices(configfile):
557585

558586

559587
def main(argv: typing.Sequence[str]) -> int:
560-
args = _parse_cmd_line_args(argv[1:])
588+
options = _parse_cmd_line_args(argv[1:])
561589

562590
root_logger = logging.getLogger()
563-
root_logger.setLevel(args.logging_level.upper())
591+
root_logger.setLevel(options.logging_level)
564592

565593
stderr_handler = StreamHandler(sys.stderr)
566594
stderr_handler.setFormatter(_create_log_formatter("device-server"))
567595
root_logger.addHandler(stderr_handler)
568596

569597
root_logger.addFilter(Filter())
570598

571-
devices = validate_devices(args.config_fpath)
599+
devices = validate_devices(options.config_fpath)
572600

573-
serve_devices(devices)
601+
serve_devices(devices, options)
574602

575603
return 0
576604

microscope/testsuite/test_device_server.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,12 @@ class BaseTestServeDevices(unittest.TestCase):
109109

110110
@_patch_out_device_server_logs
111111
def setUp(self):
112+
options = microscope.device_server.DeviceServerOptions(
113+
config_fpath="", logging_level=logging.INFO,
114+
)
112115
self.p = multiprocessing.Process(
113-
target=microscope.device_server.serve_devices, args=(self.DEVICES,)
116+
target=microscope.device_server.serve_devices,
117+
args=(self.DEVICES, options),
114118
)
115119
self.p.start()
116120
time.sleep(1)
@@ -266,6 +270,9 @@ class TestServingFloatingDevicesWithWrongUID(BaseTestDeviceServer):
266270
{"uid": "foo", "index": 0},
267271
uid="bar",
268272
),
273+
microscope.device_server.DeviceServerOptions(
274+
config_fpath="", logging_level=logging.INFO,
275+
),
269276
{"bar": "127.0.0.1"},
270277
{"bar": 8001},
271278
multiprocessing.Event(),
@@ -295,6 +302,9 @@ class TestFunctionInDeviceDefinition(BaseTestDeviceServer):
295302
"localhost",
296303
8001,
297304
),
305+
microscope.device_server.DeviceServerOptions(
306+
config_fpath="", logging_level=logging.INFO,
307+
),
298308
{},
299309
{},
300310
multiprocessing.Event(),

microscope/win32.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"""
3232

3333

34+
import logging
3435
import multiprocessing
3536
import os
3637
import sys
@@ -98,11 +99,19 @@ def SvcDoRun(self):
9899
self.log("Logging at %s." % os.getcwd())
99100
self.ReportServiceStatus(win32service.SERVICE_RUNNING)
100101

101-
from microscope.device_server import serve_devices, validate_devices
102+
from microscope.device_server import (
103+
serve_devices,
104+
validate_devices,
105+
DeviceServerOptions,
106+
)
107+
108+
options = DeviceServerOptions(
109+
config_fpath=configfile, logging_level=logging.INFO,
110+
)
102111

103112
try:
104113
devices = validate_devices(configfile)
105-
serve_devices(devices, self.stop_event)
114+
serve_devices(devices, options, self.stop_event)
106115
except Exception as e:
107116
servicemanager.LogErrorMsg(str(e))
108117
# Exit with non-zero error code so Windows will attempt to restart.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ multi_line_output = 3 # multi lineoutput 3 is vert-hanging
1515

1616
[tool.black]
1717
line-length = 79
18-
target-version = ['py36', 'py37', 'py38']
18+
target-version = ['py37', 'py38']
1919

2020

2121
[tool.pylint.FORMAT]

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ def make_distribution(self):
186186
"Tracker": "https://github.com/python-microscope/microscope",
187187
},
188188
packages=setuptools.find_packages(),
189-
python_requires=">=3.6",
189+
python_requires=">=3.7",
190190
install_requires=["Pillow", "Pyro4", "hidapi", "numpy", "pyserial"],
191191
extras_require={"GUI": ["PySide2"]},
192192
entry_points={

0 commit comments

Comments
 (0)