Merge branch 'export_replication_graph' into 'develop'

Export replication graph

See merge request slumber/multi-user!85
This commit is contained in:
Swann Martinez 2020-12-22 22:31:27 +00:00
commit 66b6c06a2c
10 changed files with 291 additions and 66 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

View File

@ -292,7 +292,7 @@ a connected user or be under :ref:`common-right<**COMMON**>` rights.
The Repository panel (see image below) allows you to monitor, change datablock states and rights manually. The Repository panel (see image below) allows you to monitor, change datablock states and rights manually.
.. figure:: img/quickstart_properties.png .. figure:: img/quickstart_save_session_data.png
:align: center :align: center
Repository panel Repository panel
@ -319,6 +319,40 @@ Here is a quick list of available actions:
| .. image:: img/quickstart_remove.png | **Delete** | Remove the data-block from network replication | | .. image:: img/quickstart_remove.png | **Delete** | Remove the data-block from network replication |
+---------------------------------------+-------------------+------------------------------------------------------------------------------------+ +---------------------------------------+-------------------+------------------------------------------------------------------------------------+
Save session data
-----------------
.. danger::
This is an experimental feature, until the stable release it is highly recommended to use regular .blend save.
The save session data allows you to create a backup of the session data.
When you hit the **save session data** button, the following popup dialog will appear.
It allows you to choose the destination folder and if you want to run an auto-save.
.. figure:: img/quickstart_save_session_data_dialog.png
:align: center
Save session data dialog.
If you enabled the auto-save option, you can cancel it from the **Cancel auto-save** button.
.. figure:: img/quickstart_save_session_data_cancel.png
:align: center
Cancel session autosave.
To import session data backups, use the following **Multiuser session snapshot** import dialog
.. figure:: img/quickstart_import_session_data.png
:align: center
Import session data dialog.
.. note::
It is not yet possible to start a session directly from a backup.
.. _advanced: .. _advanced:
Advanced settings Advanced settings

View File

@ -89,6 +89,8 @@ def register():
type=preferences.SessionUser type=preferences.SessionUser
) )
bpy.types.WindowManager.user_index = bpy.props.IntProperty() bpy.types.WindowManager.user_index = bpy.props.IntProperty()
bpy.types.TOPBAR_MT_file_import.append(operators.menu_func_import)
def unregister(): def unregister():
from . import presence from . import presence
@ -97,6 +99,8 @@ def unregister():
from . import preferences from . import preferences
from . import addon_updater_ops from . import addon_updater_ops
bpy.types.TOPBAR_MT_file_import.remove(operators.menu_func_import)
presence.unregister() presence.unregister()
addon_updater_ops.unregister() addon_updater_ops.unregister()
ui.unregister() ui.unregister()

View File

