#!/usr/bin/python

# tz_net_time.py
#       --copyright--                   Copyright 2007 (C) Tranzoa, Co. All rights reserved.    Warranty: You're free and on your own here. This code is not necessarily up-to-date or of public quality.
#       --url--                         http://www.tranzoa.net/tzpython/
#       --email--                       pycode is the name to send to. tranzoa.com is the place to send to.
#       --bodstamps--
#       July 21, 2007           bar
#       July 24, 2007           bar     multiple servers
#       September 15, 2007      bar     allow running as program on non-windows systems, though the system date/time can't be set
#       October 29, 2007        bar     thread for the whole thing
#       October 30, 2007        bar     do more protection against being rate limited by the sites
#       November 3, 2007        bar     notice when the time servers disagree
#       November 18, 2007       bar     turn on doxygen
#       November 20, 2007       bar     comments
#       November 27, 2007       bar     insert boilerplate copyright
#       December 1, 2007        bar     expose routine to set system time
#                                       use tzlib's elapsed_time for wait timing and such
#       December 3, 2007        bar     assume the server's time was from the point at half the duration it took to get it
#                                       time() routine for caller's ease
#       May 17, 2008            bar     email adr
#       November 21, 2008       bar     /s cmd line arg
#       April 11, 2009          bar     no 'as' named variable
#       September 27, 2009      bar     code and notes for setting time under non-windows (unix)
#       September 14, 2010      bar     be sorta graceful in the face of non-Active state pythons (don't work, but don't crash)
#       April 11, 2011          bar     alternatively use the binary protocol on port 37
#                                       take the aol servers off the list
#       November 29, 2011       bar     pyflake cleanup
#       May 27, 2012            bar     doxygen namespace
#       August 12, 2012         bar     put name in thread
#       May 28, 2014            bar     put thread id in threads
#       --eodstamps--
##      \file
#       \namespace              tzpython.tz_net_time
#
#
#       Connect to NIST servers on port 13 and get the ASCII time.
#           Alternatively, get the time from port 37 (binary = True)
#
#       Description of port 13's output:
#
#           http://tf.nist.gov/service/its.htm
#
#
#       http://tf.nist.gov/service/time-servers.html
#
#           time-a.nist.gov                 129.6.15.28     NIST, Gaithersburg, Maryland
#           time-b.nist.gov                 129.6.15.29     NIST, Gaithersburg, Maryland
#           time-a.timefreq.bldrdoc.gov     132.163.4.101   NIST, Boulder, Colorado
#           time-b.timefreq.bldrdoc.gov     132.163.4.102   NIST, Boulder, Colorado
#           time-c.timefreq.bldrdoc.gov     132.163.4.103   NIST, Boulder, Colorado
#           utcnist.colorado.edu            128.138.140.44  University of Colorado, Boulder
#           time.nist.gov                   192.43.244.18   NCAR, Boulder, Colorado
#           time-nw.nist.gov                131.107.1.10    Microsoft, Redmond, Washington
#           nist1.symmetricom.com           69.25.96.13     Symmetricom, San Jose, California
#           nist1-dc.WiTime.net             206.246.118.250 WiTime, Virginia
#           nist1-ny.WiTime.net             208.184.49.9    WiTime, New York City
#           nist1-sj.WiTime.net             64.125.78.85    WiTime, San Jose, California
#           nist1.aol-ca.symmetricom.com    207.200.81.113  Summetricom, AOL facility, Sunnyvale, California
#           nist1.aol-va.symmetricom.com    64.236.96.53    Symmetricom, AOL facility, Virginia
#           nist1.columbiacountyga.gov      68.216.79.113   Columbia County, Georgia
#           nist.expertsmi.com              69.222.103.98   Monroe, Michigan
#
#
#       Readable RFC-1305 NTP packet description:
#
#           http://www.bytefusion.com/products/ntm/pts/rfc1305.htm
#               http://www.bytefusion.com/products/ntm/pts/appendixa.htm
#
#


import  copy
import  datetime
import  os
import  random
import  re
import  select
import  socket
import  struct
import  sys
import  threading
import  time

win32api            = None
have_win_api        = False
if  sys.platform   == 'win32' :
    try :
        import  win32api
        have_win_api    = True
    except ImportError  :
        win32api        = None
    pass


import  tz_parse_time
import  tzlib



##              List off all time servers we know of.
ALL_SERVERS =   [
                    'time-a.nist.gov',
                    'time-b.nist.gov',
                    'time-a.timefreq.bldrdoc.gov',
                    'time-b.timefreq.bldrdoc.gov',
                    'time-c.timefreq.bldrdoc.gov',
                    'utcnist.colorado.edu',
                    'time.nist.gov',
                    'time-nw.nist.gov',
                    'nist1.symmetricom.com',
                    'nist1-dc.WiTime.net',
                    'nist1-ny.WiTime.net',
                    'nist1-sj.WiTime.net',
                    'nist1.columbiacountyga.gov',
                    'nist.expertsmi.com',
                ]


