Multiuser Alpha 1.0

This is the historic moment When the multiuser was implemented.
This commit is contained in:
Jeison Yehuda Amihud (Blender Dumbass) 2021-01-02 23:31:37 +00:00
parent 7f6ba90906
commit ba370b7cb1
7 changed files with 1371 additions and 12 deletions

153
network/insure.py Normal file
View file

@ -0,0 +1,153 @@
# THIS FILE IS A PART OF VCStudio
# PYTHON 3
###############################################################################
# With the Multiuser system. It became clear early on that simply using the UDP
# protocol would not be cool. Because it's just not checking enough things.
# In my point of view. I want to send to the end network all the data directly
# and not care too much about what format it is in and stuff.
# So this file will insure that the data will be transferred what ever it is
# just as is. The hard part will be here.
###############################################################################
import json
def send(c, message):
# This function will do the sending of the message. I will treat every
# message as if it's a file. And will write everything into a json format
# for transferring. Unless it's already in binarry. In which case I will
# keep it that way.
# The mode will be either B ( bytes ) or J ( json ). All the rest will
# handalled by the json madule hopefully.
T = b"B"
if type(message) != bytes:
T = b"J"
message = bytes(json.dumps(message), 'utf-8')
# So now it's bytes anyway and it means we can send it over the network.
# This will be done in 3 stages. TYPE ( T ), AMOUNT ( len(message) )
# and the message it self. Always.
# Now we we going to wait for a send message from the other side.
m = c.recv(4)
m = m.decode('utf8')
while m != "SEND":
print("DE-SYNCED! '"+m+"' NOT 'SEND'")
c.sendall(b"N")
m = c.recv(4)
m = m.decode('utf8')
c.sendall(T)
c.recv(2)
c.sendall(bytes(str(len(message)), 'utf-8'))
c.recv(2)
c.sendall(message)
c.recv(2)
def recv(c):
# This function will do the recieving of the message.
# I guess in order to fix most of the problems with this recv function
# which is a little unsynsing here and there. I will make a fail switch
# so it wil alight it self properly even it network if hidby, bibdy or
# any of my server or client code is terribly unrelible.
c.sendall(b"SEND")
# It might fail or it might work
T = c.recv(1)
T = T.decode('utf8')
tr = 0
while T not in ["B", "J"]:
print("DE-SYNCED! '"+T+"' NOT 'J' OR 'B'")
c.sendall(b"SEND")
T = c.recv(1)
T = T.decode('utf8')
tr = tr + 1
if tr == 8:
exit()
c.sendall(b"OK")
# So here we just recieved the T , Type of our message
SIZE = c.recv(1024)
SIZE = int(SIZE.decode('utf8'))
# Now we recieved our amount. Next is to recieve the message
c.sendall(b"OK")
message = b""
cs = 0
while SIZE > cs:
l = c.recv(SIZE)
cs = cs + len(l)
message = message + l
c.sendall(b"OK")
message[SIZE:]
# Now let's ge the data back from JSON id it's a JSON.
if T == "J":
message = json.loads(message.decode('utf8'))
return message
# P for Pain
# Can't afford to feel the joy
# Can't find strength to feel the pain
# I'll shrink and go and feel no more
# Is it fine to be lame?
# I don't want death. Since death
# Has an opposite effect
# From suffering from being lame
# Death is opposite of pain
# An importance of the pain
# Is that joy is just a drug
# Such as suffering and pain
# It's a head confusing bug
# Joyful, satisfying shit
# make you feel comfortable
# But discomfort and the pain
# It's what is affordable
# Lameness feel pathetic shit
# Me described using four words
# No more joy, only regrets
# Five more words of discomfort
# Shrink from big and fall from tall
# Give up everything you have
# Go homeless, starve, and suffer. Feel.
# The pain inducing hollow and venomous discomfort,
# swallowing all and every thought and only
# pain. Pain. And nothing more exists.
# And nothing more is fair.
# Can't afford to feel the joy
# But find strength to feel the pain
# I'll shrink and go and feel no more
# It is fine to be lame.
# https://open.lbry.com/@blenderdumbass:f/P-for-Pain:f?r=GRS5mK5GDEGa77vRotG4LwMqPCyhpF2k

