Explicitly support cancelling tasks.

The 'cancelled' status is now tracked by a Future that's passed to
different asychronous tasks. That way it is possible to cancel all
running tasks before browsing another Pillar node.
This commit is contained in:
2016-03-15 14:05:54 +01:00
parent 0174c28075
commit 5e237bea22
4 changed files with 174 additions and 56 deletions

View File

@@ -65,6 +65,7 @@ class MenuItem:
self.label_text = label_text
self._thumb_path = ''
self.icon = None
self._is_folder = file_desc is None and thumb_path == 'FOLDER'
self.thumb_path = thumb_path
@@ -93,7 +94,7 @@ class MenuItem:
@property
def is_folder(self) -> bool:
return self.file_desc is None
return self._is_folder
def update_placement(self, x, y, width, height):
"""Use OpenGL to draw this one menu item."""
@@ -161,6 +162,7 @@ class BlenderCloudBrowser(bpy.types.Operator):
project_uuid = '5672beecc0261b2005ed1a33' # Blender Cloud project UUID
node_uuid = '' # Blender Cloud node UUID
async_task = None # asyncio task for fetching thumbnails
signalling_future = None # asyncio future for signalling that we want to cancel everything.
timer = None
log = logging.getLogger('%s.BlenderCloudBrowser' % __name__)
@@ -193,7 +195,7 @@ class BlenderCloudBrowser(bpy.types.Operator):
self.current_display_content = []
self.loaded_images = set()
self.browse_assets(context)
self.browse_assets()
context.window_manager.modal_handler_add(self)
self.timer = context.window_manager.event_timer_add(1/30, context.window)
@@ -219,7 +221,7 @@ class BlenderCloudBrowser(bpy.types.Operator):
if selected.is_folder:
self.node_uuid = selected.node_uuid
self.browse_assets(context)
self.browse_assets()
else:
self.handle_item_selection(selected)
self._finish(context)
@@ -232,14 +234,33 @@ class BlenderCloudBrowser(bpy.types.Operator):
return {'RUNNING_MODAL'}
def _stop_async_task(self):
self.log.debug('Stopping async task')
if self.async_task is None:
self.log.debug('No async task, trivially stopped')
return
# Signal that we want to stop.
if not self.signalling_future.done():
self.log.info("Signalling that we want to cancel anything that's running.")
self.signalling_future.cancel()
# Wait until the asynchronous task is done.
if not self.async_task.done():
print('Cancelling running async download task {}'.format(self.async_task))
self.async_task.cancel()
else:
self.log.info("blocking until async task is done.")
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(self.async_task)
except asyncio.CancelledError:
self.log.info('Asynchronous task was cancelled')
return
# noinspection PyBroadException
try:
self.async_task.result() # This re-raises any exception of the task.
except asyncio.CancelledError:
self.log.info('Asynchronous task was cancelled')
except Exception:
self.log.exception("Exception from asynchronous task")
def _finish(self, context):
self.log.debug('Finishing the modal operator')
@@ -283,74 +304,69 @@ class BlenderCloudBrowser(bpy.types.Operator):
else:
raise ValueError('Unable to find MenuItem(node_uuid=%r)' % node_uuid)
async def async_download_previews(self, context, thumbnails_directory):
# If we have a node UUID, we fetch the textures
# FIXME: support mixture of sub-nodes and textures under one node.
async def async_download_previews(self, thumbnails_directory):
self.log.info('Asynchronously downloading previews to %r', thumbnails_directory)
self.clear_images()
def redraw():
# region = context.region
# if region is None:
# print('Unable to redraw, region is %s' % region)
# print(' (context is %s)' % context)
# return
# region.tag_redraw()
pass
def thumbnail_loading(node_uuid, texture_node):
self.add_menu_item(node_uuid, None, 'SPINNER', texture_node['name'])
redraw()
def thumbnail_loaded(node_uuid, file_desc, thumb_path):
# update MenuItem added above
self.update_menu_item(node_uuid, file_desc, thumb_path, file_desc['filename'])
redraw()
if self.node_uuid:
self.log.debug('Getting subnodes for parent node %r', self.node_uuid)
children = await pillar.get_nodes(parent_node_uuid=self.node_uuid,
node_type='group_textures')
self.log.debug('Finding parent of node %r', self.node_uuid)
# Make sure we can go up again.
parent_uuid = await pillar.parent_node_uuid(self.node_uuid)
self.add_menu_item(parent_uuid, None, 'FOLDER', '.. up ..')
self.log.debug('Iterating over child nodes of %r', self.node_uuid)
for child in children:
# print(' - %(_id)s = %(name)s' % child)
self.add_menu_item(child['_id'], None, 'FOLDER', child['name'])
directory = os.path.join(thumbnails_directory, self.project_uuid, self.node_uuid)
os.makedirs(directory, exist_ok=True)
self.log.debug('Fetching texture thumbnails for node %r', self.node_uuid)
await pillar.fetch_texture_thumbs(self.node_uuid, 's', directory,
thumbnail_loading=thumbnail_loading,
thumbnail_loaded=thumbnail_loaded)
thumbnail_loaded=thumbnail_loaded,
future=self.signalling_future)
elif self.project_uuid:
self.log.debug('Getting subnodes for project node %r', self.project_uuid)
children = await pillar.get_nodes(self.project_uuid, '')
self.log.debug('Iterating over child nodes of project %r', self.project_uuid)
for child in children:
print(' - %(_id)s = %(name)s' % child)
# print(' - %(_id)s = %(name)s' % child)
self.add_menu_item(child['_id'], None, 'FOLDER', child['name'])
redraw()
else:
# TODO: add "nothing here" icon and trigger re-draw
redraw()
self.log.warning("Not node UUID and no project UUID, I can't do anything!")
pass
# Call the 'done' callback.
loop = asyncio.get_event_loop()
loop.call_soon_threadsafe(self.downloading_done)
def browse_assets(self, context):
def browse_assets(self):
self.log.debug('Browsing assets at project %r node %r', self.project_uuid, self.node_uuid)
self._stop_async_task()
self.clear_images()
# Download the previews asynchronously.
self.signalling_future = asyncio.Future()
self.async_task = asyncio.ensure_future(
self.async_download_previews(context, self.thumbnails_cache))
self.async_download_previews(self.thumbnails_cache))
# Start the async manager so everything happens.
async_loop.ensure_async_loop()
def downloading_done(self):
# if not self.async_task.done():
# print('%s: aborting download task' % self)
# self._stop_async_task()
# else:
# print('%s: downloading done' % self)
# self.async_task.result()
pass
self.log.info('Done downloading thumbnails.')
def draw_menu(self, context):
margin_x = 20