##              Don't hit a server more than once in this often.
MAX_FREQ    =   4.0

##              Track the servers we've hit recently.
servers_hit =   {}


def get_one_server_time(server = None, binary = False) :
    """
        Get the time from the given servers (or default servers if none given.)
    """

    global  servers_hit


    if  not server :
        server = random.choice(ALL_SERVERS)

    prev    = servers_hit.get(server, -MAX_FREQ)
    t       = tzlib.elapsed_time()
    if  t - prev < MAX_FREQ :
        time.sleep(prev + MAX_FREQ - t)

    retval  = 0.0

    if  binary :
        s   = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.settimeout(5.0)
        try:
            s.sendto('', 0, ( server, 37 ) )
            retval  = long(struct.unpack('!L', s.recv(16)[:4])[0])
            #   Convert from 1900/01/01 epoch to 1970/01/01 epoch
            retval -= 2208988800
        except (socket.timeout, socket.error, socket.herror, socket.gaierror, ) :
            pass
        pass
    else    :
        sock    = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

        try :
            sock.connect( ( server, 13 ) )
        except socket.error :
            sock = None
        except socket.herror :
            sock = None
        except socket.gaierror :
            sock = None

        if  sock :
            sock.setblocking(False)

            t   = tzlib.elapsed_time()

            ws  = ""
            while True :
                r   = ""
                try :
                    rs = select.select([sock], [], [sock], 0)
                except select.error :
                    break

                if  len(rs[0]) :
                    # print "read from", server
                    try :
                        r   = sock.recv(8196)
                        if  not len(r) :
                            break
                        ws     += r
                        if  ws.find("*") > 0 :
                            retval      = tz_parse_time.parse_time(ws)
                            if  retval == None :
                                retval  = 0.0
                            break
                        pass
                    except socket.error :
                        break
                    pass

                if  tzlib.elapsed_time() - t > 0.5 :
                    break

                if  not r :
                    time.sleep(0.01)
                pass

            pass

        if  sock :
            sock.close()
        pass

    servers_hit[server] = tzlib.elapsed_time()          # as best we can, remember when we hit this server so we throttle our access to it

    return(retval)



class a_thread(threading.Thread) :
    """ Class for doing one server in a thread. """

    def __init__(me, server, binary = False) :
        """ Constructor. """

        threading.Thread.__init__(me, name = __file__ + '.a_thread')

        me.tid      = None
        me.server   = server
        me.binary   = binary or False
        me.time     = None
        me.setDaemon(True)                              # so that we can kick out of the program while the thread is running


    def run(me) :
        """ Owner object called start on us. Do the thread. """
        me.tid      = tzlib.get_tid()
        me.time     = get_one_server_time(me.server, me.binary)

    pass    # a_thread



##              regx to extract the host name of a server
host_re         = re.compile(r"([^\.]+\.[^\.]+)$")

##              How long we wait for a server response (they must be quick)
MAX_WAIT_TIME   = 2.0


def get_time(servers = None, max_servers = 0, stopper = None, show_info = False, binary = False) :
    """
        Get the time from the given servers (or default servers if none given.)
    """

    tzlib.elapsed_time()

    if  not servers :
        if  not max_servers :
            max_servers = 3
        servers     = ALL_SERVERS

    servers         = copy.copy(servers)

    random.shuffle(servers)
    if  max_servers :
        max_servers = min(max_servers, len(servers))

        srvs        = servers
        servers     = []
        hosts       = {}
        for si in xrange(len(srvs) - 1, -1, -1) :
            if  not max_servers :
                break

            s       = srvs[si]
            g       = host_re.search(s)
            if  g   :
                h   = g.group(1).lower()
                if  not hosts.has_key(h) :
                    hosts[h]        = True              # try to go to different hosts, first
                    servers.append(s)
                    del(srvs[si])
                    max_servers    -= 1
                pass
            pass
        srvs        = srvs[:max_servers]                # and if we've not enough servers, choose from the rest randomly
        servers    += srvs

    times   = []
    threads = []
    for s in servers :
        threads.append(a_thread(s, binary = binary))
        times.append(None)

    cnt     = 0
    sum     = 0.0
    diffs   = False
    ptime   = 0.0

    tb      = tzlib.elapsed_time()

    for thread in threads :
        thread.start()


    while threads :
        if  stopper and stopper.stop_get_time :
            break

        tm      = tzlib.elapsed_time()

        tia     = [ ti for ti in xrange(len(threads)) if threads[ti].time != None ]
        if  tia :
            ti  = tia[0]
            t   = threads[ti].time
            times[ti]       = t                                 # remember this server's time so we can print it out if show_info is true
            if  t > 0 :
                cnt        += 1
                sum        += (t - ((tm - tb) / 2.0))           # assume each direction of the TCP loop takes half the duration it took to get the time - so sum bumped by the time it was at the server when we first started the threads
                if  ptime and (abs(t - ptime) >= 5.0) :
                    diffs   = True
                ptime       = t
            else :
                # print "failed", threads[ti].server
                pass
            del(threads[ti])
        elif (tm - tb >= MAX_WAIT_TIME) and cnt :
            break                                               # if we've done this enough and we have at least one server's response, kick out with "good enough" time
        else :
            time.sleep(0.01)
        pass

    if  (not cnt) or (stopper and stopper.stop_get_time) :
        return(0.0)

    if  diffs and show_info :
        print "Time servers disagree:"
        for ti in xrange(times) :
            if  times[ti] :
                print servers[ti], time.asctime(time.gmtime(times[ti]))
            pass
        print
        pass

    return((sum / cnt) + (tzlib.elapsed_time() - tb))



