# simple proof-of-concept exercise in PyMOL synchronization

# NOT FOR PRODUCTION USE! HIGHLY INSECURE!

# You need to launch two PyMOL simultaneously as follows...

# MASTER/SLAVE (on same machine):

# pymol syncmol.py -- recv 8000
# pymol syncmol.py -- send 8000

# MUTUAL SYNCHRONIZATION (on same machine):

# pymol syncmol.py -- recv 8001 send 8002
# pymol syncmol.py -- send 8001 recv 8002

# MUTUAL SYNCHRONIZATION (over a lan):

# on host1: pymol syncmol.py -- recv 8000 send host2:8000
# on host2: pymol syncmol.py -- recv 8000 send host1:8000

import threading
import socket
import socket # For gethostbyaddr()
import sys
import traceback
import copy
import os
import time

try:
    import cPickle
    import SocketServer
    import Queue
except ImportError:
    import pickle as cPickle
    import socketserver as SocketServer
    import queue as Queue

    
class PyMOLWriter: # this class transmits

    def __init__(self, pymol, host='localhost', port=8000):
        host = str(host)
        port = int(port)
        self.host = host
        self.port = port
        self.sock = None
        self.cmd = pymol.cmd
        self.fifo = Queue.Queue(0)
        cmd = self.cmd

        print(" syncmol: writing to %s:%d"%(host,port))
        pymol.cmd.log_open(self.fifo)
        
        last_view = None
        last_frame = 0
        while 1:
            time.sleep(0.1) # update 10x a second
            view = cmd.get_view(output=4)
            try:
                cmd.lock()
                if hasattr(cmd,"_last_view"):
                    last_view = cmd._last_view
                    del cmd._last_view
            finally:
                cmd.unlock(-1)
            deferred = 0
            if not self.fifo.empty():
                if view != last_view:
                    self._remote_call("do",("_ cmd.set('defer_updates')",),{'log':0})
                    deferred = 1
                do_list = []
                while not self.fifo.empty():
                    do_list.append(self.fifo.get())
                self._remote_call("do",(do_list,),{'log':0})
            if view != last_view:
                self._remote_call("remote_set_view",(view,))
                last_view = view
                if deferred:
                    self._remote_call("do",("_ cmd.unset('defer_updates')",),{'log':0})
            if not cmd.get_movie_playing():
                frame = int(cmd.get("frame"))
                if last_frame != frame:
                    self._remote_call("frame",(frame,))
                    last_frame = frame
            else:
                last_frame = None
            
                
    def _remote_call(self,meth,args=(),kwds={}):
        result = None
        while self.sock == None:
            try:
                self.sock=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
                self.sock.connect((self.host,self.port))
                self.send = self.sock.makefile('w')
                self.recv = self.sock.makefile('r')
            except:
                self.sock=None
                print("retrying...")
                time.sleep(1)
        cPickle.dump(meth,self.send,1) # binary by default
        cPickle.dump(args,self.send,1)
        cPickle.dump(kwds,self.send,1)
        self.send.flush()
        result = cPickle.load(self.recv)
        return result

def remote_set_view(view,_self=cmd):
    try:
        _self.lock()
        _self._last_view = view
        _self.set_view(view)
    finally:
        _self.unlock(-1)
    
class PyMOLReader: # this class receives

    def __init__(self,pymol,port):

        sys.setcheckinterval(0)
        
        server_address = ('', port)

        ddbs = _PyMOLReader(server_address, _PyMOLRequestHandler)  

        # bind pymol instance to the reader

        pymol.cmd.remote_set_view = remote_set_view
        ddbs.cmd = pymol.cmd

        print(" syncmol: reading from port %d"%(port))
        # now serve requests forever
        ddbs.keep_alive = 1
        while ddbs.keep_alive:
            ddbs.handle_request()
        
class _PyMOLReader(SocketServer.ThreadingTCPServer):

     def server_bind(self):
          """Override server_bind to store the server name."""
          SocketServer.ThreadingTCPServer.server_bind(self)
          host, port = self.socket.getsockname()
          if not host or host == '0.0.0.0':
                host = socket.gethostname()
          try:
              hostname, hostnames, hostaddrs = socket.gethostbyaddr(host)
              if '.' not in hostname:
                    for host in hostnames:
                         if '.' in host:
                              hostname = host
                              break
          except:
              hostname = 'localhost'
          self.server_name = hostname
          self.server_port = port

class _PyMOLRequestHandler(SocketServer.StreamRequestHandler):

     def handle(self):
         while self.server.keep_alive:
             # get method name from client

             try:
                 method = cPickle.load(self.rfile)
             except (EOFError, socket.error):
                 break

             if method == 'shutdown':
                 self.server.keep_alive = 0
                 
             # get arguments from client
             args = cPickle.load(self.rfile)
             kw = cPickle.load(self.rfile)

             # get cmd method pointer
             meth_obj = getattr(self.server.cmd,method)

             # call method and return result
             cPickle.dump(meth_obj(*args, **kw),self.wfile,1) # binary by default
             self.wfile.flush()
             
if __name__=='pymol':
    import os
    import pymol
    sys.argv.reverse()
    sys.argv.pop()
    while len(sys.argv):
        tok = sys.argv.pop()
        if tok == 'recv':
            port = int(sys.argv.pop())
            _stdin_reader_thread = threading.Thread(target=PyMOLReader,
                                                    args=(pymol,port))
            _stdin_reader_thread.setDaemon(1)
            _stdin_reader_thread.start()
        elif tok == 'send':
            addr = sys.argv.pop().split(':')
            if len(addr)==1:
                host = 'localhost'
                port = int(addr[0])
            else:
                host = addr[0]
                port = addr[1]
            _stdin_reader_thread = threading.Thread(target=PyMOLWriter,
                                                    args=(pymol,host,port))
            _stdin_reader_thread.setDaemon(1)
            _stdin_reader_thread.start()

            
    
