Commit c7503270 authored by Anton Joubert's avatar Anton Joubert
Browse files

Merge branch 'sar-231-add-debugpy' into 'master'

SAR-231: Add DebugDevice command for debugpy

See merge request ska-telescope/ska-tango-base!44
parents bb35027e 0b9782c1
release=0.9.1
tag=ska_tango_base-0.9.1
release=0.10.0
tag=ska_tango_base-0.10.0
......@@ -25,6 +25,15 @@ The ska-tango-base repository includes a set of eight classes as mentioned in SK
## Version History
#### 0.10.0
- Add `DebugDevice` command to `SKABaseDevice`. This allows remote debugging to be
enabled on all devices. It cannot be disabled without restarting the process.
If there are multiple devices in a device server, debugging is only enabled for
the requested device (i.e., methods patched for debugging cppTango threads).
However, all Python threads (not cppTango threads), will also be debuggable,
even if created by devices other than the one that was used to enable debugging.
There is only one debugger instance shared by the whole process.
#### 0.9.1
- Changed dependency from `ska_logging` to `ska_ser_logging`.
......
......@@ -56,10 +56,11 @@ _MockObject.__call__ = call_mock
# hack end
autodoc_mock_imports = [
'debugpy',
'numpy',
'ska_ser_logging',
'tango',
'transitions',
'ska_ser_logging',
'numpy',
]
autodoc_default_options = {
......
......@@ -93,6 +93,15 @@
</argout>
<status abstract="false" inherited="true" concrete="true"/>
</commands>
<commands name="DebugDevice" description="Enables remote debugging of this device" execMethod="debug_device" displayLevel="OPERATOR" polledPeriod="0" isDynamic="false">
<argin description="">
<type xsi:type="pogoDsl:VoidType"/>
</argin>
<argout description="TCP port debugger is listening on">
<type xsi:type="pogoDsl:UShortType"/>
</argout>
<status abstract="false" inherited="true" concrete="true"/>
</commands>
<commands name="LoadFirmware" description="Deploy new versions of software and firmware and &#xA;trigger a restart so that a Component initializes using a &#xA;newly deployed version." execMethod="load_firmware" displayLevel="OPERATOR" polledPeriod="0" isDynamic="false">
<argin description="The file name or a pointer to the filename , &#xA;the list of components that use software or firmware package (file),&#xA;checksum or signing">
<type xsi:type="pogoDsl:StringArrayType"/>
......
......@@ -66,6 +66,15 @@
</argout>
<status abstract="false" inherited="true" concrete="true"/>
</commands>
<commands name="DebugDevice" description="Enables remote debugging of this device" execMethod="debug_device" displayLevel="OPERATOR" polledPeriod="0" isDynamic="false">
<argin description="">
<type xsi:type="pogoDsl:VoidType"/>
</argin>
<argout description="TCP port debugger is listening on">
<type xsi:type="pogoDsl:UShortType"/>
</argout>
<status abstract="false" inherited="true" concrete="true"/>
</commands>
<commands name="ConfigureScan" description="Configure the observing device parameters for the current scan," execMethod="configure_scan" displayLevel="OPERATOR" polledPeriod="0" isDynamic="false">
<argin description="JSON formatted string with the scan configuration.">
<type xsi:type="pogoDsl:StringType"/>
......
......@@ -132,6 +132,15 @@
</argout>
<status abstract="false" inherited="true" concrete="true"/>
</commands>
<commands name="DebugDevice" description="Enables remote debugging of this device" execMethod="debug_device" displayLevel="OPERATOR" polledPeriod="0" isDynamic="false">
<argin description="">
<type xsi:type="pogoDsl:VoidType"/>
</argin>
<argout description="TCP port debugger is listening on">
<type xsi:type="pogoDsl:UShortType"/>
</argout>
<status abstract="false" inherited="true" concrete="true"/>
</commands>
<commands name="ObsReset" description="Reset observation state machine to its default state" execMethod="obs_reset" displayLevel="OPERATOR" polledPeriod="0">
<argin description="">
<type xsi:type="pogoDsl:VoidType"/>
......
......@@ -104,6 +104,15 @@
</argout>
<status abstract="false" inherited="true" concrete="true" concreteHere="false"/>
</commands>
<commands name="DebugDevice" description="Enables remote debugging of this device" execMethod="debug_device" displayLevel="OPERATOR" polledPeriod="0" isDynamic="false">
<argin description="">
<type xsi:type="pogoDsl:VoidType"/>
</argin>
<argout description="TCP port debugger is listening on">
<type xsi:type="pogoDsl:UShortType"/>
</argout>
<status abstract="false" inherited="true" concrete="true"/>
</commands>
<commands name="GetVersionInfo" description="Array of version strings of all entities modelled by this device. &#xA;(One level down only)&#xA;Each string in the array lists the version info for one entity&#xA;managed by this device. &#xA;The first entry is version info for this TANGO Device itself.&#xA;The entities may be TANGO devices, or hardware LRUs or &#xA;anything else this devices manages/models.&#xA;The intention with this command is that it can provide more &#xA;detailed information than can be captured in the versionId &#xA;and buildState attributes, if necessary.&#xA;In the minimal case the GetVersionInfo will contain only the &#xA;versionId and buildState attributes of the next lower level&#xA;entities." execMethod="get_version_info" displayLevel="OPERATOR" polledPeriod="0">
<argin description="">
<type xsi:type="pogoDsl:VoidType"/>
......
......@@ -59,6 +59,15 @@
</argout>
<status abstract="false" inherited="false" concrete="true" concreteHere="true"/>
</commands>
<commands name="DebugDevice" description="Enables remote debugging of this device" execMethod="debug_device" displayLevel="OPERATOR" polledPeriod="0" isDynamic="false">
<argin description="">
<type xsi:type="pogoDsl:VoidType"/>
</argin>
<argout description="TCP port debugger is listening on">
<type xsi:type="pogoDsl:UShortType"/>
</argout>
<status abstract="false" inherited="false" concrete="true" concreteHere="true"/>
</commands>
<attributes name="buildState" attType="Scalar" rwType="READ" displayLevel="OPERATOR" polledPeriod="0" maxX="" maxY="" allocReadMember="true" isDynamic="false">
<dataType xsi:type="pogoDsl:StringType"/>
<dataReadyEvent fire="false" libCheckCriteria="true"/>
......
......@@ -81,6 +81,15 @@
</argout>
<status abstract="false" inherited="true" concrete="true"/>
</commands>
<commands name="DebugDevice" description="Enables remote debugging of this device" execMethod="debug_device" displayLevel="OPERATOR" polledPeriod="0" isDynamic="false">
<argin description="">
<type xsi:type="pogoDsl:VoidType"/>
</argin>
<argout description="TCP port debugger is listening on">
<type xsi:type="pogoDsl:UShortType"/>
</argout>
<status abstract="false" inherited="true" concrete="true"/>
</commands>
<attributes name="activationTime" attType="Scalar" rwType="READ" displayLevel="OPERATOR" polledPeriod="0" maxX="" maxY="" allocReadMember="true" isDynamic="false">
<dataType xsi:type="pogoDsl:DoubleType"/>
<changeEvent fire="false" libCheckCriteria="false"/>
......
......@@ -69,6 +69,15 @@
</argout>
<status abstract="false" inherited="true" concrete="true"/>
</commands>
<commands name="DebugDevice" description="Enables remote debugging of this device" execMethod="debug_device" displayLevel="OPERATOR" polledPeriod="0" isDynamic="false">
<argin description="">
<type xsi:type="pogoDsl:VoidType"/>
</argin>
<argout description="TCP port debugger is listening on">
<type xsi:type="pogoDsl:UShortType"/>
</argout>
<status abstract="false" inherited="true" concrete="true"/>
</commands>
<attributes name="buildState" attType="Scalar" rwType="READ" displayLevel="OPERATOR" polledPeriod="0" maxX="" maxY="" allocReadMember="true">
<dataType xsi:type="pogoDsl:StringType"/>
<status abstract="false" inherited="true" concrete="true"/>
......
......@@ -82,6 +82,15 @@
</argout>
<status abstract="false" inherited="true" concrete="true"/>
</commands>
<commands name="DebugDevice" description="Enables remote debugging of this device" execMethod="debug_device" displayLevel="OPERATOR" polledPeriod="0" isDynamic="false">
<argin description="">
<type xsi:type="pogoDsl:VoidType"/>
</argin>
<argout description="TCP port debugger is listening on">
<type xsi:type="pogoDsl:UShortType"/>
</argout>
<status abstract="false" inherited="true" concrete="true"/>
</commands>
<attributes name="elementLoggerAddress" attType="Scalar" rwType="READ" displayLevel="OPERATOR" polledPeriod="0" maxX="" maxY="" allocReadMember="true" isDynamic="false">
<dataType xsi:type="pogoDsl:StringType"/>
<changeEvent fire="false" libCheckCriteria="false"/>
......
......@@ -60,6 +60,15 @@
</argout>
<status abstract="false" inherited="true" concrete="true"/>
</commands>
<commands name="DebugDevice" description="Enables remote debugging of this device" execMethod="debug_device" displayLevel="OPERATOR" polledPeriod="0" isDynamic="false">
<argin description="">
<type xsi:type="pogoDsl:VoidType"/>
</argin>
<argout description="TCP port debugger is listening on">
<type xsi:type="pogoDsl:UShortType"/>
</argout>
<status abstract="false" inherited="true" concrete="true"/>
</commands>
<attributes name="obsState" attType="Scalar" rwType="READ" displayLevel="OPERATOR" polledPeriod="0" maxX="" maxY="" allocReadMember="true" isDynamic="false">
<dataType xsi:type="pogoDsl:EnumType"/>
<changeEvent fire="true" libCheckCriteria="true"/>
......
......@@ -131,6 +131,15 @@
</argout>
<status abstract="false" inherited="true" concrete="true" concreteHere="false"/>
</commands>
<commands name="DebugDevice" description="Enables remote debugging of this device" execMethod="debug_device" displayLevel="OPERATOR" polledPeriod="0" isDynamic="false">
<argin description="">
<type xsi:type="pogoDsl:VoidType"/>
</argin>
<argout description="TCP port debugger is listening on">
<type xsi:type="pogoDsl:UShortType"/>
</argout>
<status abstract="false" inherited="true" concrete="true"/>
</commands>
<commands name="ObsReset" description="Reset observation state machine to its default state" execMethod="obsreset" displayLevel="OPERATOR" polledPeriod="0">
<argin description="">
<type xsi:type="pogoDsl:VoidType"/>
......
......@@ -64,6 +64,15 @@
</argout>
<status abstract="false" inherited="true" concrete="true"/>
</commands>
<commands name="DebugDevice" description="Enables remote debugging of this device" execMethod="debug_device" displayLevel="OPERATOR" polledPeriod="0" isDynamic="false">
<argin description="">
<type xsi:type="pogoDsl:VoidType"/>
</argin>
<argout description="TCP port debugger is listening on">
<type xsi:type="pogoDsl:UShortType"/>
</argout>
<status abstract="false" inherited="true" concrete="true"/>
</commands>
<attributes name="buildState" attType="Scalar" rwType="READ" displayLevel="OPERATOR" polledPeriod="0" maxX="" maxY="" allocReadMember="true">
<dataType xsi:type="pogoDsl:StringType"/>
<status abstract="false" inherited="true" concrete="true"/>
......
......@@ -35,7 +35,7 @@ setuptools.setup(
],
platforms=["OS Independent"],
setup_requires=[] + pytest_runner,
install_requires=["transitions", "ska_ser_logging"],
install_requires=["debugpy", "transitions", "ska_ser_logging"],
tests_require=["pytest", "coverage", "pytest-json-report", "pytest-forked"],
entry_points={
"console_scripts": [
......
......@@ -13,13 +13,16 @@ device.
# PROTECTED REGION ID(SKABaseDevice.additionnal_import) ENABLED START #
# Standard imports
import enum
import inspect
import logging
import logging.handlers
import socket
import sys
import threading
import typing
import warnings
from transitions import MachineError
from functools import partial
from urllib.parse import urlparse
from urllib.request import url2pathname
......@@ -28,6 +31,7 @@ from tango import AttrWriteType, DebugIt, DevState
from tango.server import run, Device, attribute, command, device_property
# SKA specific imports
import debugpy
import ska_ser_logging
from ska_tango_base import release
from ska_tango_base.commands import (
......@@ -44,6 +48,7 @@ from ska_tango_base.utils import get_groups_from_json, for_testing_only
from ska_tango_base.faults import GroupDefinitionsError, LoggingTargetError, LoggingLevelError
LOG_FILE_SIZE = 1024 * 1024 # Log file size 1MB.
_DEBUGGER_PORT = 5678
class _Log4TangoLoggingLevel(enum.IntEnum):
......@@ -558,6 +563,8 @@ class SKABaseDevice(Device):
A generic base device for SKA.
"""
_global_debugger_listening = False
class InitCommand(ActionCommand):
"""
A class for the SKABaseDevice's init_device() "command".
......@@ -611,6 +618,7 @@ class SKABaseDevice(Device):
release.version,
release.description)
device._version_id = release.version
device._methods_patched_for_debugger = False
try:
# create TANGO Groups dict, according to property
......@@ -1002,6 +1010,7 @@ class SKABaseDevice(Device):
self.register_command_object(
"GetVersionInfo", self.GetVersionInfoCommand(*device_args)
)
self.register_command_object("DebugDevice", self.DebugDeviceCommand(*device_args))
def always_executed_hook(self):
# PROTECTED REGION ID(SKABaseDevice.always_executed_hook) ENABLED START #
......@@ -1629,6 +1638,112 @@ class SKABaseDevice(Device):
(return_code, message) = command()
return [[return_code], [message]]
class DebugDeviceCommand(BaseCommand):
"""
A class for the SKABaseDevice's DebugDevice() command.
"""
def do(self):
"""
Stateless hook for device DebugDevice() command.
Starts the ``debugpy`` debugger listening for remote connections
(via Debugger Adaptor Protocol), and patches all methods so that
they can be debugged.
If the debugger is already listening, additional execution of this
command will trigger a breakpoint.
:return: The TCP port the debugger is listening on.
:rtype: DevUShort
"""
if not SKABaseDevice._global_debugger_listening:
self.start_debugger(_DEBUGGER_PORT)
SKABaseDevice._global_debugger_listening = True
device = self.target
if not device._methods_patched_for_debugger:
self.monkey_patch_all_methods_for_debugger()
device._methods_patched_for_debugger = True
else:
self.logger.warning("Triggering debugger breakpoint...")
debugpy.breakpoint()
return _DEBUGGER_PORT
def start_debugger(self, port):
self.logger.warning("Starting debugger...")
debugpy.listen(("0.0.0.0", port))
self.logger.warning(
f"Debugger listening on port {port}. Performance may be degraded."
)
def monkey_patch_all_methods_for_debugger(self):
all_methods = self.get_all_methods()
patched = []
for owner, name, method in all_methods:
if self.method_must_be_patched_for_debugger(owner, method):
self.patch_method_for_debugger(owner, name, method)
patched.append(
f"{owner} {method.__func__.__qualname__} in {method.__func__.__module__}"
)
self.logger.info("Patched %s of %s methods", len(patched), len(all_methods))
self.logger.debug("Patched methods: %s", sorted(patched))
def get_all_methods(self):
methods = []
device = self.target
for name, method in inspect.getmembers(device, inspect.ismethod):
methods.append((device, name, method))
for command_object in device._command_objects.values():
for name, method in inspect.getmembers(command_object, inspect.ismethod):
methods.append((command_object, name, method))
return methods
@staticmethod
def method_must_be_patched_for_debugger(owner, method):
"""Determine if methods are worth debugging.
The goal is to find all the user's Python methods, but not the
lower level PyTango device and Boost extension methods. The
`typing.types.FunctionType` check excludes the Boost methods.
"""
skip_module_names = ["tango.device_server", "tango.server", "logging"]
skip_owner_types = [SKABaseDevice.DebugDeviceCommand]
return (
isinstance(method.__func__, typing.types.FunctionType)
and method.__func__.__module__ not in skip_module_names
and type(owner) not in skip_owner_types
)
def patch_method_for_debugger(self, owner, name, method):
"""Ensure method calls trigger the debugger.
Most methods in a device are executed by calls from threads spawned
by the cppTango layer. These threads are not known to Python, so
we have to explicitly inform the debugger about them.
"""
def debug_thread_wrapper(orig_method, *args, **kwargs):
debugpy.debug_this_thread()
return orig_method(*args, **kwargs)
patched_method = partial(debug_thread_wrapper, method)
setattr(owner, name, patched_method)
@command(
dtype_out="DevUShort",
doc_out="The TCP port the debugger is listening on."
)
@DebugIt()
def DebugDevice(self):
"""
Enables remote debugging of this device.
To modify behaviour for this command, modify the do() method of
the command class: :class:`.DebugDeviceCommand`.
"""
command = self.get_command_object("DebugDevice")
return command()
# ----------
# Run server
......
......@@ -7,7 +7,7 @@
"""Release information for ska_tango_base Python Package"""
name = """ska_tango_base"""
version = "0.9.1"
version = "0.10.0"
version_info = version.split(".")
description = """A set of generic base devices for SKA Telescope."""
author = "SKA India and SARAO and CSIRO and INAF"
......
......@@ -90,7 +90,7 @@ class TestSKAAlarmHandler(object):
"""Test for GetVersionInfo"""
# PROTECTED REGION ID(SKAAlarmHandler.test_GetVersionInfo) ENABLED START #
versionPattern = re.compile(
r'SKAAlarmHandler, ska_tango_base, [0-9].[0-9].[0-9], '
r'SKAAlarmHandler, ska_tango_base, [0-9]+.[0-9]+.[0-9]+, '
r'A set of generic base devices for SKA Telescope.')
versionInfo = tango_context.device.GetVersionInfo()
assert (re.match(versionPattern, versionInfo[0])) is not None
......@@ -142,7 +142,7 @@ class TestSKAAlarmHandler(object):
"""Test for buildState"""
# PROTECTED REGION ID(SKAAlarmHandler.test_buildState) ENABLED START #
buildPattern = re.compile(
r'ska_tango_base, [0-9].[0-9].[0-9], '
r'ska_tango_base, [0-9]+.[0-9]+.[0-9]+, '
r'A set of generic base devices for SKA Telescope')
assert (re.match(buildPattern, tango_context.device.buildState)) is not None
# PROTECTED REGION END # // SKAAlarmHandler.test_buildState
......@@ -152,7 +152,7 @@ class TestSKAAlarmHandler(object):
def test_versionId(self, tango_context):
"""Test for versionId"""
# PROTECTED REGION ID(SKAAlarmHandler.test_versionId) ENABLED START #
versionIdPattern = re.compile(r'[0-9].[0-9].[0-9]')
versionIdPattern = re.compile(r'[0-9]+.[0-9]+.[0-9]+')
assert (re.match(versionIdPattern, tango_context.device.versionId)) is not None
# PROTECTED REGION END # // SKAAlarmHandler.test_versionId
......
......@@ -25,6 +25,7 @@ from ska_tango_base.control_model import (
AdminMode, ControlMode, HealthState, LoggingLevel, SimulationMode, TestMode
)
from ska_tango_base.base_device import (
_DEBUGGER_PORT,
_Log4TangoLoggingLevel,
_PYTHON_TO_TANGO_LOGGING_LEVEL,
LoggingUtils,
......@@ -425,7 +426,7 @@ class TestSKABaseDevice(object):
"""Test for GetVersionInfo"""
# PROTECTED REGION ID(SKABaseDevice.test_GetVersionInfo) ENABLED START #
versionPattern = re.compile(
r'SKABaseDevice, ska_tango_base, [0-9].[0-9].[0-9], '
r'SKABaseDevice, ska_tango_base, [0-9]+.[0-9]+.[0-9]+, '
r'A set of generic base devices for SKA Telescope.')
versionInfo = tango_context.device.GetVersionInfo()
assert (re.match(versionPattern, versionInfo[0])) is not None
......@@ -508,7 +509,7 @@ class TestSKABaseDevice(object):
"""Test for buildState"""
# PROTECTED REGION ID(SKABaseDevice.test_buildState) ENABLED START #
buildPattern = re.compile(
r'ska_tango_base, [0-9].[0-9].[0-9], '
r'ska_tango_base, [0-9]+.[0-9]+.[0-9]+, '
r'A set of generic base devices for SKA Telescope')
assert (re.match(buildPattern, tango_context.device.buildState)) is not None
# PROTECTED REGION END # // SKABaseDevice.test_buildState
......@@ -518,7 +519,7 @@ class TestSKABaseDevice(object):
def test_versionId(self, tango_context):
"""Test for versionId"""
# PROTECTED REGION ID(SKABaseDevice.test_versionId) ENABLED START #
versionIdPattern = re.compile(r'[0-9].[0-9].[0-9]')
versionIdPattern = re.compile(r'[0-9]+.[0-9]+.[0-9]+')
assert (re.match(versionIdPattern, tango_context.device.versionId)) is not None
# PROTECTED REGION END # // SKABaseDevice.test_versionId
......@@ -649,6 +650,31 @@ class TestSKABaseDevice(object):
assert tango_context.device.testMode == TestMode.NONE
# PROTECTED REGION END # // SKABaseDevice.test_testMode
def test_debugger_not_listening_by_default(self, tango_context):
assert not SKABaseDevice._global_debugger_listening
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
with pytest.raises(ConnectionRefusedError):
s.connect(("localhost", _DEBUGGER_PORT))
def test_DebugDevice_starts_listening(self, tango_context):
port = tango_context.device.DebugDevice()
assert port == _DEBUGGER_PORT
assert SKABaseDevice._global_debugger_listening
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect(("localhost", _DEBUGGER_PORT))
assert tango_context.device.state
def test_DebugDevice_twice_does_not_raise(self, tango_context):
tango_context.device.DebugDevice()
tango_context.device.DebugDevice()
assert SKABaseDevice._global_debugger_listening
def test_DebugDevice_does_not_break_a_command(self, tango_context):
tango_context.device.DebugDevice()
assert tango_context.device.State() == DevState.OFF
tango_context.device.On()
assert tango_context.device.State() == DevState.ON
class TestSKABaseDevice_commands:
"""
......
......@@ -88,7 +88,7 @@ class TestSKACapability(object):
"""Test for buildState"""
# PROTECTED REGION ID(SKACapability.test_buildState) ENABLED START #
buildPattern = re.compile(
r'ska_tango_base, [0-9].[0-9].[0-9], '
r'ska_tango_base, [0-9]+.[0-9]+.[0-9]+, '
r'A set of generic base devices for SKA Telescope')
assert (re.match(buildPattern, tango_context.device.buildState)) is not None
# PROTECTED REGION END # // SKACapability.test_buildState
......@@ -98,7 +98,7 @@ class TestSKACapability(object):
def test_versionId(self, tango_context):
"""Test for versionId"""
# PROTECTED REGION ID(SKACapability.test_versionId) ENABLED START #
versionIdPattern = re.compile(r'[0-9].[0-9].[0-9]')
versionIdPattern = re.compile(r'[0-9]+.[0-9]+.[0-9]+')
assert (re.match(versionIdPattern, tango_context.device.versionId)) is not None
# PROTECTED REGION END # // SKACapability.test_versionId
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment