Skip to content

Commit

Permalink
Improvements for port in use handling and other cleanup in listener
Browse files Browse the repository at this point in the history
Details:
- Added support for using WBEMListener as a context manager that ensures
  that the listener is stopped when leaving its scope.
- Improved the exception message that occurs when listener port is in use.
- Added `http_started` and `https_started` properties to WBEMListener, that
  indicate whether the listener is started for the respective port.
- Docs:
  - Clarified that listener must be stopped to release the TCP/IP port.
  - Outdented comments in example to right level.
  - Removed WBEMServer code from example.
  - Added example for using WBEMListener as a context manager.
- Test:
  - Added test case for listener port in use.
  - Added test case for using WBEMListener as a context manager.
  - Cleaned up the test method `send_indications()` to remove the unused
    `https_port` argument.
  - Moved printing of sent indications in test method to be done only when
    VERBOSE is set.

Signed-off-by: Andreas Maier <maiera@de.ibm.com>
  • Loading branch information
andy-maier committed Feb 21, 2018
1 parent 5f225a4 commit 7bf40ac
Show file tree
Hide file tree
Showing 2 changed files with 157 additions and 31 deletions.
118 changes: 97 additions & 21 deletions pywbem/_listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,26 +44,30 @@ def main():
certkeyfile = 'listener.pem'
url1 = 'http://server1'
conn1 = WBEMConnection(url1)
server1 = WBEMServer(conn1)
server1.determine_interop_ns()
url2 = 'http://server2'
conn2 = WBEMConnection(url2)
server2 = WBEMServer(conn2)
server2.validate_interop_ns('root/PG_InterOp')
my_listener = WBEMListener(host=getfqdn()
listener = WBEMListener(host=getfqdn()
http_port=5988,
https_port=5989,
certfile=certkeyfile,
keyfile=certkeyfile)
my_listener.add_callback(process_indication)
listener.add_callback(process_indication)
try:
listener.start()
# process_indication() will be called for each received indication
finally:
listener.stop()
Alternative code using the class a context manager::
with WBEMListener(...) as listener:
listener.add_callback(process_indication)
listener.start()
# listener runs until executable terminated
# or listener.stop()
# process_indication() will be called for each received indication
# listener.stop() has been called automatically
See the example in section :ref:`WBEMSubscriptionManager` for an example of
using a listener in combination with a subscription manager.
Expand All @@ -73,6 +77,8 @@ def main():
creates a listener and displays any indications it receives, in MOF format.
"""

import sys
import errno
import re
import logging
try: # Python 2.7+
Expand Down Expand Up @@ -549,6 +555,10 @@ class WBEMListener(object):
The listener supports starting and stopping threads that listen for
CIM-XML ExportIndication messages using HTTP and/or HTTPS, and that pass
any received indications on to registered callback functions.
The listener must be stopped in order to free the TCP/IP port it listens
on. Using this class as a context manager ensures that the listener is
stopped when leaving the context manager scope.
"""

def __init__(self, host, http_port=None, https_port=None,
Expand Down Expand Up @@ -649,6 +659,27 @@ def __repr__(self):
self.https_port, self.certfile, self.keyfile, self.logger,
self._callbacks)

def __enter__(self):
"""
*New in pywbem 0.12.*
Enter method when the class is used as a context manager.
Returns the listener object.
"""
return self

def __exit__(self, exc_type, exc_value, traceback):
"""
*New in pywbem 0.12.*
Exit method when the class is used as a context manager.
Stops the listener by calling :meth:`~pywbem.WBEMListener.stop`.
"""
self.stop()
return False # re-raise any exceptions

@property
def host(self):
"""The IP address or host name this listener can be reached at,
Expand All @@ -673,6 +704,26 @@ def https_port(self):
"""
return self._https_port

@property
def http_started(self):
"""
Return a boolean indicating whether the listener is started for the
HTTP port.
If no port is set up for HTTP, `False` is returned.
"""
return self._http_server is not None

@property
def https_started(self):
"""
Return a boolean indicating whether the listener is started for the
HTTPS port.
If no port is set up for HTTPS, `False` is returned.
"""
return self._https_server is not None

@property
def certfile(self):
"""
Expand Down Expand Up @@ -741,15 +792,29 @@ def start(self):
described in :term:`DSP0200` and they will invoke the registered
callback functions for any received CIM indications.
These server threads can be stopped using the
:meth:`~pywbem.WBEMListener.stop` method.
They will be automatically stopped when the main thread terminates.
The listener must be stopped again in order to free the TCP/IP port it
listens on. The listener can be stopped explicitly using the
:meth:`~pywbem.WBEMListener.stop` method. The listener will be
automatically stopped when the main thread terminates (i.e. when the
Python process terminates), or when :class:`~pywbem.WBEMListener`
is used as a context manager when leaving its scope.
"""

if self._http_port:
if not self._http_server:
server = ThreadedHTTPServer((self._host, self._http_port),
ListenerRequestHandler)
try:
server = ThreadedHTTPServer((self._host, self._http_port),
ListenerRequestHandler)
except Exception as exc:
# In Python 2: socket.error; In Python 3: OSError
if getattr(exc, 'errno', None) == errno.EADDRINUSE:
# Reraise with improved error message
msg = "WBEM listener port %s already in use" % \
self._http_port
exc_type = type(exc)
six.reraise(exc_type, exc_type(errno.EADDRINUSE, msg),
sys.exc_info()[2])
raise

# pylint: disable=attribute-defined-outside-init
server.listener = self
Expand All @@ -765,8 +830,19 @@ def start(self):

if self._https_port:
if not self._https_server:
server = ThreadedHTTPServer((self._host, self._https_port),
ListenerRequestHandler)
try:
server = ThreadedHTTPServer((self._host, self._https_port),
ListenerRequestHandler)
except Exception as exc:
# In Python 2: socket.error; In Python 3: OSError
if getattr(exc, 'errno', None) == errno.EADDRINUSE:
# Reraise with improved error message
msg = "WBEM listener port %s already in use" % \
self._http_port
exc_type = type(exc)
six.reraise(exc_type, exc_type(errno.EADDRINUSE, msg),
sys.exc_info()[2])
raise

# pylint: disable=attribute-defined-outside-init
server.listener = self
Expand Down
70 changes: 60 additions & 10 deletions testsuite/test_indicationlistener.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@
from time import time
import datetime
from random import randint
import socket
import errno
import requests
import six

from pywbem import WBEMListener

Expand Down Expand Up @@ -167,14 +170,16 @@ def createlistener(host, http_port=None, https_port=None,
LISTENER.start()

# pylint: disable=unused-argument
def send_indications(self, send_count, http_port, https_port):
def send_indications(self, send_count, http_port):
"""
Send the number of indications defined by the send_count attribute
using the specified listener HTTP port.
Creates the listener, starts the listener, creates the
indication XML and adds sequence number and time to the
indication instance and sends that instance using requests.
The indication instance is modified for each indication count so
that each carries its own sequence number
that each carries its own sequence number.
"""

# pylint: disable=global-variable-not-assigned
Expand Down Expand Up @@ -220,8 +225,9 @@ def send_indications(self, send_count, http_port, https_port):
self.fail('Error return from send. Terminating.')

endtime = timer.elapsed_sec()
print('Sent %s indications in %s sec or %.2f ind/sec' %
(send_count, endtime, (send_count / endtime)))
if VERBOSE:
print('Sent %s indications in %s sec or %.2f ind/sec' %
(send_count, endtime, (send_count / endtime)))

self.assertEqual(send_count, RCV_COUNT,
'Mismatch between sent and rcvd')
Expand All @@ -232,20 +238,64 @@ def send_indications(self, send_count, http_port, https_port):

def test_send_10(self):
"""Test with sending 10 indications"""
self.send_indications(10, 50000, None)
self.send_indications(10, 50000)

def test_send_100(self):
"""Test sending 100 indications"""
self.send_indications(100, 50000, None)
self.send_indications(100, 50000)

# Disabled the following tests, because in some environments it takes 30min.
# def test_send_1000(self):
# """Test sending 1000 indications"""
# self.send_indications(1000, 5002, None)
# self.send_indications(1000, 50000)

def test_port_in_use(self):
"""
Test starting the listener when port is in use by another listener.
"""

host = 'localhost'

# Don't use this port in other tests, to be on the safe side
# as far as port reuse is concerned.
http_port = '59999'

exp_exc_type = socket.error if six.PY2 else OSError

listener1 = WBEMListener(host, http_port)
listener1.start()
assert listener1.http_started is True

listener2 = WBEMListener(host, http_port)
try:
listener2.start()
except exp_exc_type as exc:
if exc.errno != errno.EADDRINUSE:
raise

listener1.stop()
assert listener1.http_started is False

def test_context_mgr(self):
"""
Test starting the listener and automatic closing in a context manager.
"""

host = 'localhost'

# Don't use this port in other tests, to be on the safe side
# as far as port reuse is concerned.
http_port = '59998'

with WBEMListener(host, http_port) as listener1:
assert isinstance(listener1, WBEMListener)
listener1.start()
assert listener1.http_started is True
assert listener1.http_started is False

# This test takes about 60 seconds and so is disabled for now
# def test_send_10000(self):
# self.send_indications(10000)
listener2 = WBEMListener(host, http_port)
listener2.start() # verify this works -> port is not in use
listener2.stop()


if __name__ == '__main__':
Expand Down

0 comments on commit 7bf40ac

Please sign in to comment.