diff --git a/network/insure.py b/network/insure.py new file mode 100644 index 0000000..d100130 --- /dev/null +++ b/network/insure.py @@ -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 diff --git a/network/multiuser_server.py b/network/multiuser_server.py new file mode 100644 index 0000000..52badb2 --- /dev/null +++ b/network/multiuser_server.py @@ -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() + + diff --git a/network/multiuser_terminal.py b/network/multiuser_terminal.py new file mode 100644 index 0000000..3ad98ef --- /dev/null +++ b/network/multiuser_terminal.py @@ -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() + + diff --git a/network/network_multiuser.py b/network/network_multiuser.py new file mode 100644 index 0000000..4821a1f --- /dev/null +++ b/network/network_multiuser.py @@ -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) + diff --git a/network/read_messages.py b/network/read_messages.py index b742f74..b82f151 100644 --- a/network/read_messages.py +++ b/network/read_messages.py @@ -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) diff --git a/network/send_messages.py b/network/send_messages.py index 8d5c0bc..a5c1390 100644 --- a/network/send_messages.py +++ b/network/send_messages.py @@ -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) diff --git a/network/test_client.py b/network/test_client.py new file mode 100644 index 0000000..3195286 --- /dev/null +++ b/network/test_client.py @@ -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) + +