521
network/multiuser_server.py Normal file
View file

@ -0,0 +1,521 @@
# THIS FILE IS A PART OF VCStudio
# PYTHON 3
###############################################################################
# This is the SERVER program for the Multiuser system in the VCStudio.
# During production of "I'm Not Even Human" back in 2016 - 2018 we had multiple
# machines to work on the project. And back then some way of moving parts of the
# project between the machines was needed. Originally we were using a simple
# USB thumb drive. But quickly it became inpractical.
# Then I developed a little program called J.Y.Exchange. ( Python 2 ) Which
# was very cool, general purpose moving thing. And after this I introduced
# compatibility layer to J.Y.Exchange using Organizer.
# Later with the development of Blender-Organizer ( It's when I stopped using
# Gtk and wrote the first Organizer with custom UI ) I wanted to make a better
# sync function. And the history recording was a step into that direction.
# Tho unfortunatly. The sync function was never finished.
# This is an attempt so make a sync function with some extended functionality
# so in theory when a large studio wants to use VCStudio they would have a
# way to work with multiple users in the same time.
# CHALLENGES
# The main challenge of this system would be the sizes of the projects. See every
# asset, scene, shot, has a folder. In which you have hundreds of files. Take
# shots for example. Each has 4 render directories. Where the renderer puts
# single frames. 24 frames per second on a large project and we have 2 hundred
# thousand frames for a 2 hour movie. 8 hundred thousand frames if all 4 folders
# are filled up. And it's not counting all the blend files with all the revisions.
# Assets with textures and references. And other various stuff.
# This is not going to be wise to just send it over the network on every frame.
# We need a system of version contoll. Something that is a little
# more dynamic. And doesn't require scanning the entire project.
# HISTORY
# The idea behind history is to record what stuff are being changed. So we are
# sending only the moderatly small analytics file over the network. And if a user
# sees that other user has changed something. They can click a button to update
# this thing on their machine too.
# CONCEPT
# I think every item in the VCStudio. Meaning asset or shot. Every thing that we
# can access using the win.cur variable. Should be concidered as their own things
# but with some smartness added to it. For example. Since there are linked assets
# in blend files for the shots. When updating the shot. You should also update
# the assets.
# This server program is going to be the main allocator of recourses. The all
# knowing wizzard to which all the VCStudios will talk in order to get up to
# date information of who does what.
###############################################################################
import os
import sys
import time
import insure
import socket
import random
import datetime
import threading
import subprocess
# So the first thing we want to do is to make sure that we are talking to the
# right project. For this we are going to use the name of the project. As
# specified in the analytics data. Not by the folder name. Since multiple
# computers might have different folder names.
project_name = "VCStudio_Multiuser_Project_Name_Failed"
try:
project_name = sys.argv[1]
except:
pass
# Since it's a terminal application. That I'm totally suggest you run from the
# stand alone computer. Rather then from the UI of the Multiuser. We are going
# to treat it as a terminal program and print a bunch stuff to the terminal.
print("\n") # For when running using python3 run.py -ms
# Not at this moment I want to know what is my IP address. In the local network
# space there is.
ipget = subprocess.Popen(["hostname", "-I"],stdout=subprocess.PIPE, universal_newlines=True)
ipget.wait()
thisIP = ipget.stdout.read()[:-2]
# Please tell me if you see an easier methon of gathering the current IP.
# Before we go any fursther I think it's a good idea to actually initilize the
# server.
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(("", 64646))
sock.listen(0)
# Now since we are using threading. I need a way to manage them somehow. I love
# when 1 function can talk freely to another. So let's use a global dictionary
# of threads.
threads = {} # All of the connections will be each in it's own thread.
users = {} # This is the list of users and metadata about those users.
messages = [["Multiuser Server", "Server Started"]] # This is a list of messages sent by users.
assets = {} # This is a list of assets there is in the project.
story = ["0.0.0.0:0000", {}, "0000/00/00-00:00:00"] # The current story
# story | last modification time
analytics = [{}, "0000/00/00-00:00:00"] # The current analytics
# analytics | last modification time
def output(string):
# This is a fancy Print() function.
cs0 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
cs0.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
cs0.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
cs0.sendto(bytes("VCStudio MULTIUSER SERVER TERMINAL | "+string, 'utf-8'), ("255.255.255.255", 54545))
cs0.close()
seconds_format = "%H:%M:%S"
time = datetime.datetime.strftime(datetime.datetime.now(), seconds_format)
print(time+" | "+string)
output("multiuser server | started | "+thisIP+" | "+project_name)
# Okay let's define our MAIN. Because It seems like important to do here.
def main():
# This function is going to run in a loop. Unless user specifies to close it
# from the UI or by closing the terminal.
while True:
# Let's listen for connections.
client, ipport = sock.accept()
ip, port = ipport
userid = str(ip)+":"+str(port)
# Now I'm going to add the user into the main users dictionary
users[userid] = {
"client" :client,
"ip" :ip ,
"port" :port ,
"username":"" ,
"request" :[] , # The current request to this or all users
"asnswer" :[] ,
"camera" :[0,0] , # The camera in the story world
"pause" :False # I need this when a different thread is talking
# to the user
}
# And now let's call a thread for this user
threads[userid] = threading.Thread(target=user, args=(userid, ))
threads[userid].setDaemon(True) # So I could close the program
threads[userid].start()
def user(userid):
# Let's get the info about this user
client = users[userid]["client"]
ip = users[userid]["ip"]
port = users[userid]["port"]
username = users[userid]["username"]
# This function will run for every single connected user in it's own thread.
# Then we want to know client's Username.
insure.send(client, "username?")
data = insure.recv(client)
users[userid]["username"] = data
username = users[userid]["username"]
# Now let's check that this person is from our project.
insure.send(client, "project?")
data = insure.recv(client)
# If this is the wrong project. The client will be kicked out.
if data != project_name:
insure.send(client, "wrong_project")
client.close()
return
output(username+" | connected | "+userid)
# So now that the user is here. We can do some stuff with the user.
# And since the user is the user. And server is a server. Like in
# litteral sense of those 2 words. From now own user will be able
# to request a bunch of stuff. So...
def get_story(client):
global story
insure.send(client, "story")
clients_story = insure.recv(client)
if clients_story[1] > story[2]:
story = [userid, clients_story[0], clients_story[1]]
users[userid]["camera"] = clients_story[0]["camera"].copy()
insure.send(client, [story[0], story[1]])
insure.recv(client)
def get_assets(client):
insure.send(client, "assets")
clients_assets = insure.recv(client)
# "/chr/Moria" : [userid, timestamp]
for asset in clients_assets:
if asset not in assets or assets[asset][1] < clients_assets[asset][1]:
assets[asset] = clients_assets[asset]
insure.send(client, assets)
insure.recv(client)
def get_analytics(client):
global analytics
insure.send(client, "analytics")
clients_analytics = insure.recv(client)
if clients_analytics[1] > analytics[1]:
analytics = clients_analytics
insure.send(client, analytics[0])
insure.recv(client)
get_story(client)
get_assets(client)
get_analytics(client)
insure.send(client, "yours")
# Here when the connection is established. I would like to tell all the
# already connected users that the connection is established. It's going
# to be a 2 way process.
# 1. We send a notice to all the threads saying to send the corrisponding
# users the data about the new user.
request_all([0,"users"])
# 2. We execute this request. Which is not going to happen in this thread
# but in the instances of this thread for all the other users. So the
# code is in this function.
while True:
try:
# Recieving users request
request = insure.recv(client)
if request == "yours":
###############################################################
# REQUESTS FROM ONE USER TO ANOTHER #
###############################################################
if len(users[userid]["request"]) > 1:
if users[userid]["request"][1] == "story":
get_story(client)
insure.send(client, "yours")
elif users[userid]["request"][1] == "analytics":
get_analytics(client)
insure.send(client, "yours")
elif users[userid]["request"][1] == "assets":
get_assets(client)
insure.send(client, "yours")
elif users[userid]["request"][1] == "users":
insure.send(client, "users")
elif users[userid]["request"][1] == "at":
insure.send(client, users[userid]["request"])
elif users[userid]["request"][1] == "message":
insure.send(client, ["messages",messages])
elif users[userid]["request"][0] == "serve":
serve(
users[userid]["request"][1],
userid,
users[userid]["request"][2]
)
# Clearing the request.
users[userid]["request"] = []
# If there is nothing we want to tell the user that it's
# their turn to request.
else:
insure.send(client, "yours")
###############################################################
# REQUESTS FROM USER #
###############################################################
else:
if request == "story":
get_story(client)
request_all([userid, "story"])
elif request == "analytics":
get_analytics(client)
request_all([userid, "analytics"])
elif request[0] == "at":
output(userid+" | at | "+request[1]+" | time | "+request[2])
request_all([userid, "at", request[1], request[2]])
elif request == "users":
insure.send(client, users_list())
elif request[0] == "message":
messages.append([username, request[1]])
request_all([0, "message"])
elif request[0] == "get":
# If a user sends GET. It means that the user wants a
# folder from another user. This means that we need to
# communicate with 2 clients at ones. Which is kind a
# tricky.
try:
# There is a problem to just simply requesting a user
# directly. Because he might have something requested.
while users[request[1]]["request"]:
time.sleep(0.001)
users[request[1]]["request"] = ["serve", request[2], userid]
# The problem is that this thead will probably continue
# asking stuff from the server. Which is not good. We want
# to stop it here and now.
# Pause
users[userid]["pause"] = True
while users[userid]["pause"]:
time.sleep(0.001) # Funny but thread needs to do something or
# it pauses all of the threads. LOL.
# The other thread will unpause this thread when the
# folder is downloaded.
except:
# Sometimes there is no user to get it from.
print("USER GET ERROR")
# Finishing the request and giving the user it's turn
insure.send(client, "yours")
except Exception as e:
# If the connection is lost. We want to delete the user.
request_all([0,"users"])
output(username+" | "+userid+" | "+str(e)+" | line: "+str(sys.exc_info()[-1].tb_lineno))
try:
del users[userid]
except Exception as e:
output("deleting user error | "+str(e))
# We want to clear the data of the assets if this happens
globals()["assets"] = {}
request_all([0,"assets"])
return
def users_list():
# This function will make users
U = {}
for user in list(users.keys()):
U[user] = {
"username":users[user]["username"],
"camera" :users[user]["camera"]
}
return U
def request_all(request):
for user in users:
if user != request[0]:
while users[user]["request"]:
time.sleep(0.001)
users[user]["request"] = request
def broadcast():
# This function will broadcast the IP address of the server to all VCStudio
# users. So the user experience would be straight forward. As clicking a button
# and not complex as knowing the IP and stuff. I mean yes. Good for you if you
# want to do everything manually. But most people are dumb.
cs1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
cs1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
cs1.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
message = "VCStudio MULTIUSER SERVER | "+thisIP+" | "+project_name
cs1.sendto(bytes(message, 'utf-8'), ("255.255.255.255", 54545))
cs1.close()
def listen():
# This function will listen to all commenications for any kind of abbort or
# extreme messages.
message = ""
# Let's try receiving messages from the outside.
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))
sock.settimeout(0.05)
data, addr = sock.recvfrom(1024)
data = data.decode('utf8')
sock.close()
message = data
except:
pass
# Now let's close the serer if the message is VCStudio ABORT MULTIUSER
if message == "VCStudio ABORT MULTIUSER":
output("recieved abort message | closing")
exit()
def serve(folder, fromid, toid):
output("serving | "+folder+" | from | "+fromid+" | to | "+toid)
# Let's first of all get all the data that we need before starting the
# operation.
to = users[ toid ]["client"]
fr = users[fromid]["client"]
# Now let's tell our "fr" that we need the "folder" from him.
insure.send(fr, ["give", folder])
# The "fr" client should respond with a list of files / folder and their
# hashes. The is no need for us ho have this data. So let's pipe it
# directly to the "to" user.
insure.send(to, insure.recv(fr))
# Now we are going to retvieve the list of files that the user needs.
# We are going to save this list since we will be doing the connection of
# them transferring the files. And we need to know the length of it.
getlist = insure.recv(to)
insure.send(fr, getlist)
# Now let's serve the files.
for f in getlist:
print("serving file:",f)
insure.send(to, insure.recv(fr))
insure.send(fr, insure.recv(to))
# And finally let's release our "to".
users[toid]["pause"] = False
# Before we start the main loop I want to have the server loop too.
threads["server_listen"] = threading.Thread(target=main, args=())
threads["server_listen"].setDaemon(True) # So I could close the program
threads["server_listen"].start()
while True:
# This next stuff will be running in the main thread in the loop.
# First we are going to broadcast ourselves.
broadcast()
# Then we are going to listen for input from the Users. Because some kind of
# abbort operation could be called using UDP protocol. And we want to be
# able to hear it.
listen()

