BioImager  5.0.0
A .NET microscopy imaging application based on Bio library. Supports various microscopes by using imported libraries & GUI automation. Supports XInput game controllers to move stage, take images, run ImageJ macros on images or Bio C# scripts.
Loading...
Searching...
No Matches
microscope.device_server Namespace Reference

Classes

class  DeviceServer
 
class  DeviceServerOptions
 
class  Filter
 

Functions

 device (typing.Callable cls, str host, int port, typing.Mapping[str, typing.Any] conf={}, typing.Optional[str] uid=None)
 
 _create_log_formatter (str name)
 
None _check_autoproxy_feature ()
 
None _register_device (pyro_daemon, device, obj_id=None)
 
 serve_devices (devices, DeviceServerOptions options, exit_event=None)
 
DeviceServerOptions _parse_cmd_line_args (typing.Sequence[str] args)
 
 _load_source (filepath)
 
 validate_devices (configfile)
 
int main (typing.Sequence[str] argv)
 
int _setuptools_entry_point ()
 
None __main__ ()
 

Variables

 _logger = logging.getLogger(__name__)
 
 multiprocessing = multiprocessing.get_context("spawn")
 
 SERIALIZER
 
 REQUIRE_EXPOSE
 

Function Documentation

◆ __main__()

None microscope.device_server.__main__ ( )

Definition at line 624 of file device_server.py.

624def __main__() -> None:
625 # Kept for backwards compatibility. It keeps the setuptools
626 # scripts from older editable mode installations. Will be safe to
627 # remove soon.
628 _setuptools_entry_point()
629
630

◆ _check_autoproxy_feature()

None microscope.device_server._check_autoproxy_feature ( )
protected

Definition at line 202 of file device_server.py.

202def _check_autoproxy_feature() -> None:
203 # AUTOPROXY is enabled by default. If it is disabled there must
204 # be a reason so raise an error instead of silently enabling it.
205 if not Pyro4.config.AUTOPROXY:
206 raise Exception(
207 "serving of a ControllerDevice requires"
208 " Pyro4 AUTOPROXY option enabled"
209 )
210
211 # AUTOPROXY does not work with the marshal serializer. marshal is
212 # not the default serializer so if it is the current serializer
213 # there must be a reason so we don't just change it.
214 if Pyro4.config.SERIALIZER == "marshal":
215 raise Exception(
216 "Pyro's AUTOPROXY feature is required but the"
217 " 'marshal' serializer is currently selected"
218 )
219 if "marshal" in Pyro4.config.SERIALIZERS_ACCEPTED:
220 Pyro4.config.SERIALIZERS_ACCEPTED.remove("marshal")
221 _logger.info("marshal was removed from accepted serializers")
222 return None
223
224

◆ _create_log_formatter()

microscope.device_server._create_log_formatter ( str name)
protected
Create a logging.Formatter for the device server.

Each device is served on its own process and each device has its
own log file.  But the logs from all device servers also appear on
stderr where it will be difficult to figure out from which device
server a log message comes.  This creates a logging.Formatter
which includes the device server name.

Args:
    name: device name to be used on the log output.

Definition at line 133 of file device_server.py.

133def _create_log_formatter(name: str):
134 """Create a logging.Formatter for the device server.
135
136 Each device is served on its own process and each device has its
137 own log file. But the logs from all device servers also appear on
138 stderr where it will be difficult to figure out from which device
139 server a log message comes. This creates a logging.Formatter
140 which includes the device server name.
141
142 Args:
143 name: device name to be used on the log output.
144
145 """
146 return logging.Formatter(
147 "%%(asctime)s:%s (%%(name)s):%%(levelname)s"
148 ":PID %%(process)s: %%(message)s" % name
149 )
150
151

◆ _load_source()

microscope.device_server._load_source ( filepath)
protected

Definition at line 575 of file device_server.py.

575def _load_source(filepath):
576 loader = importlib.machinery.SourceFileLoader("config", filepath)
577 spec = importlib.util.spec_from_loader("config", loader)
578 module = importlib.util.module_from_spec(spec)
579 spec.loader.exec_module(module)
580 return module
581
582

