blob: cf6e6dc440ec156353ed2f9c09d7100e5f5c8ce6 [file] [log] [blame]
# Copyright 1999-2018 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2
import signal
from portage import os
from portage.util.futures import asyncio
from portage.util.SlotObject import SlotObject
class AsynchronousTask(SlotObject):
"""
Subclasses override _wait() and _poll() so that calls
to public methods can be wrapped for implementing
hooks such as exit listener notification.
Sublasses should call self._async_wait() to notify exit listeners after
the task is complete and self.returncode has been set.
"""
__slots__ = ("background", "cancelled", "returncode", "scheduler") + \
("_exit_listeners", "_exit_listener_stack", "_start_listeners")
_cancelled_returncode = - signal.SIGINT
def start(self):
"""
Start an asynchronous task and then return as soon as possible.
"""
self._start_hook()
self._start()
def async_wait(self):
"""
Wait for returncode asynchronously. Notification is available
via the add_done_callback method of the returned Future instance.
@returns: Future, result is self.returncode
"""
waiter = self.scheduler.create_future()
exit_listener = lambda self: waiter.set_result(self.returncode)
self.addExitListener(exit_listener)
waiter.add_done_callback(lambda waiter:
self.removeExitListener(exit_listener) if waiter.cancelled() else None)
if self.returncode is not None:
# If the returncode is not None, it means the exit event has already
# happened, so use _async_wait() to guarantee that the exit_listener
# is called. This does not do any harm because a given exit listener
# is never called more than once.
self._async_wait()
return waiter
def _start(self):
self.returncode = os.EX_OK
self._async_wait()
def isAlive(self):
return self.returncode is None
def poll(self):
if self.returncode is not None:
return self.returncode
self._poll()
self._wait_hook()
return self.returncode
def _poll(self):
return self.returncode
def wait(self):
"""
Wait for the returncode attribute to become ready, and return
it. If the returncode is not ready and the event loop is already
running, then the async_wait() method should be used instead of
wait(), because wait() will raise asyncio.InvalidStateError in
this case.
@rtype: int
@returns: the value of self.returncode
"""
if self.returncode is None:
if self.scheduler.is_running():
raise asyncio.InvalidStateError('Result is not ready.')
self.scheduler.run_until_complete(self.async_wait())
self._wait_hook()
return self.returncode
def _async_wait(self):
"""
For cases where _start exits synchronously, this method is a
convenient way to trigger an asynchronous call to self.wait()
(in order to notify exit listeners), avoiding excessive event
loop recursion (or stack overflow) that synchronous calling of
exit listeners can cause. This method is thread-safe.
"""
self.scheduler.call_soon(self.wait)
def cancel(self):
"""
Cancel the task, but do not wait for exit status. If asynchronous exit
notification is desired, then use addExitListener to add a listener
before calling this method.
NOTE: Synchronous waiting for status is not supported, since it would
be vulnerable to hitting the recursion limit when a large number of
tasks need to be terminated simultaneously, like in bug #402335.
"""
if not self.cancelled:
self.cancelled = True
self._cancel()
def _cancel(self):
"""
Subclasses should implement this, as a template method
to be called by AsynchronousTask.cancel().
"""
pass
def _was_cancelled(self):
"""
If cancelled, set returncode if necessary and return True.
Otherwise, return False.
"""
if self.cancelled:
if self.returncode is None:
self.returncode = self._cancelled_returncode
return True
return False
def addStartListener(self, f):
"""
The function will be called with one argument, a reference to self.
"""
if self._start_listeners is None:
self._start_listeners = []
self._start_listeners.append(f)
def removeStartListener(self, f):
if self._start_listeners is None:
return
self._start_listeners.remove(f)
def _start_hook(self):
if self._start_listeners is not None:
start_listeners = self._start_listeners
self._start_listeners = None
for f in start_listeners:
f(self)
def addExitListener(self, f):
"""
The function will be called with one argument, a reference to self.
"""
if self._exit_listeners is None:
self._exit_listeners = []
self._exit_listeners.append(f)
def removeExitListener(self, f):
if self._exit_listeners is None:
if self._exit_listener_stack is not None:
self._exit_listener_stack.remove(f)
return
self._exit_listeners.remove(f)
def _wait_hook(self):
"""
Call this method after the task completes, just before returning
the returncode from wait() or poll(). This hook is
used to trigger exit listeners when the returncode first
becomes available.
"""
if self.returncode is not None and \
self._exit_listeners is not None:
# This prevents recursion, in case one of the
# exit handlers triggers this method again by
# calling wait(). Use a stack that gives
# removeExitListener() an opportunity to consume
# listeners from the stack, before they can get
# called below. This is necessary because a call
# to one exit listener may result in a call to
# removeExitListener() for another listener on
# the stack. That listener needs to be removed
# from the stack since it would be inconsistent
# to call it after it has been been passed into
# removeExitListener().
self._exit_listener_stack = self._exit_listeners
self._exit_listeners = None
# Execute exit listeners in reverse order, so that
# the last added listener is executed first. This
# allows SequentialTaskQueue to decrement its running
# task count as soon as one of its tasks exits, so that
# the value is accurate when other listeners execute.
while self._exit_listener_stack:
self._exit_listener_stack.pop()(self)