View file

@ -0,0 +1,95 @@
# THIS FILE IS A PART OF VCStudio
# PYTHON 3
###############################################################################
# Multiuser is a complex system. It's so complex that my brain is melting at
# the moment. Anyways. Users need a way to talk to the server. And I'm going
# to provide it to the user using a multiuser Layer. See:
# studio/studio_multiuserLayer.py.
# Tho there is one problem. The user should be able to read and write messages
# to the server. And you think. Right. There is a network/network_multiuser.py
# for this. Well. for the most stuff yes.
# There are 2 types of protocols multiuser system uses. The TCP and the UDP.
# TCP protocol is used to tranfer sensetive stuff. Like files, scene text. Stuff
# that has to be at full. And where loosing packages is unexceptable.
# UDP will be used for the other stuff. Like log of the server. And messages to
# the server. Such as an Abort message.
# This file handle the UDP side of things.
###############################################################################
import os
import socket
import datetime
def listen(win):
# Listen function will run in it's own thread and get the data from the
# server. It will be teminal like messages that we show in the multiuser
# window.
UDP_IP = "255.255.255.255"
UDP_PORT = 54545
while True:
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((UDP_IP, UDP_PORT))
#sock.settimeout(0.05)
data, addr = sock.recvfrom(1024)
data = data.decode('utf8')
seconds_format = "%H:%M:%S"
time = datetime.datetime.strftime(datetime.datetime.now(), seconds_format)
if "VCStudio MULTIUSER SERVER TERMINAL" in data:
#print(time+data.replace("VCStudio MULTIUSER SERVER TERMINAL", ""))
win.multiuser["terminal"].append(time+data.replace("VCStudio MULTIUSER SERVER TERMINAL", ""))
# Now i want to use a limit. Because hell I don't want 20000000
# bazillion messages stored in the memory at all times.
win.multiuser["terminal"] = win.multiuser["terminal"][-300:]
# And we want to scroll down on each message so
win.scroll["multiuser_terminal"] = 0 - len(win.multiuser["terminal"])*50
elif "VCStudio ABORT MULTIUSER" in data:
win.multiuser["users"] = {}
sock.close()
except:
pass
def message(message):
# This is a function that will be called by multiple buttons. That want to
# send the server any kind of message. But actually it's gonna send a message
# to 255.255.255.255 port 54545 that the server is listening. At the moment
# of writing this comment the server is programed only for 1 type of command
# like this. It's the "VCStudio ABORT MULTIUSER". That makes the server stop
# working. This command is a bit dangerous at the moment. Because it's running
# on it's own thread in the server. Making it close even in the middle of a
# request. Like it can corrupt a file that's being transferred.
UDP_IP = "255.255.255.255"
UDP_PORT = 54545
cs1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
cs1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
cs1.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
cs1.sendto(bytes(message, 'utf-8'), (UDP_IP, UDP_PORT))
cs1.close()