◆ _parse_cmd_line_args()

DeviceServerOptions microscope.device_server._parse_cmd_line_args ( typing.Sequence[str] args)
protected

Definition at line 551 of file device_server.py.

551def _parse_cmd_line_args(args: typing.Sequence[str]) -> DeviceServerOptions:
552 parser = argparse.ArgumentParser(prog="device-server")
553 parser.add_argument(
554 "--logging-level",
555 action="store",
556 type=str,
557 default="info",
558 choices=["debug", "info", "warning", "error", "critical"],
559 help="Set logging level",
560 )
561 parser.add_argument(
562 "config_fpath",
563 action="store",
564 type=str,
565 metavar="CONFIG-FILEPATH",
566 help="Path to the configuration file",
567 )
568 parsed = parser.parse_args(args)
569 return DeviceServerOptions(
570 config_fpath=parsed.config_fpath,
571 logging_level=getattr(logging, parsed.logging_level.upper()),
572 )
573
574

◆ _register_device()

None microscope.device_server._register_device ( pyro_daemon,
device,
obj_id = None )
protected

Definition at line 225 of file device_server.py.

225def _register_device(pyro_daemon, device, obj_id=None) -> None:
226 pyro_daemon.register(device, obj_id)
227
228 if isinstance(device, microscope.abc.Controller):
229 _check_autoproxy_feature()
230 for sub_device in device.devices.values():
231 _register_device(pyro_daemon, sub_device, obj_id=None)
232
233 if isinstance(device, microscope.abc.Stage):
234 _check_autoproxy_feature()
235 for axis in device.axes.values():
236 _register_device(pyro_daemon, axis, obj_id=None)
237
238 return None
239
240

◆ _setuptools_entry_point()

int microscope.device_server._setuptools_entry_point ( )
protected

Definition at line 613 of file device_server.py.

613def _setuptools_entry_point() -> int:
614 # The setuptools entry point must be a function, we can't simply
615 # name this module even if this module does work as a script. We
616 # also do not want to set the default of main() to sys.argv
617 # because when the documentation is generated (with Sphinx's
618 # autodoc extension), then sys.argv gets replaced with the
619 # sys.argv value at the time docs were generated (see
620 # https://stackoverflow.com/a/12087750 )
621 return main(sys.argv)
622
623

◆ device()

microscope.device_server.device ( typing.Callable cls,
str host,
int port,
typing.Mapping[str, typing.Any] conf = {},
typing.Optional[str] uid = None )
Define devices and where to serve them.

A device definition for use in deviceserver config files.

Args:
    cls: :class:`Device` class of device to serve or function that
        returns a map of `Device` instances to wanted Pyro ID.
        The device class will be constructed, or the function will
        be called, with the arguments in ``conf``.
    host: hostname or ip address serving the devices.
    port: port number used to serve the devices.
    conf: keyword arguments for ``cls``.  The device or function
        are effectively constructed or called with `cls(**conf)`.
    uid: used to identify "floating" devices (see documentation
        for :class:`FloatingDeviceMixin`).  This must be specified
        if ``cls`` is a floating device.

Example

.. code-block:: python

    def construct_devices() -> typing.Dict[str, Device]:
        camera = Camera(some, arguments)
        # ... any other configuration that might be wanted
        return {'RedCamera': camera}

    DEVICES = [
        # passing a function that returns devices
        device(construct_devices, '127.0.0.1', 8000),
        # passing a Device class
        device(Camera, '127.0.0.1', 8001,
               conf={'kwarg1': some, 'kwarg2': arguments})
    ]

Definition at line 81 of file device_server.py.

