Blender-Pipeline/network/network_multiuser.py

506 lines
20 KiB
Python

# THIS FILE IS A PART OF VCStudio
# PYTHON 3
###############################################################################
# In order for Multiuser to function. VCStudio should have a process on the
# background can access to all the data, that will talk to the Multiuser
# server.
###############################################################################
##################### IMPLEMENTED FEATURES LIST ###############################
# [V] List of users | Important to know the usernames of all users
# [V] Assets | Important to have up to date assets everywhere
# [ ] Shots | Important to have up to date shots / and their assets
# [ ] Rendering | Ability to render on a separate machine.
# [ ] Story | Real time Sync of the story edito and the script writer
# [ ] Analytics | Real time Sync of History and Schedules.
# [ ] Messages | A little messaging system in the Multiuser window.
###############################################################################
import os
import sys
import time
import json
import socket
import random
import hashlib
import datetime
import threading
import subprocess
from UI import UI_elements
from settings import talk
from network import insure
time_format = "%Y/%m/%d-%H:%M:%S"
def client(win):
###########################################################################
# This function is the thing that we need. It's going to run in it's own
# thread. Separatelly from the rest of the program.
###########################################################################
while True:
try:
# So the first thing that we want to do is to listen for a server
# broadcasting himself. And when found connect to that server.
connect(win)
# Then it's going to ask us that project are we in and our username.
server = win.multiuser["server"]
# Then as soon as we are connected. We want to start the main loop. I know
# that some stuff should be done early. But I guess it's better to do them
# in the main loop. So basically the server will request a bunch of stuff.
# And when it sends no valid request. Or something like KEEP ALIVE then it's
# a turn for as to send requests to server. Or nothing. The communication
# happens all the time.
while win.multiuser["server"]:
request = insure.recv(server)
if request == "yours":
############################################################
# WE REQUEST FROM SERVER #
############################################################
if win.multiuser["curs"]:
# The get asset function
get(win, list(win.multiuser["curs"].keys())[0],
win.multiuser["curs"][list(win.multiuser["curs"].keys())[0]])
try:
del win.multiuser["curs"][list(win.multiuser["curs"].keys())[0]]
except:
pass
elif win.multiuser["request"]:
insure.send(server, win.multiuser["request"])
win.multiuser["request"] = []
elif not win.multiuser["users"]:
insure.send(server, "users")
win.multiuser["users"] = insure.recv(server)
elif win.cur and win.cur != win.multiuser["last_request"]:
win.multiuser["last_request"] = win.cur
# If we are currently at some asset or shot. We want to
# send to the server the current state of the folder
# if such exists. So if we have the latest one. Every
# body else could send us a give request. But if somebody
# has the latest and it's not us. We could send them
# the get request.
# First step will be to check whitch cur are we in.
if win.url == "assets":
t = "/dev"
elif win.url == "script":
t = "/rnd"
else:
t = ""
# Then we need to see if there is a folder to begin
# with. Some scenes and some shots have no folder.
if not os.path.exists(win.project+t+win.cur):
insure.send(server, "yours")
else:
# If there is a folder let's get the timestamp.
timestamp = getfoldertime(win.project+t+win.cur)
insure.send(server, ["at", t+win.cur, timestamp])
else:
insure.send(server, "yours")
if win.url not in ["assets", "script", "analytics"]:
win.multiuser["last_request"] = ""
win.cur = ""
else:
############################################################
# SERVER REQUESTS FROM US #
############################################################
if request == "story":
if not win.multiuser["story_check"]:
storytime = "1997/07/30-00:00:00"
win.multiuser["story_check"] = True
else:
storytime = gettime(win.project+"/pln/story.vcss")
selectedtmp = win.story["selected"]
tmppointers = win.story["pointers"]
tmpcamera = win.story["camera"]
insure.send(server, [win.story, storytime])
story = insure.recv(server)
win.story = story[1].copy()
win.story["selected"] = selectedtmp
win.story["camera"] = tmpcamera
win.story["pointers"] = tmppointers
if story[0] in win.multiuser["users"]:
win.multiuser["users"][story[0]]["camera"] = story[1]["camera"]
elif request == "users":
win.multiuser["users"] = {}
elif request == "analytics":
if not win.multiuser["analytics_check"]:
analyticstime = "1997/07/30-00:00:00"
win.multiuser["analytics_check"] = True
else:
analyticstime = datetime.datetime.strftime(datetime.datetime.now(), time_format)
insure.send(server, [win.analytics, analyticstime])
win.analytics = insure.recv(server)
elif request == "assets":
assets = list_all_assets(win)
insure.send(server, assets)
new_assets = insure.recv(server)
for asset in new_assets:
if asset not in assets or assets[asset][1] < new_assets[asset][1]:
# If a given asset is not on our system or
# being updated on another system. We want to
# call for get() function. To get the new
# and up to date version of the asset. And if
# doesn't exist get the asset.
# But it's better to do one by one.
try:
win.multiuser["curs"]["/dev"+asset] = new_assets[asset][0]
except:
pass
elif request[0] == "give":
give(win, request[1], server)
elif request[1] == "at":
print(request)
try:
# If we need an update let's update
if getfoldertime(win.project+request[2]) < request[3]:
win.multiuser["curs"][request[2]] = request[0]
# Else tell the world that we are the newest ones
elif getfoldertime(win.project+request[2]) > request[3]:
insure.send(server, ["at", request[2], getfoldertime(win.project+request[2])])
insure.recv(server)
except:
pass
elif request[0] == "messages":
win.multiuser["messages"] = request[1]
win.multiuser["unread"] += 1
win.scroll["multiuser_messages"] = 0-500*len(request[1])
insure.send(server, "yours")
except Exception as e:
#raise()
win.multiuser["server"] = False
print("Connection Multiuser Error | "+str(e)+" | Line: "+str(sys.exc_info()[-1].tb_lineno))
def getfoldertime(path):
# I noticed a problem with getting a last modification time for a directory.
# It is not nearly the same time as the contents inside that directory. And
# when for example I have /dev/chr/Moria folder. And I want to check is this
# asset is newer here or somebody has a more up to date version. It checks
# for only the time of the folder /dev/chr/Moria and not the contents of
# that folder. Which is not cool. I want to get the newest time from all the
# files and folders inside it. Including in the case of assets the
# /ast/chr/Moria.blend files. So this is why you see this function here.
if os.path.isdir(path):
# So basically we are doing it only if it's actually a directory. In
# case there is an error. I didn't check. But who knows.
# We need to get the full list of subdirectories and files.
allstuff = []
if "/dev/" in path and os.path.exists(path[:path.find("/dev")]+"/ast"+path[path.find("/dev")+4:]+".blend"):
allstuff.append(gettime(path[:path.find("/dev")]+"/ast"+path[path.find("/dev")+4:]+".blend"))
for i in os.walk(path):
if i[-1]:
for b in i[-1]:
allstuff.append(gettime(i[0]+"/"+b))
else:
allstuff.append(gettime(i[0]))
# Now let's find the biggest one
biggest = allstuff[0]
for i in allstuff:
if i > biggest:
biggest = i
return biggest
else:
return gettime(path)
def gettime(path):
# This function will get a pretty time for a given path. The last change
# to the file or the folder recorded by the os.
time_format = "%Y/%m/%d-%H:%M:%S"
timestamp = os.path.getmtime(path)
timestamp = datetime.datetime.fromtimestamp(timestamp)
timestamp = datetime.datetime.strftime(timestamp, time_format)
return timestamp
def connect(win):
# This is going to be a function that connects to the server and makes a
# handshake
while not win.multiuser["server"]:
# So the first step will be to listen for multiuser.
data = ""
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(("255.255.255.255", 54545))
data, addr = sock.recvfrom(1024)
data = data.decode('utf8')
sock.close()
except:
pass
# If any data revieved. It's not nessesarily the server. So let's read it
if data.startswith("VCStudio MULTIUSER SERVER"):
try:
data, ip, project = data.split(" | ")
except:
continue
# So now we know the ip of the server. And the name of a project
# that it's hosting. We need to check that our name is the same
# and if yes. Connect to the server.
if win.analytics["name"] == project:
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.connect((ip, 64646))
win.multiuser["server"] = server
win.multiuser["userid"] = str(server.getsockname()[0])+":"+str(server.getsockname()[1])
insure.recv(server)
insure.send(server, win.settings["Username"])
insure.recv(server)
insure.send(server, win.analytics["name"])
print("Connected to Multiuser as: "+win.multiuser["userid"])
# Adiing myself into the list of all users
win.multiuser["users"][win.multiuser["userid"]] = {"username":win.settings["Username"], "camera":[0,0]}
def hash_file(f):
try:
BLOCKSIZE = 65536
hasher = hashlib.md5()
with open(f, 'rb') as afile:
buf = afile.read(BLOCKSIZE)
while len(buf) > 0:
hasher.update(buf)
buf = afile.read(BLOCKSIZE)
return str(hasher.hexdigest())
except:
return "FOLDER"
def list_all_assets(win):
# This function is listing all the asset CURs in the project. Not the shots
# only the assets. Since we want to have what assets are being created. For
# shots. It's done using the story editor file. So we don't really need it.
allcurs = {}
for c in ["chr", "veh", "loc", "obj"]:
for i in os.listdir(win.project+"/dev/"+c):
allcurs["/"+c+"/"+i] = [
win.multiuser["userid"],
getfoldertime(win.project+"/dev/"+c+"/"+i)
]
return allcurs
def get_give_folder_list(project, folder):
# This function will prepare our folder list for get and giv functions.
path = folder
astblend = path[:path.find("/dev")]+"/ast"+path[path.find("/dev")+4:]+".blend"
fs = []
if os.path.exists(project+astblend):
fs.append([astblend, hash_file(project+astblend)])
# There might not even be any folder as far as this function concerned
# there is nothing in it if there is no folder.
try:
for f in os.walk(project+folder):
# If files in the folder
if f[2]:
for i in f[2]:
fs.append([f[0].replace(project, "")+"/"+i, hash_file(f[0]+"/"+i)])
# Else just put the folder in
else:
fs.append([f[0].replace(project, ""), "FOLDER"])
except Exception as e:
print("get_give_folder_list(): "+str(e))
return fs
def get(win, folder, userid):
# This function will get any folder from any other user using the connection
# to the server.
print("Trying to get: [", folder, "] From: [", userid, "]")
server = win.multiuser["server"]
insure.send(server, ["get", userid, folder])
# Next we will recieve the list of file / folders in the directory
# we are looking for. With the MD5 hash of each. We do not want to
# download files that are identical between both machines.
available = insure.recv(server)
current = get_give_folder_list(win.project, folder)
getlist = []
# Now we need to compare between them to get a list of files we want.
# Also at this stange we can already make the folders
for f in available:
if f not in current:
if f[1] == "FOLDER":
# If it's a folder there is nothing I need to download, we
# already have the name. So let's just make it.
try:
os.makedirs(win.project+f[0])
except:
pass
else:
# If it's not a folder. And it does not exist. Let's actually
# get it.
getlist.append(f[0])
# Now we want to send to that other user the "getlist" so he would know.
# which files to send back to us. This is a bit harder communication then
# just sending a big object contaning all of the files in it. Because I'm
# using Json for the complex data structures. It's just not going to be cool
# because at best it will convert the bytes of the files into strings of
# text. Which are like 4 to 8 times larger in sizes. And at worst it will
# fail complitelly. So what I will do is ask the files one by one. The
# insure script knows how to deal with bytes objects so we are fine.
insure.send(server, getlist)
# Now let's just recieve the files and save them.
for f in getlist:
# We also want the folder to be make just in case.
try:
os.makedirs(win.project+f[:f.rfind("/")])
except:
pass
data = open(win.project+f, "wb")
data.write(insure.recv(server))
data.close()
insure.send(server, "saved")
# Refrashing peviews for the images and stuff
win.checklists = {}
UI_elements.reload_images(win)
def give(win, folder, server):
# This function will send to the server any folder that other users might
# request.
print("Someone wants: [", folder, "]")
# We are going to send the list of files and folder and their hash values
# to the other client. So it could choose what files does it wants. Not
# all files will be changed. So there is no need to copy the entire folder.
insure.send(server, get_give_folder_list(win.project, folder))
# The other user will select the files that he needs. Based on what's
# changed. And will send us the short version of the same list.
getlist = insure.recv(server)
# Now let's send the actuall binaries of the files.
for f in getlist:
print("sending file:", f)
data = open(win.project+f, "rb")
data = data.read()
insure.send(server, data)
insure.recv(server)