Repository : http://git.fedorahosted.org/cgit/cura-tools.git
On branch : master
>---------------------------------------------------------------
commit 27fe64115ab037f95c30b6d465f14a62a12b1ae3
Author: Peter Hatina <phatina(a)redhat.com>
Date: Wed Oct 9 07:43:34 2013 +0200
introduce SIGINT and SIGTERM signal handling
When performing a synchronous method call, such call and job can be aborted by sending SIGINT or SIGTERM to the LMIShell.
>---------------------------------------------------------------
cli/lmi/shell/LMIMethod.py | 205 ++++++++++++++++++++++++++++++++++++-------
1 files changed, 171 insertions(+), 34 deletions(-)
diff --git a/cli/lmi/shell/LMIMethod.py b/cli/lmi/shell/LMIMethod.py
index 92faf65..b4f265f 100644
--- a/cli/lmi/shell/LMIMethod.py
+++ b/cli/lmi/shell/LMIMethod.py
@@ -14,11 +14,12 @@
# along with this program; if not, see <http://www.gnu.org/licenses/>.
import sys
-import time
import pywbem
+import signal
import socket
import urlparse
import threading
+import collections
from LMIBaseObject import LMIWrapperBaseObject
from LMIBaseClient import LMIBaseClient
@@ -50,6 +51,90 @@ from LMIExceptions import LMISynchroMethodCallFilterError
from LMIExceptions import LMIUnknownParameterError
from LMIExceptions import LMIHandlerNamePatternError
+class LMISignalHelper(object):
+ """
+ Helper class, which takes care of signal (de)registration and handling.
+ """
+
+ _instance = None
+
+ def __new__(cls):
+ """
+ Creates a new LMISignalHelper instance, if not created, and returns
+ the singleton instance object.
+ """
+ if cls._instance is None:
+ cls._instance = super(LMISignalHelper, cls).__new__(cls)
+ LMISignalHelper.reset(cls._instance)
+ return cls._instance
+
+ def reset(self):
+ """
+ Resets the single instance into default state.
+ """
+ self._handler_sigint = None
+ self._handler_sigterm = None
+ self._signal_handled = False
+ self._instance._callbacks = collections.OrderedDict()
+
+ def signal_attach(self):
+ """
+ Registers SIGINT and SIGTERM signals to local handler in which, the
+ flags for each signal are modified, if such signal is caught.
+ """
+ self._signal_handled = False
+ self._handler_sigint = signal.signal(signal.SIGINT, LMISignalHelper.__signal_handler)
+ self._handler_sigterm = signal.signal(signal.SIGTERM, LMISignalHelper.__signal_handler)
+
+ def signal_detach(self):
+ """
+ Deregisters SIGINT and SIGTERM handler and removes all the attached callbacks.
+ """
+ signal.signal(signal.SIGINT, self._handler_sigint)
+ signal.signal(signal.SIGTERM, self._handler_sigterm)
+
+ def signal_handled(self):
+ """
+ Returns True, if any of SIGINT or SIGTERM has been caught; False otherwise.
+ """
+ return self._signal_handled
+
+ def callback_attach(self, cb_name, cb):
+ """
+ Registers a callback, which will be called when a SIGINT or SIGTERM is caught.
+
+ Arguments:
+ cb_name -- string with name of the callback
+ cb -- callable object, which takes zero arguments
+ """
+ self._callbacks[cb_name] = cb
+
+ def callback_detach(self, cb_name):
+ """
+ Removes a callback from the callback dictionary.
+
+ Arguments:
+ cb_name -- string containing callback name
+ """
+ self._callbacks.pop(cb_name)
+
+ @staticmethod
+ def __signal_handler(signo, frame):
+ """
+ Signal handler, which is called, whem SIGINT and SIGTERM are sent to
+ the LMIShell.
+
+ Arguments:
+ signo -- signal number
+ frame -- stack frame
+
+ NOTE: see help(signal)
+ """
+ if signo in (signal.SIGINT, signal.SIGTERM):
+ LMISignalHelper._instance._signal_handled = True
+ for cb in LMISignalHelper._instance._callbacks.values():
+ cb()
+
class LMIMethod(LMIWrapperBaseObject):
"""
LMI wrapper class representing CIMMethod.
@@ -255,37 +340,40 @@ class LMIMethod(LMIWrapperBaseObject):
if lmi_is_job_finished(job_inst):
job_finished.value = JOB_FINISH_EARLY
+ # Register signal callback for SIGINT, SIGTERM with callback,
+ # which awakes waiting thread for immediate return.
+ LMISignalHelper().callback_attach("indication", lambda: LMIMethod.__wake(cond))
+ LMISignalHelper().signal_attach()
+
# Wait for the job to finish
- interrupt = False
- try:
- wake_cnt = 0
- cond.acquire()
- while True:
- if interrupt or job_finished.value:
+ wake_cnt = 0
+ cond.acquire()
+ while not LMISignalHelper().signal_handled() and \
+ not job_finished.value and \
+ not lmi_is_job_finished(job_inst):
+ cond.wait(LMIMethod._COND_WAIT_TIME)
+ wake_cnt += 1
+ # XXX: threading.Condition.wait() does not inform about timeout or being awaken by
+ # notify call. There is a counting to 4 sleep cycles before we actually check for
+ # job status manually. This number can be increased, so we rely more on indications,
+ # rather then on manual polling.
+ if wake_cnt >= LMIMethod._COND_WAIT_WAKE_CNT and \
+ not job_finished.value:
+ wake_cnt = 0
+ try:
+ (refreshed, _, errorstr) = job_inst.refresh()
+ except pywbem.cim_operations.CIMError, e:
+ job_exception.value = e
+ break
+ if not refreshed:
+ job_exception.value = LMISynchroMethodCallError(errorstr)
break
- cond.wait(LMIMethod._COND_WAIT_TIME)
- wake_cnt += 1
- # XXX: threading.Condition.wait() does not inform about timeout or being awaken by
- # notify call. There is a counting to 4 sleep cycles before we actually check for
- # job status manually. This number can be increased, so we rely more on indications,
- # rather then on manual polling.
- if not job_finished.value and wake_cnt >= LMIMethod._COND_WAIT_WAKE_CNT:
- wake_cnt = 0
- try:
- (refreshed, _, errorstr) = job_inst.refresh()
- except pywbem.cim_operations.CIMError, e:
- job_exception.value = e
- break
- if not refreshed:
- job_exception.value = LMISynchroMethodCallError(errorstr)
- break
- if lmi_is_job_finished(job_inst):
- listener.stop()
- break
- except KeyboardInterrupt:
- interrupt = True
- finally:
- cond.release()
+
+ # Unregister signal handler
+ LMISignalHelper().signal_detach()
+ LMISignalHelper().callback_detach("indication")
+
+ cond.release()
# Cleanup
listener.stop()
@@ -293,6 +381,13 @@ class LMIMethod(LMIWrapperBaseObject):
self._conn._client._delete_instance(cim_handler.path)
if job_exception.value:
raise job_exception.value
+ if LMISignalHelper().signal_handled() and not job_finished.value:
+ # We got SIGINT or SIGTERM, when waiting for the job, cancelling the job
+ sys.stderr.write("Cancelling a job '%s'\n" % job_inst.Name)
+ job_inst.RequestStateChange(
+ RequestedState=job_inst.RequestStateChange.RequestedStateValues.Terminate
+ )
+ return LMIReturnValue(rval=None)
# Return the job return values, refresh the job_inst object, if we got notified
# about the job finish state by indication.
@@ -311,18 +406,48 @@ class LMIMethod(LMIWrapperBaseObject):
Arguments:
job_inst -- LMIInstance of a job
"""
+
+ # Register signal callback for SIGINT, SIGTERM with callback,
+ # which awakes waiting thread for immediate return.
+ LMISignalHelper().callback_attach("polling", lambda: LMIMethod.__wake(cond))
+ LMISignalHelper().signal_attach()
+
+ cond = threading.Condition()
+ cond.acquire()
+
+ job_exception = None
+
try:
sleep_time = 1
- while not lmi_is_job_finished(job_inst):
+ while not LMISignalHelper().signal_handled() and \
+ not lmi_is_job_finished(job_inst):
# Sleep, a bit longer in every iteration
- time.sleep(sleep_time)
+ cond.wait(sleep_time)
if sleep_time < LMIMethod._POLLING_ADAPT_MAX_WAITING_TIME:
sleep_time *= 2
(refreshed, _, errorstr) = job_inst.refresh()
if not refreshed:
- raise LMISynchroMethodCallError(errorstr)
+ job_exception = LMISynchroMethodCallError(errorstr)
+ break
except pywbem.cim_operations.CIMError, e:
- raise LMISynchroMethodCallError(e.message)
+ job_exception = LMISynchroMethodCallError(e.message)
+ finally:
+ cond.release()
+
+ # Unregister signal handler and callback
+ LMISignalHelper().signal_detach()
+ LMISignalHelper().callback_detach("polling")
+
+ if LMISignalHelper().signal_handled() and not lmi_is_job_finished(job_inst):
+ # We got SIGINT or SIGTERM, when waiting for the job, cancelling the job
+ sys.stderr.write("Cancelling a job '%s'\n" % job_inst.Name)
+ job_inst.RequestStateChange(
+ RequestedState=job_inst.RequestStateChange.RequestedStateValues.Terminate
+ )
+ return LMIReturnValue(rval=None)
+
+ if not job_exception is None:
+ raise job_exception
# Return the job return values. No need to refresh the job instance, we already
# have a "fresh" one.
@@ -438,6 +563,18 @@ class LMIMethod(LMIWrapperBaseObject):
return LMIConstantValuesMethodReturnType(self._method)
raise AttributeError(name)
+ @staticmethod
+ def __wake(cond):
+ """
+ Helper function used for manual threading.Condition wakeup.
+
+ Arguments:
+ cond -- threading.Condition object
+ """
+ cond.acquire()
+ cond.notify()
+ cond.release()
+
@property
def return_type(self):
"""