505 lines
20 KiB
Python
505 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"
|
||
|
else:
|
||
|
t = "/rnd"
|
||
|
|
||
|
# 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"]:
|
||
|
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)
|
||
|
|