class   a_net_timer(threading.Thread) :
    """ Class to hit some time servers for the time. """

    def __init__(me, owner, servers = None, max_servers = 0, binary = False) :
        """ Constructor. """

        threading.Thread.__init__(me, name = __file__ + '.a_net_timer')

        me.tid              = None
        me.owner            = owner
        me.servers          = servers
        me.max_servers      = max_servers
        me.binary           = binary or False

        me.when             = 0
        me.stop_get_time    = False

        tzlib.elapsed_time()

        me.setDaemon(True)                              # so that we can kick out of the program while the thread is running


    def run(me) :
        """ Owner object called start on us. Do the thread. """
        me.tid              = tzlib.get_tid()
        me.when             = get_time(me.servers, me.max_servers, stopper = me, binary = me.binary)
        me.when_et          = tzlib.elapsed_time()

        owner               = me.owner
        me.owner            = None
        if  (not me.stop_get_time) and owner and hasattr(owner, 'net_timer_done') :
            owner.net_timer_done(me.when)

        pass


    def stop(me) :
        """ Try to stop us. Effect is not immediate. """

        me.stop_get_time    = True
        me.owner            = None


    def time(me) :
        """ Return the GMT time right now if we have it. Otherwise, return zero. """

        if  me.when <= 0 :
            return(0)

        return(me.when + (tzlib.elapsed_time() - me.when_et))



    pass    # a_net_timer




def set_system_time(t) :
    """
        If we can, set the system clock to the given time.
    """

    if  t :
        if  have_win_api :
            tm  = time.gmtime(t)
            if  win32api.SetSystemTime(int(tm.tm_year), int(tm.tm_mon), 0, int(tm.tm_mday), int(tm.tm_hour), int(tm.tm_min), int(tm.tm_sec), int((t - int(t)) * 1000.0)) :
                return(True)
            pass
        elif sys.platform != 'win32' :
            ts  = datetime.datetime.fromtimestamp(t).isoformat()
            print 'Trying (but must be root and not sudo, apparently) to set [date --set="%s"' % ts
            os.system('date --set="%s\"' % ts)                          # note: can't actually do this unless we're root - and then, what about time zone?
        pass

    return(False)



if  __name__ == '__main__' :

    import  TZCommandLineAtFile


    del(sys.argv[0])
    TZCommandLineAtFile.expand_at_sign_command_line_files(sys.argv)

    set_it  = False
    binary  = False

    while True :
        oi  = tzlib.array_find(sys.argv, [ "--set", "-s", "/s" ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        set_it          = True
        if  not have_win_api :
            if  False :
                print "Cannot set time on non-Windows systems!"
                sys.exit(101)
            pass
        pass

    while True :
        oi  = tzlib.array_find(sys.argv, [ "--binary", "-b", ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        binary          = True



    while True :
        oi  = tzlib.array_find(sys.argv, [ "--each", ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        for srv in ALL_SERVERS :
            t   = get_time([ srv, ], show_info = True, binary = binary)
            print time.asctime(time.localtime(t)), srv
        sys.exit(1)


    servers = []
    while sys.argv :
        servers.append(sys.argv.pop(0))

    t   = get_time(servers, show_info = True, binary = binary)
    p   = time.time()

    if  t :

        print "NIST", time.asctime(time.gmtime(t)), t
        print "PC  ", time.asctime(time.gmtime(p)), p
        print "Diff", t - p

        if  set_it :
            if  not set_system_time(t) :
                print "Cannot set system time!"
                sys.exit(101)

            print "Time set - PC system time was what it was..."
        pass

    else :

        print "No time found!"

    pass


#
#
#
# eof