@ -17,6 +17,8 @@
import asyncio import asyncio
import copy
import gzip
import logging import logging
import os import os
import queue import queue
@ -25,27 +27,35 @@ import shutil
import string import string
import sys import sys
import time import time
from datetime import datetime
from operator import itemgetter from operator import itemgetter
from pathlib import Path from pathlib import Path
from queue import Queue from queue import Queue
from time import gmtime, strftime
try:
import _pickle as pickle
except ImportError:
import pickle
import bpy import bpy
import mathutils import mathutils
from bpy.app.handlers import persistent from bpy.app.handlers import persistent
from replication.constants import (FETCHED, RP_COMMON, STATE_ACTIVE, from bpy_extras.io_utils import ExportHelper, ImportHelper
from replication.constants import (COMMITED, FETCHED, RP_COMMON, STATE_ACTIVE,
STATE_INITIAL, STATE_SYNCING, UP) STATE_INITIAL, STATE_SYNCING, UP)
from replication.data import ReplicatedDataFactory from replication.data import ReplicatedDataFactory
from replication.exception import NonAuthorizedOperationError from replication.exception import NonAuthorizedOperationError
from replication.interface import session from replication.interface import session
from . import bl_types, delayable, environment, ui, utils from . import bl_types, environment, timers, ui, utils
from .presence import SessionStatusWidget, renderer, view3d_find from .presence import SessionStatusWidget, renderer, view3d_find
from .timers import registry
background_execution_queue = Queue() background_execution_queue = Queue()
deleyables = [] deleyables = []
stop_modal_executor = False stop_modal_executor = False
def session_callback(name): def session_callback(name):
""" Session callback wrapper """ Session callback wrapper
@ -193,8 +203,8 @@ class SessionStartOperator(bpy.types.Operator):
if settings.update_method == 'DEFAULT': if settings.update_method == 'DEFAULT':
if type_local_config.bl_delay_apply > 0: if type_local_config.bl_delay_apply > 0:
deleyables.append( deleyables.append(
delayable.ApplyTimer( timers.ApplyTimer(
timout=type_local_config.bl_delay_apply, timeout=type_local_config.bl_delay_apply,
target_type=type_module_class)) target_type=type_module_class))
if bpy.app.version[1] >= 91: if bpy.app.version[1] >= 91:
@ -208,7 +218,7 @@ class SessionStartOperator(bpy.types.Operator):
external_update_handling=use_extern_update) external_update_handling=use_extern_update)
if settings.update_method == 'DEPSGRAPH': if settings.update_method == 'DEPSGRAPH':
deleyables.append(delayable.ApplyTimer( deleyables.append(timers.ApplyTimer(
settings.depsgraph_update_rate/1000)) settings.depsgraph_update_rate/1000))
# Host a session # Host a session
@ -259,12 +269,12 @@ class SessionStartOperator(bpy.types.Operator):
logging.error(str(e)) logging.error(str(e))
# Background client updates service # Background client updates service
deleyables.append(delayable.ClientUpdate()) deleyables.append(timers.ClientUpdate())
deleyables.append(delayable.DynamicRightSelectTimer()) deleyables.append(timers.DynamicRightSelectTimer())
session_update = delayable.SessionStatusUpdate() session_update = timers.SessionStatusUpdate()
session_user_sync = delayable.SessionUserSync() session_user_sync = timers.SessionUserSync()
session_background_executor = delayable.MainThreadExecutor( session_background_executor = timers.MainThreadExecutor(
execution_queue=background_execution_queue) execution_queue=background_execution_queue)
session_update.register() session_update.register()
@ -712,6 +722,181 @@ class SessionNotifyOperator(bpy.types.Operator):
return context.window_manager.invoke_props_dialog(self) return context.window_manager.invoke_props_dialog(self)
def dump_db(filepath):
# Replication graph
nodes_ids = session.list()
#TODO: add dump graph to replication
nodes =[]
for n in nodes_ids:
nd = session.get(uuid=n)
nodes.append((
n,
{
'owner': nd.owner,
'str_type': nd.str_type,
'data': nd.data,
'dependencies': nd.dependencies,
}
))
db = dict()
db['nodes'] = nodes
db['users'] = copy.copy(session.online_users)
stime = datetime.now().strftime('%Y_%m_%d_%H-%M-%S')
filepath = Path(filepath)
filepath = filepath.with_name(f"{filepath.stem}_{stime}{filepath.suffix}")
with gzip.open(filepath, "wb") as f:
logging.info(f"Writing session snapshot to {filepath}")
pickle.dump(db, f, protocol=4)
class SessionSaveBackupOperator(bpy.types.Operator, ExportHelper):
bl_idname = "session.save"
bl_label = "Save session data"
bl_description = "Save a snapshot of the collaborative session"
# ExportHelper mixin class uses this
filename_ext = ".db"
filter_glob: bpy.props.StringProperty(
default="*.db",
options={'HIDDEN'},
maxlen=255, # Max internal buffer length, longer would be clamped.
)
enable_autosave: bpy.props.BoolProperty(
name="Auto-save",
description="Enable session auto-save",
default=True,
)
save_interval: bpy.props.FloatProperty(
name="Auto save interval",
description="auto-save interval (seconds)",
default=10,
)
def execute(self, context):
if self.enable_autosave:
recorder = timers.SessionBackupTimer(
filepath=self.filepath,
timeout=self.save_interval)
recorder.register()
deleyables.append(recorder)
else:
dump_db(self.filepath)
return {'FINISHED'}
@classmethod
def poll(cls, context):
return session.state['STATE'] == STATE_ACTIVE
class SessionStopAutoSaveOperator(bpy.types.Operator):
bl_idname = "session.cancel_autosave"
bl_label = "Cancel auto-save"
bl_description = "Cancel session auto-save"
@classmethod
def poll(cls, context):
return (session.state['STATE'] == STATE_ACTIVE and 'SessionBackupTimer' in registry)
def execute(self, context):
autosave_timer = registry.get('SessionBackupTimer')
autosave_timer.unregister()
return {'FINISHED'}
class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
bl_idname = "session.load"
bl_label = "Load session save"
bl_description = "Load a Multi-user session save"
bl_options = {'REGISTER', 'UNDO'}
# ExportHelper mixin class uses this
filename_ext = ".db"
filter_glob: bpy.props.StringProperty(
default="*.db",
options={'HIDDEN'},
maxlen=255, # Max internal buffer length, longer would be clamped.
)
def execute(self, context):
from replication.graph import ReplicationGraph
# TODO: add filechecks
try:
f = gzip.open(self.filepath, "rb")
db = pickle.load(f)
except OSError as e:
f = open(self.filepath, "rb")
db = pickle.load(f)
if db:
logging.info(f"Reading {self.filepath}")
nodes = db.get("nodes")
logging.info(f"{len(nodes)} Nodes to load")
# init the factory with supported types
bpy_factory = ReplicatedDataFactory()
for type in bl_types.types_to_register():
type_module = getattr(bl_types, type)
name = [e.capitalize() for e in type.split('_')[1:]]
type_impl_name = 'Bl'+''.join(name)
type_module_class = getattr(type_module, type_impl_name)
bpy_factory.register_type(
type_module_class.bl_class,
type_module_class)
graph = ReplicationGraph()
for node, node_data in nodes:
node_type = node_data.get('str_type')
impl = bpy_factory.get_implementation_from_net(node_type)
if impl:
logging.info(f"Loading {node}")
instance = impl(owner=node_data['owner'],
uuid=node,
dependencies=node_data['dependencies'],
data=node_data['data'])
instance.store(graph)
instance.state = FETCHED
logging.info("Graph succefully loaded")
utils.clean_scene()
# Step 1: Construct nodes
for node in graph.list_ordered():
graph[node].resolve()
# Step 2: Load nodes
for node in graph.list_ordered():
graph[node].apply()
return {'FINISHED'}
@classmethod
def poll(cls, context):
return True
def menu_func_import(self, context):
self.layout.operator(SessionLoadSaveOperator.bl_idname, text='Multi-user session snapshot (.db)')
classes = ( classes = (
SessionStartOperator, SessionStartOperator,
SessionStopOperator, SessionStopOperator,
@ -726,6 +911,9 @@ classes = (
SessionInitOperator, SessionInitOperator,
SessionClearCache, SessionClearCache,
SessionNotifyOperator, SessionNotifyOperator,
SessionSaveBackupOperator,
SessionLoadSaveOperator,
SessionStopAutoSaveOperator,
) )
@ -794,7 +982,7 @@ def depsgraph_evaluation(scene):
def register(): def register():
from bpy.utils import register_class from bpy.utils import register_class
for cls in classes: for cls in classes:
register_class(cls) register_class(cls)
bpy.app.handlers.undo_post.append(sanitize_deps_graph) bpy.app.handlers.undo_post.append(sanitize_deps_graph)

View File

@ -16,68 +16,48 @@
# ##### END GPL LICENSE BLOCK ##### # ##### END GPL LICENSE BLOCK #####
import logging import logging
import sys
import bpy import bpy
from replication.constants import (FETCHED, RP_COMMON, STATE_ACTIVE,
from . import utils STATE_INITIAL, STATE_LOBBY, STATE_QUITTING,
from .presence import (renderer, STATE_SRV_SYNC, STATE_SYNCING, UP)
UserFrustumWidget,
UserNameWidget,
UserSelectionWidget,
refresh_3d_view,
generate_user_camera,
get_view_matrix,
refresh_sidebar_view)
from . import operators
from replication.constants import (FETCHED,
UP,
RP_COMMON,
STATE_INITIAL,
STATE_QUITTING,
STATE_ACTIVE,
STATE_SYNCING,
STATE_LOBBY,
STATE_SRV_SYNC)
from replication.interface import session
from replication.exception import NonAuthorizedOperationError from replication.exception import NonAuthorizedOperationError
from replication.interface import session
from . import operators, utils
from .presence import (UserFrustumWidget, UserNameWidget, UserSelectionWidget,
generate_user_camera, get_view_matrix, refresh_3d_view,
refresh_sidebar_view, renderer)
this = sys.modules[__name__]
# Registered timers
this.registry = dict()
def is_annotating(context: bpy.types.Context): def is_annotating(context: bpy.types.Context):
""" Check if the annotate mode is enabled """ Check if the annotate mode is enabled
""" """
return bpy.context.workspace.tools.from_space_view3d_mode('OBJECT', create=False).idname == 'builtin.annotate' return bpy.context.workspace.tools.from_space_view3d_mode('OBJECT', create=False).idname == 'builtin.annotate'
class Delayable():
"""Delayable task interface
"""
def register(self): class Timer(object):
raise NotImplementedError
def execute(self):
raise NotImplementedError
def unregister(self):
raise NotImplementedError
class Timer(Delayable):
"""Timer binder interface for blender """Timer binder interface for blender
Run a bpy.app.Timer in the background looping at the given rate Run a bpy.app.Timer in the background looping at the given rate
""" """
def __init__(self, duration=1): def __init__(self, timeout=10, id=None):
super().__init__() self._timeout = timeout
self._timeout = duration
self.is_running = False self.is_running = False
self.id = id if id else self.__class__.__name__
def register(self): def register(self):
"""Register the timer into the blender timer system """Register the timer into the blender timer system
""" """
if not self.is_running: if not self.is_running:
this.registry[self.id] = self
bpy.app.timers.register(self.main) bpy.app.timers.register(self.main)
self.is_running = True self.is_running = True
logging.debug(f"Register {self.__class__.__name__}") logging.debug(f"Register {self.__class__.__name__}")
@ -105,15 +85,26 @@ class Timer(Delayable):
"""Unnegister the timer of the blender timer system """Unnegister the timer of the blender timer system
""" """
if bpy.app.timers.is_registered(self.main): if bpy.app.timers.is_registered(self.main):
logging.info(f"Unregistering {self.id}")
bpy.app.timers.unregister(self.main) bpy.app.timers.unregister(self.main)
del this.registry[self.id]
self.is_running = False self.is_running = False
class SessionBackupTimer(Timer):
def __init__(self, timeout=10, filepath=None):
self._filepath = filepath
super().__init__(timeout)
def execute(self):
operators.dump_db(self._filepath)
class ApplyTimer(Timer): class ApplyTimer(Timer):
def __init__(self, timout=1, target_type=None): def __init__(self, timeout=1, target_type=None):
self._type = target_type self._type = target_type
super().__init__(timout) super().__init__(timeout)
self.id = target_type.__name__
def execute(self): def execute(self):
if session and session.state['STATE'] == STATE_ACTIVE: if session and session.state['STATE'] == STATE_ACTIVE:
@ -140,8 +131,8 @@ class ApplyTimer(Timer):
session.apply(n, force=True) session.apply(n, force=True)
class DynamicRightSelectTimer(Timer): class DynamicRightSelectTimer(Timer):
def __init__(self, timout=.1): def __init__(self, timeout=.1):
super().__init__(timout) super().__init__(timeout)
self._last_selection = [] self._last_selection = []
self._user = None self._user = None
self._annotating = False self._annotating = False
@ -262,8 +253,8 @@ class DynamicRightSelectTimer(Timer):
class ClientUpdate(Timer): class ClientUpdate(Timer):
def __init__(self, timout=.1): def __init__(self, timeout=.1):
super().__init__(timout) super().__init__(timeout)
self.handle_quit = False self.handle_quit = False
self.users_metadata = {} self.users_metadata = {}
@ -325,16 +316,16 @@ class ClientUpdate(Timer):
class SessionStatusUpdate(Timer): class SessionStatusUpdate(Timer):
def __init__(self, timout=1): def __init__(self, timeout=1):
super().__init__(timout) super().__init__(timeout)
def execute(self): def execute(self):
refresh_sidebar_view() refresh_sidebar_view()
class SessionUserSync(Timer): class SessionUserSync(Timer):
def __init__(self, timout=1): def __init__(self, timeout=1):
super().__init__(timout) super().__init__(timeout)
self.settings = utils.get_preferences() self.settings = utils.get_preferences()
def execute(self): def execute(self):
@ -367,12 +358,12 @@ class SessionUserSync(Timer):
class MainThreadExecutor(Timer): class MainThreadExecutor(Timer):
def __init__(self, timout=1, execution_queue=None): def __init__(self, timeout=1, execution_queue=None):
super().__init__(timout) super().__init__(timeout)
self.execution_queue = execution_queue self.execution_queue = execution_queue
def execute(self): def execute(self):
while not self.execution_queue.empty(): while not self.execution_queue.empty():
function, kwargs = self.execution_queue.get() function, kwargs = self.execution_queue.get()
logging.debug(f"Executing {function.__name__}") logging.debug(f"Executing {function.__name__}")
function(**kwargs) function(**kwargs)

View File

@ -29,6 +29,7 @@ from replication.constants import (ADDED, ERROR, FETCHED,
STATE_LAUNCHING_SERVICES) STATE_LAUNCHING_SERVICES)
from replication import __version__ from replication import __version__
from replication.interface import session from replication.interface import session
from .timers import registry
ICONS_PROP_STATES = ['TRIA_DOWN', # ADDED ICONS_PROP_STATES = ['TRIA_DOWN', # ADDED
'TRIA_UP', # COMMITED 'TRIA_UP', # COMMITED
@ -563,6 +564,13 @@ class SESSION_PT_repository(bpy.types.Panel):
row = layout.row() row = layout.row()
if session.state['STATE'] == STATE_ACTIVE: if session.state['STATE'] == STATE_ACTIVE:
if 'SessionBackupTimer' in registry:
row.alert = True
row.operator('session.cancel_autosave', icon="CANCEL")
row.alert = False
else:
row.operator('session.save', icon="FILE_TICK")
flow = layout.grid_flow( flow = layout.grid_flow(
row_major=True, row_major=True,
columns=0, columns=0,