View file

@ -0,0 +1,504 @@
# 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)

View file

@ -1,22 +1,40 @@
# GNU General Public License
# This script is for developers of VCStudio rendering. This will read any
# message on 127.0.0.1 port 54545
# This script is for developers of VCStudio. This one is for reading messages.
import sys
import datetime
# Fisrt we need to know what mode are we testing.
print(" VCStudio Read Messages.")
print(" Modes: r [Render], m [Multiuser]")
if len(sys.argv) > 2:
mode = sys.argv[2]
print("Mode: "+mode)
else:
mode = input("Mode: ")
import socket
UDP_IP = "127.0.0.1"
UDP_PORT = 54545
if mode == "r":
UDP_IP = "127.0.0.1"
UDP_PORT = 54545
elif mode == "m":
UDP_IP = "255.255.255.255"
UDP_PORT = 54545
else:
print("There is no mode: "+mode)
exit()
prev = ""
while True:
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((UDP_IP, UDP_PORT))
@ -25,8 +43,11 @@ while True:
data, addr = sock.recvfrom(1024)
data = data.decode('utf8')
seconds_format = "%H:%M:%S"
time = datetime.datetime.strftime(datetime.datetime.now(), seconds_format)
if prev != str(data):
print("<<< "+str(data))
print(time+" | "+str(data))
prev = str(data)

