522 lines
19 KiB
Python
522 lines
19 KiB
Python
|
# 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()
|
||
|
|
||
|
|