87):
88 """Define devices and where to serve them.
89
90 A device definition for use in deviceserver config files.
91
92 Args:
93 cls: :class:`Device` class of device to serve or function that
94 returns a map of `Device` instances to wanted Pyro ID.
95 The device class will be constructed, or the function will
96 be called, with the arguments in ``conf``.
97 host: hostname or ip address serving the devices.
98 port: port number used to serve the devices.
99 conf: keyword arguments for ``cls``. The device or function
100 are effectively constructed or called with `cls(**conf)`.
101 uid: used to identify "floating" devices (see documentation
102 for :class:`FloatingDeviceMixin`). This must be specified
103 if ``cls`` is a floating device.
104
105 Example
106
107 .. code-block:: python
108
109 def construct_devices() -> typing.Dict[str, Device]:
110 camera = Camera(some, arguments)
111 # ... any other configuration that might be wanted
112 return {'RedCamera': camera}
113
114 DEVICES = [
115 # passing a function that returns devices
116 device(construct_devices, '127.0.0.1', 8000),
117 # passing a Device class
118 device(Camera, '127.0.0.1', 8001,
119 conf={'kwarg1': some, 'kwarg2': arguments})
120 ]
121
122 """
123 if not callable(cls):
124 raise TypeError("cls must be a callable")
125 elif isinstance(cls, type):
126 if issubclass(cls, FloatingDeviceMixin) and uid is None:
127 raise TypeError("uid must be specified for floating devices")
128 elif not issubclass(cls, FloatingDeviceMixin) and uid is not None:
129 raise TypeError("uid must not be given for non floating devices")
130 return dict(cls=cls, host=host, port=int(port), uid=uid, conf=conf)
131
132

◆ main()

int microscope.device_server.main ( typing.Sequence[str] argv)

Definition at line 594 of file device_server.py.

594def main(argv: typing.Sequence[str]) -> int:
595 options = _parse_cmd_line_args(argv[1:])
596
597 root_logger = logging.getLogger()
598 root_logger.setLevel(options.logging_level)
599
600 stderr_handler = StreamHandler(sys.stderr)
601 stderr_handler.setFormatter(_create_log_formatter("device-server"))
602 root_logger.addHandler(stderr_handler)
603
604 root_logger.addFilter(Filter())
605
606 devices = validate_devices(options.config_fpath)
607
608 serve_devices(devices, options)
609
610 return 0
611
612

◆ serve_devices()

microscope.device_server.serve_devices ( devices,
DeviceServerOptions options,
exit_event = None )

Definition at line 393 of file device_server.py.

