Coverage for pyanaconda/threading.py: 0%

# Coverage for pyanaconda/threading.py : 0%

## 126 statements 0 run 126 missing 0 excluded

Hot-keys on this page

r m x p toggle line displays

j k next/prev highlighted chunk

0 (zero) top of page

1 (one) first highlighted chunk

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 | # # threading.py: anaconda thread management # # Copyright (C) 2012 # Red Hat, Inc. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # import threading from pyanaconda.anaconda_loggers import get_module_logger log = get_module_logger(__name__) _WORKER_THREAD_PREFIX = "AnaWorkerThread" _WORKER_WAIT_THREAD = "AnaWaitThread" class ThreadManager(object): """A singleton class for managing threads and processes. Notes: THE INSTANCE HAS TO BE CREATED IN THE MAIN THREAD! This manager makes one assumption that contradicts python's threading module documentation. In this class, we assume that thread names are unique and meaningful. This is an okay assumption for us to make given that anaconda is only ever going to have a handful of special purpose threads. """ def __init__(self): self._objs = {} self._objs_lock = threading.RLock() self._errors = {} self._errors_lock = threading.RLock() self._main_thread = threading.main_thread() def set_current_thread_as_main(self): self._main_thread = threading.current_thread() def __call__(self): return self def add(self, obj): """Given a Thread or Process object, add it to the list of known objects and start it. It is assumed that obj.name is unique and descriptive. """ # we need to lock the thread dictionary when adding a new thread, # so that callers can't get & join threads that are not yet started with self._objs_lock: if obj.name in self._objs: raise KeyError("Cannot add thread '%s', a thread with the same name already running" % obj.name) self._objs[obj.name] = obj obj.start() return obj.name def remove(self, name): """Removes a thread from the list of known objects. This should only be called when a thread exits, or there will be no way to get a handle on it. """ with self._objs_lock: self._objs.pop(name) def exists(self, name): """Determine if a thread or process exists with the given name.""" # thread in the ThreadManager only officially exists once started with self._objs_lock: return name in self._objs def get(self, name): """Given an object name, see if it exists and return the object. Return None if no such object exists. Additionally, this method will re-raise any uncaught exception in the thread. """ # without the lock it would be possible to get & join # a thread that was not yet started with self._objs_lock: obj = self._objs.get(name) if obj: self.raise_if_error(name) return obj def wait(self, name): """Wait for the thread to exit and if the thread exited with an error re-raise it here. """ ret_val = True # we don't need a lock here, # because get() acquires it itself try: self.get(name).join() except AttributeError: ret_val = False # - if there is a thread object for the given name, # we join it # - if there is not a thread object for the given name, # we get None, try to join it, suppress the AttributeError # and return immediately self.raise_if_error(name) # return True if we waited for the thread, False otherwise return ret_val def wait_all(self): """Wait for all threads to exit and if there was an error re-raise it. """ with self._objs_lock: names = list(self._objs.keys()) for name in names: if self.get(name) == threading.current_thread(): continue log.debug("Waiting for thread %s to exit", name) self.wait(name) if self.any_errors: with self._errors_lock: thread_names = ", ".join(thread_name for thread_name in self._errors.keys() if self._errors[thread_name]) msg = "Unhandled errors from the following threads detected: %s" % thread_names raise RuntimeError(msg) def call_when_thread_terminates(self, thread_name, callback, *args, **kwargs): """Call the callback when thread with thread_name terminates. The callback will be called on the special thread so this is NOT thread safe. For the thread safe variant see the pyanaconda.async_utils.async_action_no_wait decorator. :param thread_name: Name of the thread to look on. :type thread_name: str :param callback: Call this callback when the thread terminates. :type callback: A function. Other args and kwargs passed here are passed to the callback. """ # Check and inject variables to kwargs if "_thread_name" is kwargs: log.error("The '_thread_name' variable can't be used in the callback!" "This variable will be overridden!") if "_callback" is kwargs: log.error("The '_callback' variable can't be used in the callback!" "This variable will be overridden!") kwargs["_thread_name"] = thread_name kwargs["_callback"] = callback thread = AnacondaThread(name=_WORKER_WAIT_THREAD, target=self._call_when_thread_terminates, args=args, kwargs=kwargs) self.add(thread) def _call_when_thread_terminates(self, _thread_name, _callback, *args, **kwargs): self.wait(_thread_name) _callback(*args, **kwargs) def set_error(self, name, *exc_info): """Set the error data for a thread The exception data is expected to be the tuple from sys.exc_info() """ with self._errors_lock: self._errors[name] = exc_info def get_error(self, name): """Get the error data for a thread using its name """ return self._errors.get(name) @property def any_errors(self): """Return True of there have been any errors in any threads """ with self._errors_lock: return any(self._errors.values()) def raise_if_error(self, name): """If a thread has failed due to an exception, raise it into the main thread and remove it from errors. """ if name not in self._errors: # no errors found for the thread return with self._errors_lock: exc_info = self._errors.pop(name) if exc_info: raise exc_info[0](exc_info[1]).with_traceback(exc_info[2]) def in_main_thread(self): """Return True if it is run in the main thread.""" cur_thread = threading.current_thread() return cur_thread is self._main_thread @property def running(self): """ Return the number of running threads. :returns: number of running threads :rtype: int """ with self._objs_lock: return len(self._objs) @property def names(self): """ Return the names of the running threads. :returns: list of thread names :rtype: list of strings """ with self._objs_lock: return list(self._objs.keys()) def wait_for_error_threads(self): """ Waits for all threads that caused exceptions. In other words, waits for exception handling (possibly interactive) to be finished. """ with self._errors_lock: for thread_name in self._errors.keys(): thread = self._objs[thread_name] thread.join() class AnacondaThread(threading.Thread): """A threading.Thread subclass that exists only for a couple purposes: (1) Make exceptions that happen in a thread invoke our exception handling code as well. Otherwise, threads will silently die and we are doing a lot of complicated code in them now. (2) Remove themselves from the thread manager when completed. (3) All created threads are made daemonic, which means anaconda will quit when the main process is killed. """ # class-wide dictionary ensuring unique thread names _prefix_thread_counts = dict() def __init__(self, *args, **kwargs): # if neither name nor prefix is given, use the worker prefix if "name" not in kwargs and "prefix" not in kwargs: kwargs["prefix"] = _WORKER_THREAD_PREFIX # if prefix is specified, use it to construct new thread name prefix = kwargs.pop("prefix", None) if prefix: thread_num = self._prefix_thread_counts.get(prefix, 0) + 1 self._prefix_thread_counts[prefix] = thread_num kwargs["name"] = prefix + str(thread_num) if "fatal" in kwargs: self._fatal = kwargs.pop("fatal") else: self._fatal = True super().__init__(*args, **kwargs) self.daemon = True def run(self): # http://bugs.python.org/issue1230540#msg25696 import sys log.info("Running Thread: %s (%s)", self.name, self.ident) try: threading.Thread.run(self) # pylint: disable=bare-except except: if self._fatal: sys.excepthook(*sys.exc_info()) else: threadMgr.set_error(self.name, *sys.exc_info()) finally: threadMgr.remove(self.name) log.info("Thread Done: %s (%s)", self.name, self.ident) def initThreading(): """Set up threading for anaconda's use. This method must be called before any GTK or threading code is called, or else threads will only run when an event is triggered in the GTK main loop. And IT HAS TO BE CALLED IN THE MAIN THREAD. """ threadMgr.set_current_thread_as_main() threadMgr = ThreadManager()