diff --git a/multi_user/__init__.py b/multi_user/__init__.py index 4c7c7e6..6b4e8a5 100644 --- a/multi_user/__init__.py +++ b/multi_user/__init__.py @@ -44,13 +44,16 @@ from . import environment, utils DEPENDENCIES = { - ("replication", '0.0.21a5'), + ("replication", '0.0.21a6'), } def register(): # Setup logging policy - logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.INFO) + logging.basicConfig( + format='%(asctime)s CLIENT %(levelname)-8s %(message)s', + datefmt='%H:%M:%S', + level=logging.INFO) try: environment.setup(DEPENDENCIES, bpy.app.binary_path_python) diff --git a/multi_user/operators.py b/multi_user/operators.py index 428fe94..0292e2d 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -34,8 +34,8 @@ from bpy.app.handlers import persistent from . import bl_types, delayable, environment, presence, ui, utils from replication.constants import (FETCHED, STATE_ACTIVE, - STATE_INITIAL, - STATE_SYNCING, RP_COMMON, UP) + STATE_INITIAL, + STATE_SYNCING, RP_COMMON, UP) from replication.data import ReplicatedDataFactory from replication.exception import NonAuthorizedOperationError from replication.interface import Session @@ -71,6 +71,29 @@ class SessionStartOperator(bpy.types.Operator): users.clear() delayables.clear() + logging.getLogger().handlers.clear() + # logging.basicConfig(level=settings.logging_level) + + formatter = logging.Formatter( + fmt='%(asctime)s CLIENT %(levelname)-8s %(message)s', + datefmt='%H:%M:%S' + ) + + log_directory = os.path.join( + settings.cache_directory, + "multiuser_client.log") + + os.makedirs(settings.cache_directory, exist_ok=True) + logger = logging.getLogger() + handler = logging.FileHandler(log_directory, mode='w') + logger.addHandler(handler) + + for handler in logger.handlers: + if isinstance(handler, logging.NullHandler): + continue + + handler.setFormatter(formatter) + bpy_factory = ReplicatedDataFactory() supported_bl_types = [] @@ -104,7 +127,8 @@ class SessionStartOperator(bpy.types.Operator): external_update_handling=use_extern_update) if settings.update_method == 'DEPSGRAPH': - delayables.append(delayable.ApplyTimer(settings.depsgraph_update_rate/1000)) + delayables.append(delayable.ApplyTimer( + settings.depsgraph_update_rate/1000)) # Host a session if self.host: @@ -123,7 +147,10 @@ class SessionStartOperator(bpy.types.Operator): port=settings.port, ipc_port=settings.ipc_port, timeout=settings.connection_timeout, - password=admin_pass + password=admin_pass, + cache_directory=settings.cache_directory, + server_log_level=logging.getLevelName( + logging.getLogger().level), ) except Exception as e: self.report({'ERROR'}, repr(e)) @@ -162,7 +189,6 @@ class SessionStartOperator(bpy.types.Operator): delayables.append(session_update) delayables.append(session_user_sync) - @client.register('on_connection') def initialize_session(): settings = utils.get_preferences() @@ -177,7 +203,6 @@ class SessionStartOperator(bpy.types.Operator): if node_ref.state == FETCHED: node_ref.apply() - # Launch drawing module if runtime_settings.enable_presence: presence.renderer.run() @@ -185,9 +210,10 @@ class SessionStartOperator(bpy.types.Operator): # Register blender main thread tools for d in delayables: d.register() - + if settings.update_method == 'DEPSGRAPH': - bpy.app.handlers.depsgraph_update_post.append(depsgraph_evaluation) + bpy.app.handlers.depsgraph_update_post.append( + depsgraph_evaluation) @client.register('on_exit') def desinitialize_session(): @@ -204,7 +230,8 @@ class SessionStartOperator(bpy.types.Operator): presence.renderer.stop() if settings.update_method == 'DEPSGRAPH': - bpy.app.handlers.depsgraph_update_post.remove(depsgraph_evaluation) + bpy.app.handlers.depsgraph_update_post.remove( + depsgraph_evaluation) bpy.ops.session.apply_armature_operator() @@ -422,14 +449,16 @@ class SessionSnapUserOperator(bpy.types.Operator): if target_scene != context.scene.name: blender_scene = bpy.data.scenes.get(target_scene, None) if blender_scene is None: - self.report({'ERROR'}, f"Scene {target_scene} doesn't exist on the local client.") + self.report( + {'ERROR'}, f"Scene {target_scene} doesn't exist on the local client.") session_sessings.time_snap_running = False return {"CANCELLED"} bpy.context.window.scene = blender_scene # Update client viewmatrix - client_vmatrix = target_ref['metadata'].get('view_matrix', None) + client_vmatrix = target_ref['metadata'].get( + 'view_matrix', None) if client_vmatrix: rv3d.view_matrix = mathutils.Matrix(client_vmatrix) @@ -625,6 +654,7 @@ def update_client_frame(scene): 'frame_current': scene.frame_current }) + @persistent def depsgraph_evaluation(scene): if client and client.state['STATE'] == STATE_ACTIVE: @@ -640,7 +670,7 @@ def depsgraph_evaluation(scene): if update.id.uuid: # Retrieve local version node = client.get(update.id.uuid) - + # Check our right on this update: # - if its ours or ( under common and diff), launch the # update process @@ -648,7 +678,7 @@ def depsgraph_evaluation(scene): if node and node.owner in [client.id, RP_COMMON] and node.state == UP: # Avoid slow geometry update if 'EDIT' in context.mode and \ - not settings.enable_editmode_updates: + not settings.enable_editmode_updates: break client.stash(node.uuid) @@ -671,8 +701,6 @@ def register(): bpy.app.handlers.load_pre.append(load_pre_handler) bpy.app.handlers.frame_change_pre.append(update_client_frame) - - def unregister(): global client @@ -690,4 +718,3 @@ def unregister(): bpy.app.handlers.load_pre.remove(load_pre_handler) bpy.app.handlers.frame_change_pre.remove(update_client_frame) - \ No newline at end of file diff --git a/multi_user/preferences.py b/multi_user/preferences.py index 6dd3e80..90ec490 100644 --- a/multi_user/preferences.py +++ b/multi_user/preferences.py @@ -21,7 +21,8 @@ import bpy import string import re -from . import utils, bl_types, environment, addon_updater_ops, presence, ui +from . import bl_types, environment, addon_updater_ops, presence, ui +from .utils import get_preferences, get_expanded_icon from replication.constants import RP_COMMON IP_EXPR = re.compile('\d+\.\d+\.\d+\.\d+') @@ -46,6 +47,7 @@ def update_panel_category(self, context): ui.SESSION_PT_settings.bl_category = self.panel_category ui.register() + def update_ip(self, context): ip = IP_EXPR.search(self.ip) @@ -55,14 +57,25 @@ def update_ip(self, context): logging.error("Wrong IP format") self['ip'] = "127.0.0.1" + def update_port(self, context): max_port = self.port + 3 if self.ipc_port < max_port and \ - self['ipc_port'] >= self.port: - logging.error("IPC Port in conflic with the port, assigning a random value") + self['ipc_port'] >= self.port: + logging.error( + "IPC Port in conflic with the port, assigning a random value") self['ipc_port'] = random.randrange(self.port+4, 10000) + +def set_log_level(self, value): + logging.getLogger().setLevel(value) + + +def get_log_level(self): + return logging.getLogger().level + + class ReplicatedDatablock(bpy.types.PropertyGroup): type_name: bpy.props.StringProperty() bl_name: bpy.props.StringProperty() @@ -134,7 +147,8 @@ class SessionPrefs(bpy.types.AddonPreferences): description='replication update method', items=[ ('DEFAULT', "Default", "Default: Use threads to monitor databloc changes"), - ('DEPSGRAPH', "Depsgraph", "Experimental: Use the blender dependency graph to trigger updates"), + ('DEPSGRAPH', "Depsgraph", + "Experimental: Use the blender dependency graph to trigger updates"), ], ) # Replication update settings @@ -158,17 +172,18 @@ class SessionPrefs(bpy.types.AddonPreferences): ], default='CONFIG' ) - # WIP logging_level: bpy.props.EnumProperty( name="Log level", description="Log verbosity level", items=[ - ('ERROR', "error", "show only errors"), - ('WARNING', "warning", "only show warnings and errors"), - ('INFO', "info", "default level"), - ('DEBUG', "debug", "show all logs"), + ('ERROR', "error", "show only errors", logging.ERROR), + ('WARNING', "warning", "only show warnings and errors", logging.WARNING), + ('INFO', "info", "default level", logging.INFO), + ('DEBUG', "debug", "show all logs", logging.DEBUG), ], - default='INFO' + default='INFO', + set=set_log_level, + get=get_log_level ) conf_session_identity_expanded: bpy.props.BoolProperty( name="Identity", @@ -200,7 +215,21 @@ class SessionPrefs(bpy.types.AddonPreferences): description="Interface", default=False ) - + sidebar_advanced_rep_expanded: bpy.props.BoolProperty( + name="sidebar_advanced_rep_expanded", + description="sidebar_advanced_rep_expanded", + default=False + ) + sidebar_advanced_log_expanded: bpy.props.BoolProperty( + name="sidebar_advanced_log_expanded", + description="sidebar_advanced_log_expanded", + default=False + ) + sidebar_advanced_net_expanded: bpy.props.BoolProperty( + name="sidebar_advanced_net_expanded", + description="sidebar_advanced_net_expanded", + default=False + ) auto_check_update: bpy.props.BoolProperty( name="Auto-check for Update", description="If enabled, auto-check for updates using an interval", @@ -252,8 +281,8 @@ class SessionPrefs(bpy.types.AddonPreferences): box = grid.box() box.prop( self, "conf_session_identity_expanded", text="User informations", - icon='DISCLOSURE_TRI_DOWN' if self.conf_session_identity_expanded - else 'DISCLOSURE_TRI_RIGHT', emboss=False) + icon=get_expanded_icon(self.conf_session_identity_expanded), + emboss=False) if self.conf_session_identity_expanded: box.row().prop(self, "username", text="name") box.row().prop(self, "client_color", text="color") @@ -262,8 +291,8 @@ class SessionPrefs(bpy.types.AddonPreferences): box = grid.box() box.prop( self, "conf_session_net_expanded", text="Netorking", - icon='DISCLOSURE_TRI_DOWN' if self.conf_session_net_expanded - else 'DISCLOSURE_TRI_RIGHT', emboss=False) + icon=get_expanded_icon(self.conf_session_net_expanded), + emboss=False) if self.conf_session_net_expanded: box.row().prop(self, "ip", text="Address") @@ -280,8 +309,8 @@ class SessionPrefs(bpy.types.AddonPreferences): table = box.box() table.row().prop( self, "conf_session_timing_expanded", text="Refresh rates", - icon='DISCLOSURE_TRI_DOWN' if self.conf_session_timing_expanded - else 'DISCLOSURE_TRI_RIGHT', emboss=False) + icon=get_expanded_icon(self.conf_session_timing_expanded), + emboss=False) if self.conf_session_timing_expanded: line = table.row() @@ -299,8 +328,8 @@ class SessionPrefs(bpy.types.AddonPreferences): box = grid.box() box.prop( self, "conf_session_hosting_expanded", text="Hosting", - icon='DISCLOSURE_TRI_DOWN' if self.conf_session_hosting_expanded - else 'DISCLOSURE_TRI_RIGHT', emboss=False) + icon=get_expanded_icon(self.conf_session_hosting_expanded), + emboss=False) if self.conf_session_hosting_expanded: row = box.row() row.label(text="Init the session from:") @@ -310,8 +339,8 @@ class SessionPrefs(bpy.types.AddonPreferences): box = grid.box() box.prop( self, "conf_session_cache_expanded", text="Cache", - icon='DISCLOSURE_TRI_DOWN' if self.conf_session_cache_expanded - else 'DISCLOSURE_TRI_RIGHT', emboss=False) + icon=get_expanded_icon(self.conf_session_cache_expanded), + emboss=False) if self.conf_session_cache_expanded: box.row().prop(self, "cache_directory", text="Cache directory") @@ -319,7 +348,7 @@ class SessionPrefs(bpy.types.AddonPreferences): box = grid.box() box.prop( self, "conf_session_ui_expanded", text="Interface", - icon='DISCLOSURE_TRI_DOWN' if self.conf_session_ui_expanded else 'DISCLOSURE_TRI_RIGHT', + icon=get_expanded_icon(self.conf_session_ui_expanded), emboss=False) if self.conf_session_ui_expanded: box.row().prop(self, "panel_category", text="Panel category", expand=True) @@ -353,7 +382,7 @@ def client_list_callback(scene, context): items = [(RP_COMMON, RP_COMMON, "")] - username = utils.get_preferences().username + username = get_preferences().username cli = operators.client if cli: client_ids = cli.online_users.keys() diff --git a/multi_user/ui.py b/multi_user/ui.py index fb2972a..37b71ab 100644 --- a/multi_user/ui.py +++ b/multi_user/ui.py @@ -18,7 +18,8 @@ import bpy -from . import operators, utils +from . import operators +from .utils import get_preferences, get_expanded_icon from replication.constants import (ADDED, ERROR, FETCHED, MODIFIED, RP_COMMON, UP, STATE_ACTIVE, STATE_AUTH, @@ -112,7 +113,7 @@ class SESSION_PT_settings(bpy.types.Panel): layout.use_property_split = True row = layout.row() runtime_settings = context.window_manager.session - settings = utils.get_preferences() + settings = get_preferences() if hasattr(context.window_manager, 'session'): # STATE INITIAL @@ -195,7 +196,7 @@ class SESSION_PT_settings_network(bpy.types.Panel): layout = self.layout runtime_settings = context.window_manager.session - settings = utils.get_preferences() + settings = get_preferences() # USER SETTINGS row = layout.row() @@ -253,7 +254,7 @@ class SESSION_PT_settings_user(bpy.types.Panel): layout = self.layout runtime_settings = context.window_manager.session - settings = utils.get_preferences() + settings = get_preferences() row = layout.row() # USER SETTINGS @@ -284,60 +285,87 @@ class SESSION_PT_advanced_settings(bpy.types.Panel): layout = self.layout runtime_settings = context.window_manager.session - settings = utils.get_preferences() + settings = get_preferences() net_section = layout.row().box() - net_section.label(text="Network ", icon='TRIA_DOWN') - net_section_row = net_section.row() - net_section_row.label(text="IPC Port:") - net_section_row.prop(settings, "ipc_port", text="") - net_section_row = net_section.row() - net_section_row.label(text="Timeout (ms):") - net_section_row.prop(settings, "connection_timeout", text="") + net_section.prop( + settings, + "sidebar_advanced_net_expanded", + text="Network", + icon=get_expanded_icon(settings.sidebar_advanced_net_expanded), + emboss=False) + + if settings.sidebar_advanced_net_expanded: + net_section_row = net_section.row() + net_section_row.label(text="IPC Port:") + net_section_row.prop(settings, "ipc_port", text="") + net_section_row = net_section.row() + net_section_row.label(text="Timeout (ms):") + net_section_row.prop(settings, "connection_timeout", text="") replication_section = layout.row().box() - replication_section.label(text="Replication ", icon='TRIA_DOWN') - replication_section_row = replication_section.row() - replication_section_row.label(text="Sync flags", icon='COLLECTION_NEW') - replication_section_row = replication_section.row() - replication_section_row.prop(settings.sync_flags, "sync_render_settings") - replication_section_row = replication_section.row() - # replication_section_row.label(text=":", icon='EDITMODE_HLT') - replication_section_row.prop(settings, "enable_editmode_updates") - replication_section_row = replication_section.row() - if settings.enable_editmode_updates: - warning = replication_section_row.box() - warning.label(text="Don't use this with heavy meshes !", icon='ERROR') + replication_section.prop( + settings, + "sidebar_advanced_rep_expanded", + text="Replication", + icon=get_expanded_icon(settings.sidebar_advanced_rep_expanded), + emboss=False) + + if settings.sidebar_advanced_rep_expanded: replication_section_row = replication_section.row() - replication_section_row.label(text="Update method", icon='RECOVER_LAST') - replication_section_row = replication_section.row() - replication_section_row.prop(settings, "update_method", expand=True) - replication_section_row = replication_section.row() - replication_timers = replication_section_row.box() - replication_timers.label(text="Replication timers", icon='TIME') - if settings.update_method == "DEFAULT": - replication_timers = replication_timers.row() - # Replication frequencies - flow = replication_timers.grid_flow( - row_major=True, columns=0, even_columns=True, even_rows=False, align=True) - line = flow.row(align=True) - line.label(text=" ") - line.separator() - line.label(text="refresh (sec)") - line.label(text="apply (sec)") - for item in settings.supported_datablocks: + replication_section_row.label(text="Sync flags", icon='COLLECTION_NEW') + replication_section_row = replication_section.row() + replication_section_row.prop(settings.sync_flags, "sync_render_settings") + replication_section_row = replication_section.row() + + replication_section_row.prop(settings, "enable_editmode_updates") + replication_section_row = replication_section.row() + if settings.enable_editmode_updates: + warning = replication_section_row.box() + warning.label(text="Don't use this with heavy meshes !", icon='ERROR') + replication_section_row = replication_section.row() + + replication_section_row.label(text="Update method", icon='RECOVER_LAST') + replication_section_row = replication_section.row() + replication_section_row.prop(settings, "update_method", expand=True) + replication_section_row = replication_section.row() + replication_timers = replication_section_row.box() + replication_timers.label(text="Replication timers", icon='TIME') + if settings.update_method == "DEFAULT": + replication_timers = replication_timers.row() + # Replication frequencies + flow = replication_timers.grid_flow( + row_major=True, columns=0, even_columns=True, even_rows=False, align=True) line = flow.row(align=True) - line.prop(item, "auto_push", text="", icon=item.icon) + line.label(text=" ") line.separator() - line.prop(item, "bl_delay_refresh", text="") - line.prop(item, "bl_delay_apply", text="") - else: - replication_timers = replication_timers.row() - replication_timers.label(text="Update rate (ms):") - replication_timers.prop(settings, "depsgraph_update_rate", text="") + line.label(text="refresh (sec)") + line.label(text="apply (sec)") + for item in settings.supported_datablocks: + line = flow.row(align=True) + line.prop(item, "auto_push", text="", icon=item.icon) + line.separator() + line.prop(item, "bl_delay_refresh", text="") + line.prop(item, "bl_delay_apply", text="") + else: + replication_timers = replication_timers.row() + replication_timers.label(text="Update rate (ms):") + replication_timers.prop(settings, "depsgraph_update_rate", text="") + + log_section_row = layout.row().box() + log_section_row.prop( + settings, + "sidebar_advanced_log_expanded", + text="Logging", + icon=get_expanded_icon(settings.sidebar_advanced_log_expanded), + emboss=False) + + if settings.sidebar_advanced_log_expanded: + log_section_row.label(text="Logging level:") + log_section_row.prop(settings, 'logging_level', text="") class SESSION_PT_user(bpy.types.Panel): bl_idname = "MULTIUSER_USER_PT_panel" bl_label = "Online users" @@ -356,7 +384,7 @@ class SESSION_PT_user(bpy.types.Panel): layout = self.layout online_users = context.window_manager.online_users selected_user = context.window_manager.user_index - settings = utils.get_preferences() + settings = get_preferences() active_user = online_users[selected_user] if len( online_users)-1 >= selected_user else 0 runtime_settings = context.window_manager.session @@ -402,7 +430,7 @@ class SESSION_PT_user(bpy.types.Panel): class SESSION_UL_users(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index, flt_flag): session = operators.client - settings = utils.get_preferences() + settings = get_preferences() is_local_user = item.username == settings.username ping = '-' frame_current = '-' @@ -486,7 +514,7 @@ class SESSION_PT_services(bpy.types.Panel): def draw_property(context, parent, property_uuid, level=0): - settings = utils.get_preferences() + settings = get_preferences() runtime_settings = context.window_manager.session item = operators.client.get(uuid=property_uuid) @@ -557,7 +585,7 @@ class SESSION_PT_repository(bpy.types.Panel): @classmethod def poll(cls, context): session = operators.client - settings = utils.get_preferences() + settings = get_preferences() admin = False if session and hasattr(session,'online_users'): @@ -576,7 +604,7 @@ class SESSION_PT_repository(bpy.types.Panel): layout = self.layout # Filters - settings = utils.get_preferences() + settings = get_preferences() runtime_settings = context.window_manager.session session = operators.client diff --git a/multi_user/utils.py b/multi_user/utils.py index 55ea7e6..a71e772 100644 --- a/multi_user/utils.py +++ b/multi_user/utils.py @@ -39,7 +39,7 @@ def find_from_attr(attr_name, attr_value, list): def get_datablock_users(datablock): users = [] - supported_types = get_preferences().supported_datablocks + supported_types = get_preferences().supported_datablocks if hasattr(datablock, 'users_collection') and datablock.users_collection: users.extend(list(datablock.users_collection)) if hasattr(datablock, 'users_scene') and datablock.users_scene: @@ -77,10 +77,18 @@ def resolve_from_id(id, optionnal_type=None): if id in root and ((optionnal_type is None) or (optionnal_type.lower() in root[id].__class__.__name__.lower())): return root[id] return None - + def get_preferences(): return bpy.context.preferences.addons[__package__].preferences + def current_milli_time(): - return int(round(time.time() * 1000)) \ No newline at end of file + return int(round(time.time() * 1000)) + + +def get_expanded_icon(prop: bpy.types.BoolProperty) -> str: + if prop: + return 'DISCLOSURE_TRI_DOWN' + else: + return 'DISCLOSURE_TRI_RIGHT'