393def serve_devices(devices, options: DeviceServerOptions, exit_event=None):
394 # We make changes to `devices` (would be great if we didn't had
395 # to) so make a a copy of it because we don't want to make those
396 # changes on the caller. See original issue on #211 and PRs #212
397 # and #217 (most discussion happens on #212).
398 devices = copy.deepcopy(devices)
399
400 root_logger = logging.getLogger()
401
402 log_handler = RotatingFileHandler("__MAIN__.log")
403 log_handler.setFormatter(_create_log_formatter("device-server"))
404 root_logger.addHandler(log_handler)
405
406 # An event to trigger clean termination of subprocesses. This is the
407 # only way to ensure devices are shut down properly when processes
408 # exit, as __del__ is not necessarily called when the interpreter exits.
409 if exit_event is None:
410 exit_event = multiprocessing.Event()
411
412 servers = (
413 []
414 ) # DeviceServers instances that we need to wait for when exiting
415
416 # Child processes inherit signal handling from the parent so we
417 # need to make sure that only the parent process sets the exit
418 # event and waits for the DeviceServers to exit. See issue #9.
419 # This won't work behind a Windows service wrapper, so we deal with
420 # clean shutdown on win32 elsewhere.
421 parent = multiprocessing.current_process()
422
423 def term_func(sig, frame):
424 """Terminate subprocesses cleanly."""
425 if parent == multiprocessing.current_process():
426 _logger.debug("Shutting down all servers.")
427 exit_event.set()
428 # Join keep_alive_thread so that it can't modify the list
429 # of servers.
430 keep_alive_thread.join()
431 for this_server in servers:
432 this_server.join()
433 sys.exit()
434
435 if sys.platform != "win32":
436 signal.signal(signal.SIGTERM, term_func)
437 signal.signal(signal.SIGINT, term_func)
438
439 # Group devices by class.
440 by_class = {}
441 for dev in devices:
442 by_class[dev["cls"]] = by_class.get(dev["cls"], []) + [dev]
443
444 if not by_class:
445 _logger.warning("No valid devices specified. Maybe an empty list?")
446
447 for cls, devs in by_class.items():
448 # Floating devices are devices that can only be identified
449 # after having been initialized, so the constructor will
450 # return any device that it supports. To work around this we
451 # map all device uid to host/port first. After the
452 # DeviceServer constructs the device, it can check on the map
453 # where to serve it. For non floating devices that
454 # information is part of the device definition, no map is
455 # needed.
456 uid_to_host = {}
457 uid_to_port = {}
458 if isinstance(cls, type) and issubclass(cls, FloatingDeviceMixin):
459 # In addition to the maps of uid to host/port, floating
460 # devices SDKs need the number of devices to index them.
461 count = 0
462 for dev in devs:
463 uid = dev["uid"]
464 uid_to_host[uid] = dev["host"]
465 uid_to_port[uid] = dev["port"]
466
467 dev["conf"]["index"] = count
468 count += 1
469
470 for dev in devs:
471 servers.append(
472 DeviceServer(
473 dev,
474 options,
475 uid_to_host,
476 uid_to_port,
477 exit_event=exit_event,
478 )
479 )
480 servers[-1].start()
481
482 # Main thread must be idle to process signals correctly, so use another
483 # thread to check DeviceServers, restarting them where necessary. Define
484 # the thread target here so that it can access variables in __main__ scope.
485 def keep_alive():
486 """Keep DeviceServers alive."""
487 while not exit_event.is_set():
488 for s in servers:
489 if s.is_alive():
490 continue
491 else:
492 _logger.info(
493 "DeviceServer Failure. Process %s is dead with"
494 " exitcode %s. Restarting...",
495 s.pid,
496 s.exitcode,
497 )
498 servers.remove(s)
499 servers.append(s.clone())
500
501 try:
502 s.join(30)
503 except:
504 _logger.error("... could not join PID %s.", s.pid)
505 else:
506 old_pid = s.pid
507 del s
508 servers[-1].start()
509 _logger.info(
510 "... DeviceServer with PID %s restarted"
511 " as PID %s.",
512 old_pid,
513 servers[-1].pid,
514 )
515 if not servers:
516 # Log and exit if no servers running. May want to change this
517 # if we add some interface to interactively restart servers.
518 _logger.info("No servers running. Exiting.")
519 exit_event.set()
520 else:
521 try:
522 time.sleep(5)
523 except (KeyboardInterrupt, IOError):
524 pass
525
526 keep_alive_thread = Thread(target=keep_alive)
527 keep_alive_thread.start()
528
529 _logger.info("Device Server started. Press Ctrl+C to exit.")
530 while not exit_event.is_set():
531 try:
532 time.sleep(5)
533 except (KeyboardInterrupt, IOError):
534 _logger.debug("KeyboardInterrupt or IOError")
535 exit_event.set()
536
537 _logger.debug("Shutting down servers ...")
538 while servers:
539 for s in servers:
540 if not s.is_alive():
541 servers.remove(s)
542 del s
543 time.sleep(1)
544 _logger.info(" ... No more servers running.")
545 _logger.debug("Joining threads ...")
546 keep_alive_thread.join()
547 _logger.debug("... Threads joined. Exiting.")
548 return
549
550

◆ validate_devices()

microscope.device_server.validate_devices ( configfile)

Definition at line 583 of file device_server.py.

583def validate_devices(configfile):
584 config = _load_source(configfile)
585 try:
586 devices = getattr(config, "DEVICES")
587 except AttributeError:
588 raise Exception("No 'DEVICES=...' in config file.")
589 if not isinstance(devices, Iterable):
590 raise Exception("Error in config: DEVICES should be an iterable.")
591 return devices
592
593

Variable Documentation

◆ _logger

microscope.device_server._logger = logging.getLogger(__name__)
protected

Definition at line 59 of file device_server.py.

◆ multiprocessing

microscope.device_server.multiprocessing = multiprocessing.get_context("spawn")

Definition at line 66 of file device_server.py.

◆ REQUIRE_EXPOSE

microscope.device_server.REQUIRE_EXPOSE

Definition at line 78 of file device_server.py.

◆ SERIALIZER

microscope.device_server.SERIALIZER

Definition at line 71 of file device_server.py.