View file

@ -1,10 +1,37 @@
# GNU General Public License
# This script is for developers of VCStudio rendering. This will send any
# message you type in the terminal to 127.0.0.1 port 54545
# This script is for developers of VCStudio. This one is sending test messages.
import socket
import time
import sys
# Fisrt we need to know what mode are we testing.
print(" VCStudio Send Test Messages.")
print(" Modes: r [Render], m [Multiuser]")
if len(sys.argv) > 2:
mode = sys.argv[2]
print("Mode: "+mode)
else:
mode = input("Mode: ")
import socket
if mode == "r":
UDP_IP = "127.0.0.1"
UDP_PORT = 54545
elif mode == "m":
UDP_IP = "255.255.255.255"
UDP_PORT = 54545
else:
print("There is no mode: "+mode)
exit()
prev = ""
@ -20,5 +47,5 @@ while True:
cs1.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
#message = "["+str(i)+"]TEST VCStudio rendering"
cs1.sendto(bytes(message, 'utf-8'), ('127.0.0.1', 54545))
cs1.sendto(bytes(message, 'utf-8'), (UDP_IP, UDP_PORT))
#print(message)

38
network/test_client.py Normal file
View file

@ -0,0 +1,38 @@
# GNU General Public License
# This script is for developers of VCStudio. This one is going to simulate the
# VCStudio client. But in a way where you have more control.
# This is a dumb script so deal with it.
import socket
print("What is te IP of the server?")
ip = input("IP: ")
port = 64646
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((ip, port))
# Now let's talk to the server
while True:
# Simple cycle of receive / write
# RECIEVE
data = sock.recv(1024)
data = data.decode('utf8')
print(data)
# WRITE
message = input(">>> ")
if message == "exit":
exit()
message = bytes(message, 'utf-8')
sock.sendall(message)