network_multiuser.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. # THIS FILE IS A PART OF VCStudio
  2. # PYTHON 3
  3. ###############################################################################
  4. # In order for Multiuser to function. VCStudio should have a process on the
  5. # background can access to all the data, that will talk to the Multiuser
  6. # server.
  7. ###############################################################################
  8. ##################### IMPLEMENTED FEATURES LIST ###############################
  9. # [V] List of users | Important to know the usernames of all users
  10. # [V] Assets | Important to have up to date assets everywhere
  11. # [ ] Shots | Important to have up to date shots / and their assets
  12. # [ ] Rendering | Ability to render on a separate machine.
  13. # [ ] Story | Real time Sync of the story edito and the script writer
  14. # [ ] Analytics | Real time Sync of History and Schedules.
  15. # [ ] Messages | A little messaging system in the Multiuser window.
  16. ###############################################################################
  17. import os
  18. import sys
  19. import time
  20. import json
  21. import socket
  22. import random
  23. import hashlib
  24. import datetime
  25. import threading
  26. import subprocess
  27. from UI import UI_elements
  28. from settings import talk
  29. from network import insure
  30. time_format = "%Y/%m/%d-%H:%M:%S"
  31. def client(win):
  32. ###########################################################################
  33. # This function is the thing that we need. It's going to run in it's own
  34. # thread. Separatelly from the rest of the program.
  35. ###########################################################################
  36. while True:
  37. try:
  38. # So the first thing that we want to do is to listen for a server
  39. # broadcasting himself. And when found connect to that server.
  40. connect(win)
  41. # Then it's going to ask us that project are we in and our username.
  42. server = win.multiuser["server"]
  43. # Then as soon as we are connected. We want to start the main loop. I know
  44. # that some stuff should be done early. But I guess it's better to do them
  45. # in the main loop. So basically the server will request a bunch of stuff.
  46. # And when it sends no valid request. Or something like KEEP ALIVE then it's
  47. # a turn for as to send requests to server. Or nothing. The communication
  48. # happens all the time.
  49. while win.multiuser["server"]:
  50. request = insure.recv(server)
  51. if request == "yours":
  52. ############################################################
  53. # WE REQUEST FROM SERVER #
  54. ############################################################
  55. if win.multiuser["curs"]:
  56. # The get asset function
  57. get(win, list(win.multiuser["curs"].keys())[0],
  58. win.multiuser["curs"][list(win.multiuser["curs"].keys())[0]])
  59. try:
  60. del win.multiuser["curs"][list(win.multiuser["curs"].keys())[0]]
  61. except:
  62. pass
  63. elif win.multiuser["request"]:
  64. insure.send(server, win.multiuser["request"])
  65. win.multiuser["request"] = []
  66. elif not win.multiuser["users"]:
  67. insure.send(server, "users")
  68. win.multiuser["users"] = insure.recv(server)
  69. elif win.cur and win.cur != win.multiuser["last_request"]:
  70. win.multiuser["last_request"] = win.cur
  71. # If we are currently at some asset or shot. We want to
  72. # send to the server the current state of the folder
  73. # if such exists. So if we have the latest one. Every
  74. # body else could send us a give request. But if somebody
  75. # has the latest and it's not us. We could send them
  76. # the get request.
  77. # First step will be to check whitch cur are we in.
  78. if win.url == "assets":
  79. t = "/dev"
  80. elif win.url == "script":
  81. t = "/rnd"
  82. else:
  83. t = ""
  84. # Then we need to see if there is a folder to begin
  85. # with. Some scenes and some shots have no folder.
  86. if not os.path.exists(win.project+t+win.cur):
  87. insure.send(server, "yours")
  88. else:
  89. # If there is a folder let's get the timestamp.
  90. timestamp = getfoldertime(win.project+t+win.cur)
  91. insure.send(server, ["at", t+win.cur, timestamp])
  92. else:
  93. insure.send(server, "yours")
  94. if win.url not in ["assets", "script", "analytics"]:
  95. win.multiuser["last_request"] = ""
  96. win.cur = ""
  97. else:
  98. ############################################################
  99. # SERVER REQUESTS FROM US #
  100. ############################################################
  101. if request == "story":
  102. if not win.multiuser["story_check"]:
  103. storytime = "1997/07/30-00:00:00"
  104. win.multiuser["story_check"] = True
  105. else:
  106. storytime = gettime(win.project+"/pln/story.vcss")
  107. selectedtmp = win.story["selected"]
  108. tmppointers = win.story["pointers"]
  109. tmpcamera = win.story["camera"]
  110. insure.send(server, [win.story, storytime])
  111. story = insure.recv(server)
  112. win.story = story[1].copy()
  113. win.story["selected"] = selectedtmp
  114. win.story["camera"] = tmpcamera
  115. win.story["pointers"] = tmppointers
  116. if story[0] in win.multiuser["users"]:
  117. win.multiuser["users"][story[0]]["camera"] = story[1]["camera"]
  118. elif request == "users":
  119. win.multiuser["users"] = {}
  120. elif request == "analytics":
  121. if not win.multiuser["analytics_check"]:
  122. analyticstime = "1997/07/30-00:00:00"
  123. win.multiuser["analytics_check"] = True
  124. else:
  125. analyticstime = datetime.datetime.strftime(datetime.datetime.now(), time_format)
  126. insure.send(server, [win.analytics, analyticstime])
  127. win.analytics = insure.recv(server)
  128. elif request == "assets":
  129. assets = list_all_assets(win)
  130. insure.send(server, assets)
  131. new_assets = insure.recv(server)
  132. for asset in new_assets:
  133. if asset not in assets or assets[asset][1] < new_assets[asset][1]:
  134. # If a given asset is not on our system or
  135. # being updated on another system. We want to
  136. # call for get() function. To get the new
  137. # and up to date version of the asset. And if
  138. # doesn't exist get the asset.
  139. # But it's better to do one by one.
  140. try:
  141. win.multiuser["curs"]["/dev"+asset] = new_assets[asset][0]
  142. except:
  143. pass
  144. elif request[0] == "give":
  145. give(win, request[1], server)
  146. elif request[1] == "at":
  147. print(request)
  148. try:
  149. # If we need an update let's update
  150. if getfoldertime(win.project+request[2]) < request[3]:
  151. win.multiuser["curs"][request[2]] = request[0]
  152. # Else tell the world that we are the newest ones
  153. elif getfoldertime(win.project+request[2]) > request[3]:
  154. insure.send(server, ["at", request[2], getfoldertime(win.project+request[2])])
  155. insure.recv(server)
  156. except:
  157. pass
  158. elif request[0] == "messages":
  159. win.multiuser["messages"] = request[1]
  160. win.multiuser["unread"] += 1
  161. win.scroll["multiuser_messages"] = 0-500*len(request[1])
  162. insure.send(server, "yours")
  163. except Exception as e:
  164. #raise()
  165. win.multiuser["server"] = False
  166. print("Connection Multiuser Error | "+str(e)+" | Line: "+str(sys.exc_info()[-1].tb_lineno))
  167. def getfoldertime(path):
  168. # I noticed a problem with getting a last modification time for a directory.
  169. # It is not nearly the same time as the contents inside that directory. And
  170. # when for example I have /dev/chr/Moria folder. And I want to check is this
  171. # asset is newer here or somebody has a more up to date version. It checks
  172. # for only the time of the folder /dev/chr/Moria and not the contents of
  173. # that folder. Which is not cool. I want to get the newest time from all the
  174. # files and folders inside it. Including in the case of assets the
  175. # /ast/chr/Moria.blend files. So this is why you see this function here.
  176. if os.path.isdir(path):
  177. # So basically we are doing it only if it's actually a directory. In
  178. # case there is an error. I didn't check. But who knows.
  179. # We need to get the full list of subdirectories and files.
  180. allstuff = []
  181. if "/dev/" in path and os.path.exists(path[:path.find("/dev")]+"/ast"+path[path.find("/dev")+4:]+".blend"):
  182. allstuff.append(gettime(path[:path.find("/dev")]+"/ast"+path[path.find("/dev")+4:]+".blend"))
  183. for i in os.walk(path):
  184. if i[-1]:
  185. for b in i[-1]:
  186. allstuff.append(gettime(i[0]+"/"+b))
  187. else:
  188. allstuff.append(gettime(i[0]))
  189. # Now let's find the biggest one
  190. biggest = allstuff[0]
  191. for i in allstuff:
  192. if i > biggest:
  193. biggest = i
  194. return biggest
  195. else:
  196. return gettime(path)
  197. def gettime(path):
  198. # This function will get a pretty time for a given path. The last change
  199. # to the file or the folder recorded by the os.
  200. time_format = "%Y/%m/%d-%H:%M:%S"
  201. timestamp = os.path.getmtime(path)
  202. timestamp = datetime.datetime.fromtimestamp(timestamp)
  203. timestamp = datetime.datetime.strftime(timestamp, time_format)
  204. return timestamp
  205. def connect(win):
  206. # This is going to be a function that connects to the server and makes a
  207. # handshake
  208. while not win.multiuser["server"]:
  209. # So the first step will be to listen for multiuser.
  210. data = ""
  211. try:
  212. sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  213. sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  214. sock.bind(("255.255.255.255", 54545))
  215. data, addr = sock.recvfrom(1024)
  216. data = data.decode('utf8')
  217. sock.close()
  218. except:
  219. pass
  220. # If any data revieved. It's not nessesarily the server. So let's read it
  221. if data.startswith("VCStudio MULTIUSER SERVER"):
  222. try:
  223. data, ip, project = data.split(" | ")
  224. except:
  225. continue
  226. # So now we know the ip of the server. And the name of a project
  227. # that it's hosting. We need to check that our name is the same
  228. # and if yes. Connect to the server.
  229. if win.analytics["name"] == project:
  230. server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  231. server.connect((ip, 64646))
  232. win.multiuser["server"] = server
  233. win.multiuser["userid"] = str(server.getsockname()[0])+":"+str(server.getsockname()[1])
  234. insure.recv(server)
  235. insure.send(server, win.settings["Username"])
  236. insure.recv(server)
  237. insure.send(server, win.analytics["name"])
  238. print("Connected to Multiuser as: "+win.multiuser["userid"])
  239. # Adiing myself into the list of all users
  240. win.multiuser["users"][win.multiuser["userid"]] = {"username":win.settings["Username"], "camera":[0,0]}
  241. def hash_file(f):
  242. try:
  243. BLOCKSIZE = 65536
  244. hasher = hashlib.md5()
  245. with open(f, 'rb') as afile:
  246. buf = afile.read(BLOCKSIZE)
  247. while len(buf) > 0:
  248. hasher.update(buf)
  249. buf = afile.read(BLOCKSIZE)
  250. return str(hasher.hexdigest())
  251. except:
  252. return "FOLDER"
  253. def list_all_assets(win):
  254. # This function is listing all the asset CURs in the project. Not the shots
  255. # only the assets. Since we want to have what assets are being created. For
  256. # shots. It's done using the story editor file. So we don't really need it.
  257. allcurs = {}
  258. for c in ["chr", "veh", "loc", "obj"]:
  259. for i in os.listdir(win.project+"/dev/"+c):
  260. allcurs["/"+c+"/"+i] = [
  261. win.multiuser["userid"],
  262. getfoldertime(win.project+"/dev/"+c+"/"+i)
  263. ]
  264. return allcurs
  265. def get_give_folder_list(project, folder):
  266. # This function will prepare our folder list for get and giv functions.
  267. path = folder
  268. astblend = path[:path.find("/dev")]+"/ast"+path[path.find("/dev")+4:]+".blend"
  269. fs = []
  270. if os.path.exists(project+astblend):
  271. fs.append([astblend, hash_file(project+astblend)])
  272. # There might not even be any folder as far as this function concerned
  273. # there is nothing in it if there is no folder.
  274. try:
  275. for f in os.walk(project+folder):
  276. # If files in the folder
  277. if f[2]:
  278. for i in f[2]:
  279. fs.append([f[0].replace(project, "")+"/"+i, hash_file(f[0]+"/"+i)])
  280. # Else just put the folder in
  281. else:
  282. fs.append([f[0].replace(project, ""), "FOLDER"])
  283. except Exception as e:
  284. print("get_give_folder_list(): "+str(e))
  285. return fs
  286. def get(win, folder, userid):
  287. # This function will get any folder from any other user using the connection
  288. # to the server.
  289. print("Trying to get: [", folder, "] From: [", userid, "]")
  290. server = win.multiuser["server"]
  291. insure.send(server, ["get", userid, folder])
  292. # Next we will recieve the list of file / folders in the directory
  293. # we are looking for. With the MD5 hash of each. We do not want to
  294. # download files that are identical between both machines.
  295. available = insure.recv(server)
  296. current = get_give_folder_list(win.project, folder)
  297. getlist = []
  298. # Now we need to compare between them to get a list of files we want.
  299. # Also at this stange we can already make the folders
  300. for f in available:
  301. if f not in current:
  302. if f[1] == "FOLDER":
  303. # If it's a folder there is nothing I need to download, we
  304. # already have the name. So let's just make it.
  305. try:
  306. os.makedirs(win.project+f[0])
  307. except:
  308. pass
  309. else:
  310. # If it's not a folder. And it does not exist. Let's actually
  311. # get it.
  312. getlist.append(f[0])
  313. # Now we want to send to that other user the "getlist" so he would know.
  314. # which files to send back to us. This is a bit harder communication then
  315. # just sending a big object contaning all of the files in it. Because I'm
  316. # using Json for the complex data structures. It's just not going to be cool
  317. # because at best it will convert the bytes of the files into strings of
  318. # text. Which are like 4 to 8 times larger in sizes. And at worst it will
  319. # fail complitelly. So what I will do is ask the files one by one. The
  320. # insure script knows how to deal with bytes objects so we are fine.
  321. insure.send(server, getlist)
  322. # Now let's just recieve the files and save them.
  323. for f in getlist:
  324. # We also want the folder to be make just in case.
  325. try:
  326. os.makedirs(win.project+f[:f.rfind("/")])
  327. except:
  328. pass
  329. data = open(win.project+f, "wb")
  330. data.write(insure.recv(server))
  331. data.close()
  332. insure.send(server, "saved")
  333. # Refrashing peviews for the images and stuff
  334. win.checklists = {}
  335. UI_elements.reload_images(win)
  336. def give(win, folder, server):
  337. # This function will send to the server any folder that other users might
  338. # request.
  339. print("Someone wants: [", folder, "]")
  340. # We are going to send the list of files and folder and their hash values
  341. # to the other client. So it could choose what files does it wants. Not
  342. # all files will be changed. So there is no need to copy the entire folder.
  343. insure.send(server, get_give_folder_list(win.project, folder))
  344. # The other user will select the files that he needs. Based on what's
  345. # changed. And will send us the short version of the same list.
  346. getlist = insure.recv(server)
  347. # Now let's send the actuall binaries of the files.
  348. for f in getlist:
  349. print("sending file:", f)
  350. data = open(win.project+f, "rb")
  351. data = data.read()
  352. insure.send(server, data)
  353. insure.recv(server)