import bpy import os import re import time from queue import Queue from threading import Thread, Event from subprocess import Popen, PIPE, STDOUT from bpy.types import Panel, Operator, PropertyGroup from bpy.props import (StringProperty, BoolProperty, IntProperty, FloatProperty, EnumProperty, PointerProperty, CollectionProperty ) class BlendFileProperty(PropertyGroup): path = StringProperty(subtype="FILE_PATH") filename = StringProperty(subtype="FILE_NAME") blending_enabled = BoolProperty( name="Enable or Disable", description="Enable blend file for blending", default=False) status = StringProperty( name="Status", description="Status of current file", default="" ) class PushBlendPullProperties(PropertyGroup): project_folder = StringProperty( name="Project folder", description="Path to project folder", default="//", subtype="DIR_PATH" ) remote_host = StringProperty( name="Remote host", description="Address or host of remote", defaul ="" ) remote_folder = StringProperty( name="Remote project folder", description="Path to remote project folder", default="" ) blend_file_list = CollectionProperty(type=BlendFileProperty) upload_status = StringProperty( name="Upload status", description="Status of current upload", default="" ) class ProjectManagerRefreshBlenFileOperator(Operator): bl_idname = "wm.pm_refresh_blend" bl_label = "Refresh Blender Files" def execute(self, context): blendFileList = context.window_manager.project_manager.blend_file_list enabled = [] for blendFile in blendFileList: if blendFile.blending_enabled: enabled.append(blendFile.path) blendFileList.clear() path = bpy.path.abspath(context.window_manager.project_manager.project_folder) for root, dirs, files in os.walk(path): for file in files: if file.endswith(".blend"): blendFile = blendFileList.add() blendFile.filename = os.path.splitext(file)[0] blendFile.path = os.path.join(root, file) blendFile.name = bpy.path.relpath(blendFile.path) blendFile.blending_enabled = blendFile.path in enabled return {'FINISHED'} class ProjectManagerStartBlend(Operator): bl_idname = "wm.project_manager" bl_label = "Start blend" _timer = None _queue = None _worker = None def execute(self, context): wm = context.window_manager self._queue = Queue() self._worker = RemoteThreadHandler( self._queue, wm.project_manager.blend_file_list, bpy.path.abspath(wm.project_manager.project_folder), wm.project_manager.remote_host, wm.project_manager.remote_folder ) self._worker.start() self._timer = wm.event_timer_add(1, context.window) wm.modal_handler_add(self) return {'RUNNING_MODAL'} def modal(self, context, event): if event.type in {'ESC'}: self.cancel(context) print("canceled") return {'CANCELLED'} if event.type == 'TIMER': while not self._queue.empty(): self.handle_message(self._queue.get(), context.window_manager.project_manager) self._queue.task_done() if not self._worker.is_alive(): self.cancel(context) print("finished") return {'FINISHED'} return {'PASS_THROUGH'} def cancel(self, context): wm = context.window_manager wm.event_timer_remove(self._timer) self._worker.stop() def handle_message(self, msg, pm): if msg[0] == "push": print(msg) pm.upload_status = "push "+msg[1] elif msg[0] == "pull": msg[2].status = "pull "+msg[1] elif msg[0] == "blend": msg[2].status = "blend "+msg[1] class RemoteThreadHandler(Thread): def __init__(self, q, blendFileList, localFolder, remoteHost, remoteFolder): super(RemoteThreadHandler, self).__init__() self._queue = q self.blendFileList = blendFileList self.remotePath = "{}:{}".format(remoteHost,remoteFolder) self.localFolder = localFolder self.remoteHost = remoteHost self.remoteFolder = remoteFolder self._stop_event = Event() def stop(self): self._stop_event.set() def stopped(self): return self._stop_event.is_set() ARGS_PUSH = "rsync -azP --info=progress2 --exclude '*.blend1' --exclude 'README.md' {} {}" ARGS_PULL = "rsync -azP --info=progress2 --exclude '*.blend' {} {}" ARGS_BLEND = "ssh -t hawat 'blender -b {} -a'" REGEX_PUSHPULL_STATUS = "\d+%" def handleLinePush(self, line, current_file=None): if line == 'end': self._queue.put(("push", "end",)) return t = re.search(self.REGEX_PUSHPULL_STATUS, line) if t is not None: self._queue.put(("push", t[0],)) def handleLinePull(self, line, current_file): if line == 'end': self._queue.put(("pull", "end", current_file,)) return t = re.search(self.REGEX_PUSHPULL_STATUS, line) if t is not None: self._queue.put(("pull", t[0], current_file,)) def run(self): self.start_process_handle_queue(self.ARGS_PUSH.format(self.localFolder, self.remotePath), self.handleLinePush) for blendFile in self.blendFileList: if blendFile.blending_enabled: remoteBlendFilePath = blendFile.path.replace(self.localFolder, self.remoteFolder) self.start_process_handle_queue(self.ARGS_BLEND.format(remoteBlendFilePath), self.handleLineBlend, blendFile) pullWorker = Thread( target=self.start_process_handle_queue, args=(self.ARGS_PULL.format(self.remotePath, self.localFolder) , self.handleLinePull, blendFile) ) pullWorker.start() def start_process_handle_queue(self, args, lineHandler, current_file = None): if self.stopped(): return print(args) p = Popen(args, shell=True, stdout=PIPE, stderr=STDOUT, universal_newlines=True, bufsize=1) last_line = '' while not self.stopped(): line = p.stdout.readline() if line == '': if p.poll() is not None: break elif line != last_line: lineHandler(line, current_file) last_line = line if self.stopped(): p.kill() lineHandler('end', current_file) class ProjectManagerPanel(Panel): bl_idname = "OBJECT_PT_project_manager" bl_label = "Project Management" bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' bl_context = "" def draw(self, context): layout = self.layout layout.operator(ProjectManagerStartBlend.bl_idname) configuration = context.window_manager.project_manager layout.label(text="Configuration") layout = self.layout row = layout.row(align=True) row.prop(configuration, "project_folder") row = layout.row(align=True) row.prop(configuration, "remote_host") row = layout.row(align=True) row.prop(configuration, "remote_folder") row = layout.row(align=True) row.prop(configuration, "upload_status") layout.operator(ProjectManagerRefreshBlenFileOperator.bl_idname) fileList = configuration.blend_file_list for blendFile in sorted(fileList.values(), key=lambda file: file.path): row = layout.row() row.prop(blendFile, "blending_enabled", text=bpy.path.relpath(blendFile.path)) row.prop(blendFile, "status", text="") def register(): bpy.utils.register_module(__name__) bpy.types.WindowManager.project_manager = PointerProperty(type=ProjectManagerProperties) def unregister(): bpy.utils.unregister_module(__name__) del bpy.types.WindowManager.project_manage if __name__ == "__main__": register()