diff --git a/multi_user/__init__.py b/multi_user/__init__.py index 0d5577b..1533252 100644 --- a/multi_user/__init__.py +++ b/multi_user/__init__.py @@ -59,6 +59,7 @@ def register(): from . import presence from . import operators + from . import handlers from . import ui from . import preferences from . import addon_updater_ops @@ -67,6 +68,7 @@ def register(): addon_updater_ops.register(bl_info) presence.register() operators.register() + handlers.register() ui.register() except ModuleNotFoundError as e: raise Exception(module_error_msg) @@ -87,6 +89,7 @@ def register(): def unregister(): from . import presence from . import operators + from . import handlers from . import ui from . import preferences from . import addon_updater_ops @@ -96,6 +99,7 @@ def unregister(): presence.unregister() addon_updater_ops.unregister() ui.unregister() + handlers.unregister() operators.unregister() preferences.unregister() diff --git a/multi_user/handlers.py b/multi_user/handlers.py new file mode 100644 index 0000000..f5033c9 --- /dev/null +++ b/multi_user/handlers.py @@ -0,0 +1,150 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# 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 3 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 . +# +# ##### END GPL LICENSE BLOCK ##### + +import logging + +import bpy +from bpy.app.handlers import persistent +from replication import porcelain +from replication.constants import RP_COMMON, STATE_ACTIVE, STATE_SYNCING, UP +from replication.exception import ContextError, NonAuthorizedOperationError +from replication.interface import session + +from . import shared_data, utils + + +def sanitize_deps_graph(remove_nodes: bool = False): + """ Cleanup the replication graph + """ + if session and session.state == STATE_ACTIVE: + start = utils.current_milli_time() + rm_cpt = 0 + for node in session.repository.graph.values(): + node.instance = session.repository.rdp.resolve(node.data) + if node is None \ + or (node.state == UP and not node.instance): + if remove_nodes: + try: + porcelain.rm(session.repository, + node.uuid, + remove_dependencies=False) + logging.info(f"Removing {node.uuid}") + rm_cpt += 1 + except NonAuthorizedOperationError: + continue + logging.info(f"Sanitize took { utils.current_milli_time()-start} ms, removed {rm_cpt} nodes") + + +def update_external_dependencies(): + """Force external dependencies(files such as images) evaluation + """ + nodes_ids = [n.uuid for n in session.repository.graph.values() if n.data['type_id'] in ['WindowsPath', 'PosixPath']] + for node_id in nodes_ids: + node = session.repository.graph.get(node_id) + if node and node.owner in [session.repository.username, RP_COMMON]: + porcelain.commit(session.repository, node_id) + porcelain.push(session.repository, 'origin', node_id) + + +@persistent +def on_scene_update(scene): + """Forward blender depsgraph update to replication + """ + if session and session.state == STATE_ACTIVE: + context = bpy.context + blender_depsgraph = bpy.context.view_layer.depsgraph + dependency_updates = [u for u in blender_depsgraph.updates] + settings = utils.get_preferences() + incoming_updates = shared_data.session.applied_updates + + distant_update = [getattr(u.id, 'uuid', None) for u in dependency_updates if getattr(u.id, 'uuid', None) in incoming_updates] + if distant_update: + for u in distant_update: + shared_data.session.applied_updates.remove(u) + logging.info(f"Ignoring distant update of {dependency_updates[0].id.name}") + return + + update_external_dependencies() + + # NOTE: maybe we don't need to check each update but only the first + for update in reversed(dependency_updates): + update_uuid = getattr(update.id, 'uuid', None) + if update_uuid: + node = session.repository.graph.get(update.id.uuid) + check_common = session.repository.rdp.get_implementation(update.id).bl_check_common + + if node and (node.owner == session.repository.username or check_common): + logging.debug(f"Evaluate {update.id.name}") + if node.state == UP: + try: + porcelain.commit(session.repository, node.uuid) + porcelain.push(session.repository, + 'origin', node.uuid) + except ReferenceError: + logging.debug(f"Reference error {node.uuid}") + except ContextError as e: + logging.debug(e) + except Exception as e: + logging.error(e) + else: + continue + elif isinstance(update.id, bpy.types.Scene): + scn_uuid = porcelain.add(session.repository, update.id) + porcelain.commit(session.repository, scn_uuid) + porcelain.push(session.repository, 'origin', scn_uuid) + + +@persistent +def resolve_deps_graph(dummy): + """Resolve deps graph + + Temporary solution to resolve each node pointers after a Undo. + A future solution should be to avoid storing dataclock reference... + + """ + if session and session.state == STATE_ACTIVE: + sanitize_deps_graph(remove_nodes=True) + + +@persistent +def load_pre_handler(dummy): + if session and session.state in [STATE_ACTIVE, STATE_SYNCING]: + bpy.ops.session.stop() + + +@persistent +def update_client_frame(scene): + if session and session.state == STATE_ACTIVE: + porcelain.update_user_metadata(session.repository, { + 'frame_current': scene.frame_current + }) + + +def register(): + bpy.app.handlers.undo_post.append(resolve_deps_graph) + bpy.app.handlers.redo_post.append(resolve_deps_graph) + + bpy.app.handlers.load_pre.append(load_pre_handler) + bpy.app.handlers.frame_change_pre.append(update_client_frame) + + +def unregister(): + bpy.app.handlers.undo_post.remove(resolve_deps_graph) + bpy.app.handlers.redo_post.remove(resolve_deps_graph) + + bpy.app.handlers.load_pre.remove(load_pre_handler) + bpy.app.handlers.frame_change_pre.remove(update_client_frame) diff --git a/multi_user/operators.py b/multi_user/operators.py index df9853d..5360d9c 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -27,12 +27,12 @@ import shutil import string import sys import time +import traceback from datetime import datetime from operator import itemgetter from pathlib import Path from queue import Queue from time import gmtime, strftime -import traceback from bpy.props import FloatProperty @@ -45,16 +45,17 @@ import bpy import mathutils from bpy.app.handlers import persistent from bpy_extras.io_utils import ExportHelper, ImportHelper +from replication import porcelain from replication.constants import (COMMITED, FETCHED, RP_COMMON, STATE_ACTIVE, STATE_INITIAL, STATE_SYNCING, UP) -from replication.protocol import DataTranslationProtocol from replication.exception import ContextError, NonAuthorizedOperationError from replication.interface import session -from replication import porcelain -from replication.repository import Repository from replication.objects import Node +from replication.protocol import DataTranslationProtocol +from replication.repository import Repository -from . import bl_types, environment, timers, ui, utils +from . import bl_types, environment, shared_data, timers, ui, utils +from .handlers import on_scene_update, sanitize_deps_graph from .presence import SessionStatusWidget, renderer, view3d_find from .timers import registry @@ -112,7 +113,7 @@ def initialize_session(): utils.flush_history() # Step 6: Launch deps graph update handling - bpy.app.handlers.depsgraph_update_post.append(depsgraph_evaluation) + bpy.app.handlers.depsgraph_update_post.append(on_scene_update) @session_callback('on_exit') @@ -132,8 +133,8 @@ def on_connection_end(reason="none"): stop_modal_executor = True - if depsgraph_evaluation in bpy.app.handlers.depsgraph_update_post: - bpy.app.handlers.depsgraph_update_post.remove(depsgraph_evaluation) + if on_scene_update in bpy.app.handlers.depsgraph_update_post: + bpy.app.handlers.depsgraph_update_post.remove(on_scene_update) # Step 3: remove file handled logger = logging.getLogger() @@ -684,6 +685,7 @@ class SessionPurgeOperator(bpy.types.Operator): def execute(self, context): try: sanitize_deps_graph(remove_nodes=True) + porcelain.purge_orphan_nodes(session.repository) except Exception as e: self.report({'ERROR'}, repr(e)) @@ -716,7 +718,6 @@ class SessionNotifyOperator(bpy.types.Operator): layout = self.layout layout.row().label(text=self.message) - def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self) @@ -919,110 +920,6 @@ classes = ( ) -def update_external_dependencies(): - nodes_ids = [n.uuid for n in session.repository.graph.values() if n.data['type_id'] in ['WindowsPath', 'PosixPath']] - for node_id in nodes_ids: - node = session.repository.graph.get(node_id) - if node and node.owner in [session.repository.username, RP_COMMON]: - porcelain.commit(session.repository, node_id) - porcelain.push(session.repository,'origin', node_id) - - -def sanitize_deps_graph(remove_nodes: bool = False): - """ Cleanup the replication graph - """ - if session and session.state == STATE_ACTIVE: - start = utils.current_milli_time() - rm_cpt = 0 - for node in session.repository.graph.values(): - node.instance = session.repository.rdp.resolve(node.data) - if node is None \ - or (node.state == UP and not node.instance): - if remove_nodes: - try: - porcelain.rm(session.repository, - node.uuid, - remove_dependencies=False) - logging.info(f"Removing {node.uuid}") - rm_cpt += 1 - except NonAuthorizedOperationError: - continue - logging.info(f"Sanitize took { utils.current_milli_time()-start} ms, removed {rm_cpt} nodes") - - -@persistent -def resolve_deps_graph(dummy): - """Resolve deps graph - - Temporary solution to resolve each node pointers after a Undo. - A future solution should be to avoid storing dataclock reference... - - """ - if session and session.state == STATE_ACTIVE: - sanitize_deps_graph(remove_nodes=True) - - -@persistent -def load_pre_handler(dummy): - if session and session.state in [STATE_ACTIVE, STATE_SYNCING]: - bpy.ops.session.stop() - - -@persistent -def update_client_frame(scene): - if session and session.state == STATE_ACTIVE: - porcelain.update_user_metadata(session.repository, { - 'frame_current': scene.frame_current - }) - - -@persistent -def depsgraph_evaluation(scene): - if session and session.state == STATE_ACTIVE: - context = bpy.context - blender_depsgraph = bpy.context.view_layer.depsgraph - dependency_updates = [u for u in blender_depsgraph.updates] - settings = utils.get_preferences() - - update_external_dependencies() - - is_internal = [u for u in dependency_updates if u.is_updated_geometry or u.is_updated_shading or u.is_updated_transform] - - # NOTE: maybe we don't need to check each update but only the first - if not is_internal: - return - for update in reversed(dependency_updates): - # Is the object tracked ? - if update.id.uuid: - # Retrieve local version - node = session.repository.graph.get(update.id.uuid) - check_common = session.repository.rdp.get_implementation(update.id).bl_check_common - # Check our right on this update: - # - if its ours or ( under common and diff), launch the - # update process - # - if its to someone else, ignore the update - if node and (node.owner == session.repository.username or check_common): - if node.state == UP: - try: - porcelain.commit(session.repository, node.uuid) - porcelain.push(session.repository, 'origin', node.uuid) - except ReferenceError: - logging.debug(f"Reference error {node.uuid}") - except ContextError as e: - logging.debug(e) - except Exception as e: - logging.error(e) - else: - continue - # A new scene is created - elif isinstance(update.id, bpy.types.Scene): - ref = session.repository.get_node_by_datablock(update.id) - if ref: - pass - else: - scn_uuid = porcelain.add(session.repository, update.id) - porcelain.commit(session.node_id, scn_uuid) - porcelain.push(session.repository,'origin', scn_uuid) def register(): from bpy.utils import register_class @@ -1030,13 +927,6 @@ def register(): register_class(cls) - bpy.app.handlers.undo_post.append(resolve_deps_graph) - bpy.app.handlers.redo_post.append(resolve_deps_graph) - - bpy.app.handlers.load_pre.append(load_pre_handler) - bpy.app.handlers.frame_change_pre.append(update_client_frame) - - def unregister(): if session and session.state == STATE_ACTIVE: session.disconnect() @@ -1044,9 +934,3 @@ def unregister(): from bpy.utils import unregister_class for cls in reversed(classes): unregister_class(cls) - - bpy.app.handlers.undo_post.remove(resolve_deps_graph) - bpy.app.handlers.redo_post.remove(resolve_deps_graph) - - bpy.app.handlers.load_pre.remove(load_pre_handler) - bpy.app.handlers.frame_change_pre.remove(update_client_frame) diff --git a/multi_user/shared_data.py b/multi_user/shared_data.py new file mode 100644 index 0000000..525d9e8 --- /dev/null +++ b/multi_user/shared_data.py @@ -0,0 +1,48 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# 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 3 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 . +# +# ##### END GPL LICENSE BLOCK ##### + +from replication.constants import STATE_INITIAL + + +class SessionData(): + """ A structure to share easily the current session data across the addon + modules. + This object will completely replace the Singleton lying in replication + interface module. + """ + + def __init__(self): + self.repository = None # The current repository + self.remote = None # The active remote + self.server = None + self.applied_updates = [] + + @property + def state(self): + if self.remote is None: + return STATE_INITIAL + else: + return self.remote.connection_status + + def clear(self): + self.remote = None + self.repository = None + self.server = None + self.applied_updates = [] + + +session = SessionData() diff --git a/multi_user/timers.py b/multi_user/timers.py index 3af27a1..dafbf08 100644 --- a/multi_user/timers.py +++ b/multi_user/timers.py @@ -31,6 +31,8 @@ from .presence import (UserFrustumWidget, UserNameWidget, UserSelectionWidget, generate_user_camera, get_view_matrix, refresh_3d_view, refresh_sidebar_view, renderer) +from . import shared_data + this = sys.modules[__name__] # Registered timers @@ -89,7 +91,7 @@ class Timer(object): if bpy.app.timers.is_registered(self.main): logging.info(f"Unregistering {self.id}") bpy.app.timers.unregister(self.main) - + del this.registry[self.id] self.is_running = False @@ -114,6 +116,7 @@ class ApplyTimer(Timer): if node_ref.state == FETCHED: try: + shared_data.session.applied_updates.append(node) porcelain.apply(session.repository, node) except Exception as e: logging.error(f"Fail to apply {node_ref.uuid}") @@ -251,6 +254,7 @@ class DynamicRightSelectTimer(Timer): is_selectable = not session.repository.is_node_readonly(object_uuid) if obj.hide_select != is_selectable: obj.hide_select = is_selectable + shared_data.session.applied_updates.append(object_uuid) class ClientUpdate(Timer):