multiuser_server.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. # THIS FILE IS A PART OF VCStudio
  2. # PYTHON 3
  3. ###############################################################################
  4. # This is the SERVER program for the Multiuser system in the VCStudio.
  5. # During production of "I'm Not Even Human" back in 2016 - 2018 we had multiple
  6. # machines to work on the project. And back then some way of moving parts of the
  7. # project between the machines was needed. Originally we were using a simple
  8. # USB thumb drive. But quickly it became inpractical.
  9. # Then I developed a little program called J.Y.Exchange. ( Python 2 ) Which
  10. # was very cool, general purpose moving thing. And after this I introduced
  11. # compatibility layer to J.Y.Exchange using Organizer.
  12. # Later with the development of Blender-Organizer ( It's when I stopped using
  13. # Gtk and wrote the first Organizer with custom UI ) I wanted to make a better
  14. # sync function. And the history recording was a step into that direction.
  15. # Tho unfortunatly. The sync function was never finished.
  16. # This is an attempt so make a sync function with some extended functionality
  17. # so in theory when a large studio wants to use VCStudio they would have a
  18. # way to work with multiple users in the same time.
  19. # CHALLENGES
  20. # The main challenge of this system would be the sizes of the projects. See every
  21. # asset, scene, shot, has a folder. In which you have hundreds of files. Take
  22. # shots for example. Each has 4 render directories. Where the renderer puts
  23. # single frames. 24 frames per second on a large project and we have 2 hundred
  24. # thousand frames for a 2 hour movie. 8 hundred thousand frames if all 4 folders
  25. # are filled up. And it's not counting all the blend files with all the revisions.
  26. # Assets with textures and references. And other various stuff.
  27. # This is not going to be wise to just send it over the network on every frame.
  28. # We need a system of version contoll. Something that is a little
  29. # more dynamic. And doesn't require scanning the entire project.
  30. # HISTORY
  31. # The idea behind history is to record what stuff are being changed. So we are
  32. # sending only the moderatly small analytics file over the network. And if a user
  33. # sees that other user has changed something. They can click a button to update
  34. # this thing on their machine too.
  35. # CONCEPT
  36. # I think every item in the VCStudio. Meaning asset or shot. Every thing that we
  37. # can access using the win.cur variable. Should be concidered as their own things
  38. # but with some smartness added to it. For example. Since there are linked assets
  39. # in blend files for the shots. When updating the shot. You should also update
  40. # the assets.
  41. # This server program is going to be the main allocator of recourses. The all
  42. # knowing wizzard to which all the VCStudios will talk in order to get up to
  43. # date information of who does what.
  44. ###############################################################################
  45. import os
  46. import sys
  47. import time
  48. import insure
  49. import socket
  50. import random
  51. import datetime
  52. import threading
  53. import subprocess
  54. # So the first thing we want to do is to make sure that we are talking to the
  55. # right project. For this we are going to use the name of the project. As
  56. # specified in the analytics data. Not by the folder name. Since multiple
  57. # computers might have different folder names.
  58. project_name = "VCStudio_Multiuser_Project_Name_Failed"
  59. try:
  60. project_name = sys.argv[1]
  61. except:
  62. pass
  63. # Since it's a terminal application. That I'm totally suggest you run from the
  64. # stand alone computer. Rather then from the UI of the Multiuser. We are going
  65. # to treat it as a terminal program and print a bunch stuff to the terminal.
  66. print("\n") # For when running using python3 run.py -ms
  67. # Not at this moment I want to know what is my IP address. In the local network
  68. # space there is.
  69. ipget = subprocess.Popen(["hostname", "-I"],stdout=subprocess.PIPE, universal_newlines=True)
  70. ipget.wait()
  71. thisIP = ipget.stdout.read()[:-2]
  72. # Please tell me if you see an easier methon of gathering the current IP.
  73. # Before we go any fursther I think it's a good idea to actually initilize the
  74. # server.
  75. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  76. sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  77. sock.bind(("", 64646))
  78. sock.listen(0)
  79. # Now since we are using threading. I need a way to manage them somehow. I love
  80. # when 1 function can talk freely to another. So let's use a global dictionary
  81. # of threads.
  82. threads = {} # All of the connections will be each in it's own thread.
  83. users = {} # This is the list of users and metadata about those users.
  84. messages = [["Multiuser Server", "Server Started"]] # This is a list of messages sent by users.
  85. assets = {} # This is a list of assets there is in the project.
  86. story = ["0.0.0.0:0000", {}, "0000/00/00-00:00:00"] # The current story
  87. # story | last modification time
  88. analytics = [{}, "0000/00/00-00:00:00"] # The current analytics
  89. # analytics | last modification time
  90. def output(string):
  91. # This is a fancy Print() function.
  92. cs0 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  93. cs0.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  94. cs0.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
  95. cs0.sendto(bytes("VCStudio MULTIUSER SERVER TERMINAL | "+string, 'utf-8'), ("255.255.255.255", 54545))
  96. cs0.close()
  97. seconds_format = "%H:%M:%S"
  98. time = datetime.datetime.strftime(datetime.datetime.now(), seconds_format)
  99. print(time+" | "+string)
  100. output("multiuser server | started | "+thisIP+" | "+project_name)
  101. # Okay let's define our MAIN. Because It seems like important to do here.
  102. def main():
  103. # This function is going to run in a loop. Unless user specifies to close it
  104. # from the UI or by closing the terminal.
  105. while True:
  106. # Let's listen for connections.
  107. client, ipport = sock.accept()
  108. ip, port = ipport
  109. userid = str(ip)+":"+str(port)
  110. # Now I'm going to add the user into the main users dictionary
  111. users[userid] = {
  112. "client" :client,
  113. "ip" :ip ,
  114. "port" :port ,
  115. "username":"" ,
  116. "request" :[] , # The current request to this or all users
  117. "asnswer" :[] ,
  118. "camera" :[0,0] , # The camera in the story world
  119. "pause" :False # I need this when a different thread is talking
  120. # to the user
  121. }
  122. # And now let's call a thread for this user
  123. threads[userid] = threading.Thread(target=user, args=(userid, ))
  124. threads[userid].setDaemon(True) # So I could close the program
  125. threads[userid].start()
  126. def user(userid):
  127. # Let's get the info about this user
  128. client = users[userid]["client"]
  129. ip = users[userid]["ip"]
  130. port = users[userid]["port"]
  131. username = users[userid]["username"]
  132. # This function will run for every single connected user in it's own thread.
  133. # Then we want to know client's Username.
  134. insure.send(client, "username?")
  135. data = insure.recv(client)
  136. users[userid]["username"] = data
  137. username = users[userid]["username"]
  138. # Now let's check that this person is from our project.
  139. insure.send(client, "project?")
  140. data = insure.recv(client)
  141. # If this is the wrong project. The client will be kicked out.
  142. if data != project_name:
  143. insure.send(client, "wrong_project")
  144. client.close()
  145. return
  146. output(username+" | connected | "+userid)
  147. # So now that the user is here. We can do some stuff with the user.
  148. # And since the user is the user. And server is a server. Like in
  149. # litteral sense of those 2 words. From now own user will be able
  150. # to request a bunch of stuff. So...
  151. request_all([0,"users"])
  152. def get_story(client):
  153. global story
  154. insure.send(client, "story")
  155. clients_story = insure.recv(client)
  156. if clients_story[1] > story[2]:
  157. story = [userid, clients_story[0], clients_story[1]]
  158. users[userid]["camera"] = clients_story[0]["camera"].copy()
  159. insure.send(client, [story[0], story[1]])
  160. insure.recv(client)
  161. def get_assets(client):
  162. insure.send(client, "assets")
  163. clients_assets = insure.recv(client)
  164. # "/chr/Moria" : [userid, timestamp]
  165. for asset in clients_assets:
  166. if asset not in assets or assets[asset][1] < clients_assets[asset][1]:
  167. assets[asset] = clients_assets[asset]
  168. insure.send(client, assets)
  169. insure.recv(client)
  170. def get_analytics(client):
  171. global analytics
  172. insure.send(client, "analytics")
  173. clients_analytics = insure.recv(client)
  174. if clients_analytics[1] > analytics[1]:
  175. analytics = clients_analytics
  176. insure.send(client, analytics[0])
  177. insure.recv(client)
  178. get_story(client)
  179. get_assets(client)
  180. get_analytics(client)
  181. insure.send(client, "yours")
  182. while True:
  183. try:
  184. # Recieving users request
  185. request = insure.recv(client)
  186. if request == "yours":
  187. ###############################################################
  188. # REQUESTS FROM ONE USER TO ANOTHER #
  189. ###############################################################
  190. if len(users[userid]["request"]) > 1:
  191. if users[userid]["request"][1] == "story":
  192. get_story(client)
  193. insure.send(client, "yours")
  194. elif users[userid]["request"][1] == "analytics":
  195. get_analytics(client)
  196. insure.send(client, "yours")
  197. elif users[userid]["request"][1] == "assets":
  198. get_assets(client)
  199. insure.send(client, "yours")
  200. elif users[userid]["request"][1] == "users":
  201. insure.send(client, "users")
  202. elif users[userid]["request"][1] == "at":
  203. insure.send(client, users[userid]["request"])
  204. elif users[userid]["request"][1] == "message":
  205. insure.send(client, ["messages",messages])
  206. elif users[userid]["request"][0] == "serve":
  207. serve(
  208. users[userid]["request"][1],
  209. userid,
  210. users[userid]["request"][2]
  211. )
  212. # Clearing the request.
  213. users[userid]["request"] = []
  214. ###########################################################
  215. # AUTOMATIC REQUESTING #
  216. ###########################################################
  217. if not assets:
  218. request_all([0,"assets"])
  219. print("assets requested")
  220. # If there is nothing we want to tell the user that it's
  221. # their turn to request.
  222. else:
  223. insure.send(client, "yours")
  224. ###############################################################
  225. # REQUESTS FROM USER #
  226. ###############################################################
  227. else:
  228. if request == "story":
  229. get_story(client)
  230. request_all([userid, "story"])
  231. elif request == "analytics":
  232. get_analytics(client)
  233. request_all([userid, "analytics"])
  234. elif request[0] == "at":
  235. output(userid+" | at | "+request[1]+" | time | "+request[2])
  236. request_all([userid, "at", request[1], request[2]])
  237. elif request == "users":
  238. insure.send(client, users_list())
  239. elif request[0] == "message":
  240. messages.append([username, request[1]])
  241. request_all([0, "message"])
  242. elif request[0] == "get":
  243. # If a user sends GET. It means that the user wants a
  244. # folder from another user. This means that we need to
  245. # communicate with 2 clients at ones. Which is kind a
  246. # tricky.
  247. try:
  248. # There is a problem to just simply requesting a user
  249. # directly. Because he might have something requested.
  250. while users[request[1]]["request"]:
  251. time.sleep(0.001)
  252. users[request[1]]["request"] = ["serve", request[2], userid]
  253. # The problem is that this thead will probably continue
  254. # asking stuff from the server. Which is not good. We want
  255. # to stop it here and now.
  256. # Pause
  257. users[userid]["pause"] = True
  258. while users[userid]["pause"]:
  259. time.sleep(0.001) # Funny but thread needs to do something or
  260. # it pauses all of the threads. LOL.
  261. # The other thread will unpause this thread when the
  262. # folder is downloaded.
  263. except Exception as e:
  264. # Sometimes there is no user to get it from.
  265. output(userid+" | get | error | "+str(e))
  266. globals()["assets"] = {}
  267. # Finishing the request and giving the user it's turn
  268. insure.send(client, "yours")
  269. except Exception as e:
  270. # If the connection is lost. We want to delete the user.
  271. request_all([0,"users"])
  272. output(username+" | "+userid+" | "+str(e)+" | line: "+str(sys.exc_info()[-1].tb_lineno))
  273. try:
  274. del users[userid]
  275. except Exception as e:
  276. output("deleting user error | "+str(e))
  277. # We want to clear the data of the assets if this happens
  278. globals()["assets"] = {}
  279. return
  280. def users_list():
  281. # This function will make users
  282. U = {}
  283. for user in list(users.keys()):
  284. U[user] = {
  285. "username":users[user]["username"],
  286. "camera" :users[user]["camera"]
  287. }
  288. return U
  289. def request_all(request):
  290. for user in users:
  291. if user != request[0]:
  292. #while users[user]["request"]:
  293. # time.sleep(0.001)
  294. users[user]["request"] = request
  295. def broadcast():
  296. # This function will broadcast the IP address of the server to all VCStudio
  297. # users. So the user experience would be straight forward. As clicking a button
  298. # and not complex as knowing the IP and stuff. I mean yes. Good for you if you
  299. # want to do everything manually. But most people are dumb.
  300. cs1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  301. cs1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  302. cs1.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
  303. message = "VCStudio MULTIUSER SERVER | "+thisIP+" | "+project_name
  304. cs1.sendto(bytes(message, 'utf-8'), ("255.255.255.255", 54545))
  305. cs1.close()
  306. def listen():
  307. # This function will listen to all commenications for any kind of abbort or
  308. # extreme messages.
  309. message = ""
  310. # Let's try receiving messages from the outside.
  311. try:
  312. sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  313. sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  314. sock.bind(("255.255.255.255", 54545))
  315. sock.settimeout(0.05)
  316. data, addr = sock.recvfrom(1024)
  317. data = data.decode('utf8')
  318. sock.close()
  319. message = data
  320. except:
  321. pass
  322. # Now let's close the serer if the message is VCStudio ABORT MULTIUSER
  323. if message == "VCStudio ABORT MULTIUSER":
  324. output("recieved abort message | closing")
  325. exit()
  326. def serve(folder, fromid, toid):
  327. output("serving | "+folder+" | from | "+fromid+" | to | "+toid)
  328. # Let's first of all get all the data that we need before starting the
  329. # operation.
  330. to = users[ toid ]["client"]
  331. fr = users[fromid]["client"]
  332. # Now let's tell our "fr" that we need the "folder" from him.
  333. insure.send(fr, ["give", folder])
  334. # The "fr" client should respond with a list of files / folder and their
  335. # hashes. The is no need for us ho have this data. So let's pipe it
  336. # directly to the "to" user.
  337. insure.send(to, insure.recv(fr))
  338. # Now we are going to retvieve the list of files that the user needs.
  339. # We are going to save this list since we will be doing the connection of
  340. # them transferring the files. And we need to know the length of it.
  341. getlist = insure.recv(to)
  342. insure.send(fr, getlist)
  343. # Now let's serve the files.
  344. for f in getlist:
  345. print("serving file:",f)
  346. insure.send(to, insure.recv(fr))
  347. insure.send(fr, insure.recv(to))
  348. # And finally let's release our "to".
  349. users[toid]["pause"] = False
  350. # Before we start the main loop I want to have the server loop too.
  351. threads["server_listen"] = threading.Thread(target=main, args=())
  352. threads["server_listen"].setDaemon(True) # So I could close the program
  353. threads["server_listen"].start()
  354. while True:
  355. # This next stuff will be running in the main thread in the loop.
  356. # First we are going to broadcast ourselves.
  357. broadcast()
  358. # Then we are going to listen for input from the Users. Because some kind of
  359. # abbort operation could be called using UDP protocol. And we want to be
  360. # able to hear it.
  361. listen()