xref: /petsc/config/BuildSystem/RDict.py (revision 2fa40bb9206b96114faa7cb222621ec184d31cd2) !
1#!/usr/bin/env python
2'''A remote dictionary server
3
4    RDict is a typed, hierarchical, persistent dictionary intended to manage
5    all arguments or options for a program. The interface remains exactly the
6    same as dict, but the storage is more complicated.
7
8    Argument typing is handled by wrapping all values stored in the dictionary
9    with nargs.Arg or a subclass. A user can call setType() to set the type of
10    an argument without any value being present. Whenever __getitem__() or
11    __setitem__() is called, values are extracted or replaced in the wrapper.
12    These wrappers can be accessed directly using getType(), setType(), and
13    types().
14
15    Hierarchy is allowed using a single "parent" dictionary. All operations
16    cascade to the parent. For instance, the length of the dictionary is the
17    number of local keys plus the number of keys in the parent, and its
18    parent, etc. Also, a dictionary need not have a parent. If a key does not
19    appear in the local dicitonary, the call if passed to the parent. However,
20    in this case we see that local keys can shadow those in a parent.
21    Communication with the parent is handled using sockets, with the parent
22    being a server and the interactive dictionary a client.
23
24    The default persistence mechanism is a pickle file, RDict.db, written
25    whenever an argument is changed locally. A timer thread is created after
26    an initial change, so that many rapid changes do not cause many writes.
27    Each dictionary only saves its local entries, so all parents also
28    separately save data in different RDict.db files. Each time a dictionary
29    is created, the current directory is searched for an RDict.db file, and
30    if found the contents are loaded into the dictionary.
31
32    This script also provides some default actions:
33
34      - server [parent]
35        Starts a server in the current directory with an optional parent. This
36        server will accept socket connections from other dictionaries and act
37        as a parent.
38
39      - client [parent]
40        Creates a dictionary in the current directory with an optional parent
41        and lists the contents. Notice that the contents may come from either
42        an RDict.db file in the current directory, or from the parent.
43
44      - clear [parent]
45        Creates a dictionary in the current directory with an optional parent
46        and clears the contents. Notice that this will also clear the parent.
47
48      - insert <parent> <key> <value>
49        Creates a dictionary in the current directory with a parent, and inserts
50        the key-value pair. If "parent" is "None", no parent is assigned.
51
52      - remove <parent> <key>
53        Creates a dictionary in the current directory with a parent, and removes
54        the given key. If "parent" is "None", no parent is assigned.
55'''
56from __future__ import print_function
57from __future__ import absolute_import
58try:
59  import project          # This is necessary for us to create Project objects on load
60  import build.buildGraph # This is necessary for us to create BuildGraph objects on load
61except ImportError:
62  pass
63import nargs
64
65import pickle
66import os
67import sys
68useThreads = nargs.Arg.findArgument('useThreads', sys.argv[1:])
69if useThreads is None:
70  useThreads = 0 # workaround issue with parallel configure
71elif useThreads == 'no' or useThreads == '0':
72  useThreads = 0
73elif useThreads == 'yes' or useThreads == '1':
74  useThreads = 1
75else:
76  raise RuntimeError('Unknown option value for --useThreads ',useThreads)
77
78class RDict(dict):
79  '''An RDict is a typed dictionary, which may be hierarchically composed. All elements derive from the
80Arg class, which wraps the usual value.'''
81  # The server will self-shutdown after this many seconds
82  shutdownDelay = 60*60*5
83
84  def __init__(self, parentAddr = None, parentDirectory = None, load = 1, autoShutdown = 1, readonly = False):
85    import atexit
86    import time
87    import xdrlib
88
89    self.logFile         = None
90    self.setupLogFile()
91    self.target          = ['default']
92    self.parent          = None
93    self.saveTimer       = None
94    self.shutdownTimer   = None
95    self.lastAccess      = time.time()
96    self.saveFilename    = 'RDict.db'
97    self.addrFilename    = 'RDict.loc'
98    self.parentAddr      = parentAddr
99    self.isServer        = 0
100    self.readonly        = readonly
101    self.parentDirectory = parentDirectory
102    self.packer          = xdrlib.Packer()
103    self.unpacker        = xdrlib.Unpacker('')
104    self.stopCmd         = pickle.dumps(('stop',))
105    self.writeLogLine('Greetings')
106    self.connectParent(self.parentAddr, self.parentDirectory)
107    if load: self.load()
108    if autoShutdown and useThreads:
109      atexit.register(self.shutdown)
110    self.writeLogLine('SERVER: Last access '+str(self.lastAccess))
111    return
112
113  def __getstate__(self):
114    '''Remove any parent socket object, the XDR translators, and the log file from the dictionary before pickling'''
115    self.writeLogLine('Pickling RDict')
116    d = self.__dict__.copy()
117    if 'parent'    in d: del d['parent']
118    if 'saveTimer' in d: del d['saveTimer']
119    if '_setCommandLine' in d: del d['_setCommandLine']
120    del d['packer']
121    del d['unpacker']
122    del d['logFile']
123    return d
124
125  def __setstate__(self, d):
126    '''Reconnect the parent socket object, recreate the XDR translators and reopen the log file after unpickling'''
127    self.logFile  = open('RDict.log', 'a')
128    self.writeLogLine('Unpickling RDict')
129    self.__dict__.update(d)
130    import xdrlib
131    self.packer   = xdrlib.Packer()
132    self.unpacker = xdrlib.Unpacker('')
133    self.connectParent(self.parentAddr, self.parentDirectory)
134    return
135
136  def setupLogFile(self, filename = 'RDict.log'):
137    if not self.logFile is None:
138      self.logFile.close()
139    if os.path.isfile(filename) and os.stat(filename).st_size > 10*1024*1024:
140      if os.path.isfile(filename+'.bkp'):
141        os.remove(filename+'.bkp')
142      os.rename(filename, filename+'.bkp')
143      self.logFile = open(filename, 'w')
144    else:
145      self.logFile = open(filename, 'a')
146    return
147
148  def writeLogLine(self, message):
149    '''Writes the message to the log along with the current time'''
150    import time
151    self.logFile.write('('+str(os.getpid())+')('+str(id(self))+')'+message+' ['+time.asctime(time.localtime())+']\n')
152    self.logFile.flush()
153    return
154
155  def __len__(self):
156    '''Returns the length of both the local and parent dictionaries'''
157    length = dict.__len__(self)
158    if not self.parent is None:
159      length = length + self.send()
160    return length
161
162  def getType(self, key):
163    '''Checks for the key locally, and if not found consults the parent. Returns the Arg object or None if not found.'''
164    try:
165      value = dict.__getitem__(self, key)
166      self.writeLogLine('getType: Getting local type for '+key+' '+str(value))
167      return value
168    except KeyError:
169      pass
170    if self.parent:
171      return self.send(key)
172    return None
173
174  def dict_has_key(self, key):
175    """Utility to check whether the key is present in the dictionary without RDict side-effects."""
176    return key in dict(self)
177
178  def __getitem__(self, key):
179    '''Checks for the key locally, and if not found consults the parent. Returns the value of the Arg.
180       - If the value has not been set, the user will be prompted for input'''
181    if self.dict_has_key(key):
182      self.writeLogLine('__getitem__: '+key+' has local type')
183      pass
184    elif not self.parent is None:
185      self.writeLogLine('__getitem__: Checking parent value')
186      if self.send(key, operation = 'has_key'):
187        self.writeLogLine('__getitem__: Parent has value')
188        return self.send(key)
189      else:
190        self.writeLogLine('__getitem__: Checking parent type')
191        arg = self.send(key, operation = 'getType')
192        if not arg:
193          self.writeLogLine('__getitem__: Parent has no type')
194          arg = nargs.Arg(key)
195        try:
196          value = arg.getValue()
197        except AttributeError as e:
198          self.writeLogLine('__getitem__: Parent had invalid entry: '+str(e))
199          arg   = nargs.Arg(key)
200          value = arg.getValue()
201        self.writeLogLine('__getitem__: Setting parent value '+str(value))
202        self.send(key, value, operation = '__setitem__')
203        return value
204    else:
205      self.writeLogLine('__getitem__: Setting local type for '+key)
206      dict.__setitem__(self, key, nargs.Arg(key))
207      #self.save()
208    self.writeLogLine('__getitem__: Setting local value for '+key)
209    return dict.__getitem__(self, key).getValue()
210
211  def setType(self, key, value, forceLocal = 0):
212    '''Checks for the key locally, and if not found consults the parent. Sets the type for this key.
213       - If a value for the key already exists, it is converted to the new type'''
214    if not isinstance(value, nargs.Arg):
215      raise TypeError('An argument type must be a subclass of Arg')
216    value.setKey(key)
217    if forceLocal or self.parent is None or self.dict_has_key(key):
218      if self.dict_has_key(key):
219        v = dict.__getitem__(self, key)
220        if v.isValueSet():
221          try:
222            value.setValue(v.getValue())
223          except TypeError:
224            print(value.__class__.__name__[3:])
225            print('-----------------------------------------------------------------------')
226            print('Warning! Incorrect argument type specified: -'+str(key)+'='+str(v.getValue())+' - expecting type '+value.__class__.__name__[3:]+'.')
227            print('-----------------------------------------------------------------------')
228            pass
229      dict.__setitem__(self, key, value)
230      #self.save()
231    else:
232      return self.send(key, value)
233    return
234
235  def __setitem__(self, key, value):
236    '''Checks for the key locally, and if not found consults the parent. Sets the value of the Arg.'''
237    if not self.dict_has_key(key):
238      if not self.parent is None:
239        return self.send(key, value)
240      else:
241        dict.__setitem__(self, key, nargs.Arg(key))
242    dict.__getitem__(self, key).setValue(value)
243    self.writeLogLine('__setitem__: Set value for '+key+' to '+str(dict.__getitem__(self, key)))
244    #self.save()
245    return
246
247  def __delitem__(self, key):
248    '''Checks for the key locally, and if not found consults the parent. Deletes the Arg completely.'''
249    if self.dict_has_key(key):
250      dict.__delitem__(self, key)
251      #self.save()
252    elif not self.parent is None:
253      self.send(key)
254    return
255
256  def clear(self):
257    '''Clears both the local and parent dictionaries'''
258    if dict.__len__(self):
259      dict.clear(self)
260      #self.save()
261    if not self.parent is None:
262      self.send()
263    return
264
265  def __contains__(self, key):
266    '''Checks for the key locally, and if not found consults the parent. Then checks whether the value has been set'''
267    if self.dict_has_key(key):
268      if dict.__getitem__(self, key).isValueSet():
269        self.writeLogLine('has_key: Have value for '+key)
270      else:
271        self.writeLogLine('has_key: Do not have value for '+key)
272      return dict.__getitem__(self, key).isValueSet()
273    elif not self.parent is None:
274      return self.send(key)
275    return 0
276
277  def get(self, key, default=None):
278    if key in self:
279      return self.__getitem__(key)
280    else:
281      return default
282
283  def hasType(self, key):
284    '''Checks for the key locally, and if not found consults the parent. Then checks whether the type has been set'''
285    if self.dict_has_key(key):
286      return 1
287    elif not self.parent is None:
288      return self.send(key)
289    return 0
290
291  def items(self):
292    '''Return a list of all accessible items, as (key, value) pairs.'''
293    l = dict.items(self)
294    if not self.parent is None:
295      l.extend(self.send())
296    return l
297
298  def localitems(self):
299    '''Return a list of all the items stored locally, as (key, value) pairs.'''
300    return dict.items(self)
301
302  def keys(self):
303    '''Returns the list of keys in both the local and parent dictionaries'''
304    keyList = [key for key in dict.keys(self) if dict.__getitem__(self, key).isValueSet()]
305    if not self.parent is None:
306      keyList.extend(self.send())
307    return keyList
308
309  def types(self):
310    '''Returns the list of keys for which types are defined in both the local and parent dictionaries'''
311    keyList = dict.keys(self)
312    if not self.parent is None:
313      keyList.extend(self.send())
314    return keyList
315
316  def update(self, d):
317    '''Update the dictionary with the contents of d'''
318    for k in d:
319      self[k] = d[k]
320    return
321
322  def updateTypes(self, d):
323    '''Update types locally, which is equivalent to the dict.update() method'''
324    return dict.update(self, d)
325
326  def insertArg(self, key, value, arg):
327    '''Insert a (key, value) pair into the dictionary. If key is None, arg is put into the target list.'''
328    if not key is None:
329      self[key] = value
330    else:
331      if not self.target == ['default']:
332        self.target.append(arg)
333      else:
334        self.target = [arg]
335    return
336
337  def insertArgs(self, args):
338    '''Insert some text arguments into the dictionary (list and dictionaries are recognized)'''
339
340    if isinstance(args, list):
341      for arg in args:
342        (key, value) = nargs.Arg.parseArgument(arg)
343        self.insertArg(key, value, arg)
344    elif hasattr(args, 'keys'):
345      for key in args.keys():
346        if isinstance(args[key], str):
347          value = nargs.Arg.parseValue(args[key])
348        else:
349          value = args[key]
350        self.insertArg(key, value, None)
351    elif isinstance(args, str):
352        (key, value) = nargs.Arg.parseArgument(args)
353        self.insertArg(key, value, args)
354    return
355
356  def hasParent(self):
357    '''Return True if this RDict has a parent dictionary'''
358    return not self.parent is None
359
360  def getServerAddr(self, dir):
361    '''Read the server socket address (in pickled form) from a file, usually RDict.loc
362       - If we fail to connect to the server specified in the file, we spawn it using startServer()'''
363    filename = os.path.join(dir, self.addrFilename)
364    if not os.path.exists(filename):
365      self.startServer(filename)
366    if not os.path.exists(filename):
367      raise RuntimeError('Server address file does not exist: '+filename)
368    try:
369      f    = open(filename, 'r')
370      addr = pickle.load(f)
371      f.close()
372      return addr
373    except Exception as e:
374      self.writeLogLine('CLIENT: Exception during server address determination: '+str(e.__class__)+': '+str(e))
375    raise RuntimeError('Could not get server address in '+filename)
376
377  def writeServerAddr(self, server):
378    '''Write the server socket address (in pickled form) to a file, usually RDict.loc.'''
379    f = open(self.addrFilename, 'w')
380    pickle.dump(server.server_address, f)
381    f.close()
382    self.writeLogLine('SERVER: Wrote lock file '+os.path.abspath(self.addrFilename))
383    return
384
385  def startServer(self, addrFilename):
386    '''Spawn a new RDict server in the parent directory'''
387    import RDict # Need this to locate server script
388    import sys
389    import time
390    import sysconfig
391
392    self.writeLogLine('CLIENT: Spawning a new server with lock file '+os.path.abspath(addrFilename))
393    if os.path.exists(addrFilename):
394      os.remove(addrFilename)
395    oldDir      = os.getcwd()
396    source      = os.path.join(os.path.dirname(os.path.abspath(sys.modules['RDict'].__file__)), 'RDict.py')
397    interpreter = os.path.join(sysconfig.get_config_var('BINDIR'), sysconfig.get_config_var('PYTHON'))
398    if not os.path.isfile(interpreter):
399      interpreter = 'python'
400    os.chdir(os.path.dirname(addrFilename))
401    self.writeLogLine('CLIENT: Executing '+interpreter+' '+source+' server"')
402    try:
403      os.spawnvp(os.P_NOWAIT, interpreter, [interpreter, source, 'server'])
404    except:
405      self.writeLogLine('CLIENT: os.spawnvp failed.\n \
406      This is a typical problem on CYGWIN systems.  If you are using CYGWIN,\n \
407      you can fix this problem by running /bin/rebaseall.  If you do not have\n \
408      this program, you can install it with the CYGWIN installer in the package\n \
409      Rebase, under the category System.  You must run /bin/rebaseall after\n \
410      turning off all cygwin services -- in particular sshd, if any such services\n \
411      are running.  For more information about rebase, go to http://www.cygwin.com')
412      print('\n \
413      This is a typical problem on CYGWIN systems.  If you are using CYGWIN,\n \
414      you can fix this problem by running /bin/rebaseall.  If you do not have\n \
415      this program, you can install it with the CYGWIN installer in the package\n \
416      Rebase, under the category System.  You must run /bin/rebaseall after\n \
417      turning off all cygwin services -- in particular sshd, if any such services\n \
418      are running.  For more information about rebase, go to http://www.cygwin.com\n')
419      raise
420    os.chdir(oldDir)
421    timeout = 1
422    for i in range(10):
423      time.sleep(timeout)
424      timeout *= 2
425      if timeout > 100: timeout = 100
426      if os.path.exists(addrFilename): return
427    self.writeLogLine('CLIENT: Could not start server')
428    return
429
430  def connectParent(self, addr, dir):
431    '''Try to connect to a parent RDict server
432       - If addr and dir are both None, this operation fails
433       - If addr is None, check for an address file in dir'''
434    if addr is None:
435      if dir is None: return 0
436      addr = self.getServerAddr(dir)
437
438    import socket
439    import errno
440    connected = 0
441    s         = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
442    timeout   = 1
443    for i in range(10):
444      try:
445        self.writeLogLine('CLIENT: Trying to connect to '+str(addr))
446        s.connect(addr)
447        connected = 1
448        break
449      except socket.error as e:
450        self.writeLogLine('CLIENT: Failed to connect: '+str(e))
451        if e[0] == errno.ECONNREFUSED:
452          try:
453            import time
454            time.sleep(timeout)
455            timeout *= 2
456            if timeout > 100: timeout = 100
457          except KeyboardInterrupt:
458            break
459          # Try to spawn parent
460          if dir:
461            filename = os.path.join(dir, self.addrFilename)
462            if os.path.isfile(filename):
463              os.remove(filename)
464            self.startServer(filename)
465      except Exception as e:
466        self.writeLogLine('CLIENT: Failed to connect: '+str(e.__class__)+': '+str(e))
467    if not connected:
468      self.writeLogLine('CLIENT: Failed to connect to parent')
469      return 0
470    self.parent = s
471    self.writeLogLine('CLIENT: Connected to '+str(self.parent))
472    return 1
473
474  def sendPacket(self, s, packet, source = 'Unknown', isPickled = 0):
475    '''Pickle the input packet. Send first the size of the pickled string in 32-bit integer, and then the string itself'''
476    self.writeLogLine(source+': Sending packet '+str(packet))
477    if isPickled:
478      p = packet
479    else:
480      p = pickle.dumps(packet)
481    self.packer.reset()
482    self.packer.pack_uint(len(p))
483    if hasattr(s, 'write'):
484      s.write(self.packer.get_buffer())
485      s.write(p)
486    else:
487      s.sendall(self.packer.get_buffer())
488      s.sendall(p)
489    self.writeLogLine(source+': Sent packet')
490    return
491
492  def recvPacket(self, s, source = 'Unknown'):
493    '''Receive first the size of the pickled string in a 32-bit integer, and then the string itself. Return the unpickled object'''
494    self.writeLogLine(source+': Receiving packet')
495    if hasattr(s, 'read'):
496      s.read(4)
497      value = pickle.load(s)
498    else:
499      # I probably need to check that it actually read these 4 bytes
500      self.unpacker.reset(s.recv(4))
501      length    = self.unpacker.unpack_uint()
502      objString = ''
503      while len(objString) < length:
504        objString += s.recv(length - len(objString))
505      value = pickle.loads(objString)
506    self.writeLogLine(source+': Received packet '+str(value))
507    return value
508
509  def send(self, key = None, value = None, operation = None):
510    '''Send a request to the parent'''
511    import inspect
512
513    objString = ''
514    for i in range(3):
515      try:
516        packet = []
517        if operation is None:
518          operation = inspect.stack()[1][3]
519        packet.append(operation)
520        if not key is None:
521          packet.append(key)
522          if not value is None:
523            packet.append(value)
524        self.sendPacket(self.parent, tuple(packet), source = 'CLIENT')
525        response = self.recvPacket(self.parent, source = 'CLIENT')
526        break
527      except IOError as e:
528        self.writeLogLine('CLIENT: IOError '+str(e))
529        if e.errno == 32:
530          self.connectParent(self.parentAddr, self.parentDirectory)
531      except Exception as e:
532        self.writeLogLine('CLIENT: Exception '+str(e)+' '+str(e.__class__))
533    try:
534      if isinstance(response, Exception):
535        self.writeLogLine('CLIENT: Got an exception '+str(response))
536        raise response
537      else:
538        self.writeLogLine('CLIENT: Received value '+str(response)+' '+str(type(response)))
539    except UnboundLocalError:
540      self.writeLogLine('CLIENT: Could not unpickle response')
541      response  = None
542    return response
543
544  def serve(self):
545    '''Start a server'''
546    import socket
547    import SocketServer # novermin
548
549    if not useThreads:
550      raise RuntimeError('Cannot run a server if threads are disabled')
551
552    class ProcessHandler(SocketServer.StreamRequestHandler):
553      def handle(self):
554        import time
555
556        self.server.rdict.lastAccess = time.time()
557        self.server.rdict.writeLogLine('SERVER: Started new handler')
558        while 1:
559          try:
560            value = self.server.rdict.recvPacket(self.rfile, source = 'SERVER')
561          except EOFError as e:
562            self.server.rdict.writeLogLine('SERVER: EOFError receiving packet '+str(e)+' '+str(e.__class__))
563            return
564          except Exception as e:
565            self.server.rdict.writeLogLine('SERVER: Error receiving packet '+str(e)+' '+str(e.__class__))
566            self.server.rdict.sendPacket(self.wfile, e, source = 'SERVER')
567            continue
568          if value[0] == 'stop': break
569          try:
570            response = getattr(self.server.rdict, value[0])(*value[1:])
571          except Exception as e:
572            self.server.rdict.writeLogLine('SERVER: Error executing operation '+str(e)+' '+str(e.__class__))
573            self.server.rdict.sendPacket(self.wfile, e, source = 'SERVER')
574          else:
575            self.server.rdict.sendPacket(self.wfile, response, source = 'SERVER')
576        return
577
578    # check if server is running
579    if os.path.exists(self.addrFilename):
580      rdict     = RDict(parentDirectory = '.')
581      hasParent = rdict.hasParent()
582      del rdict
583      if hasParent:
584        self.writeLogLine('SERVER: Another server is already running')
585        raise RuntimeError('Server already running')
586
587    # Daemonize server
588    self.writeLogLine('SERVER: Daemonizing server')
589    if os.fork(): # Launch child
590      os._exit(0) # Kill off parent, so we are not a process group leader and get a new PID
591    os.setsid()   # Set session ID, so that we have no controlling terminal
592    # We choose to leave cwd at RDict.py: os.chdir('/') # Make sure root directory is not on a mounted drive
593    os.umask(0o77) # Fix creation mask
594    for i in range(3): # Crappy stopgap for closing descriptors
595      try:
596        os.close(i)
597      except OSError as e:
598        if e.errno != errno.EBADF:
599          raise RuntimeError('Could not close default descriptor '+str(i))
600
601    # wish there was a better way to get a usable socket
602    self.writeLogLine('SERVER: Establishing socket server')
603    basePort = 8000
604    flag     = 'nosocket'
605    p        = 1
606    while p < 1000 and flag == 'nosocket':
607      try:
608        server = SocketServer.ThreadingTCPServer((socket.gethostname(), basePort+p), ProcessHandler)
609        flag   = 'socket'
610      except Exception as e:
611        p = p + 1
612    if flag == 'nosocket':
613      p = 1
614      while p < 1000 and flag == 'nosocket':
615        try:
616          server = SocketServer.ThreadingTCPServer(('localhost', basePort+p), ProcessHandler)
617          flag   = 'socket'
618        except Exception as e:
619          p = p + 1
620    if flag == 'nosocket':
621      self.writeLogLine('SERVER: Could not established socket server on port '+str(basePort+p))
622      raise RuntimeError('Cannot get available socket')
623    self.writeLogLine('SERVER: Established socket server on port '+str(basePort+p))
624
625    self.isServer = 1
626    self.writeServerAddr(server)
627    self.serverShutdown(os.getpid())
628
629    server.rdict = self
630    self.writeLogLine('SERVER: Started server')
631    server.serve_forever()
632    return
633
634  def load(self):
635    '''Load the saved dictionary'''
636    if not self.parentDirectory is None and os.path.samefile(os.getcwd(), self.parentDirectory):
637      return
638    self.saveFilename = os.path.abspath(self.saveFilename)
639    if os.path.exists(self.saveFilename):
640      try:
641        dbFile = open(self.saveFilename, 'rb')
642        data   = pickle.load(dbFile)
643        self.updateTypes(data)
644        dbFile.close()
645        self.writeLogLine('Loaded dictionary from '+self.saveFilename)
646      except Exception as e:
647        self.writeLogLine('Problem loading dictionary from '+self.saveFilename+'\n--> '+str(e))
648    else:
649      self.writeLogLine('No dictionary to load in this file: '+self.saveFilename)
650    return
651
652  def save(self, force = 1):
653    '''Save the dictionary after 5 seconds, ignoring all subsequent calls until the save
654       - Giving force = True will cause an immediate save'''
655    if self.readonly: return
656    if force:
657      self.saveTimer = None
658      # This should be a critical section
659      dbFile = open(self.saveFilename, 'wb')
660      data   = dict([i for i in self.localitems() if not i[1].getTemporary()])
661      pickle.dump(data, dbFile)
662      dbFile.close()
663      self.writeLogLine('Saved local dictionary to '+os.path.abspath(self.saveFilename))
664    elif not self.saveTimer:
665      import threading
666      self.saveTimer = threading.Timer(5, self.save, [], {'force': 1})
667      self.saveTimer.setDaemon(1)
668      self.saveTimer.start()
669    return
670
671  def shutdown(self):
672    '''Shutdown the dictionary, writing out changes and notifying parent'''
673    if self.saveTimer:
674      self.saveTimer.cancel()
675      self.save(force = 1)
676    if self.isServer and os.path.isfile(self.addrFilename):
677      os.remove(self.addrFilename)
678    if not self.parent is None:
679      self.sendPacket(self.parent, self.stopCmd, isPickled = 1)
680      self.parent.close()
681      self.parent = None
682    self.writeLogLine('Shutting down')
683    self.logFile.close()
684    return
685
686  def serverShutdown(self, pid, delay = shutdownDelay):
687    if self.shutdownTimer is None:
688      import threading
689
690      self.shutdownTimer = threading.Timer(delay, self.serverShutdown, [pid], {'delay': 0})
691      self.shutdownTimer.setDaemon(1)
692      self.shutdownTimer.start()
693      self.writeLogLine('SERVER: Set shutdown timer for process '+str(pid)+' at '+str(delay)+' seconds')
694    else:
695      try:
696        import signal
697        import time
698
699        idleTime = time.time() - self.lastAccess
700        self.writeLogLine('SERVER: Last access '+str(self.lastAccess))
701        self.writeLogLine('SERVER: Idle time '+str(idleTime))
702        if idleTime < RDict.shutdownDelay:
703          self.writeLogLine('SERVER: Extending shutdown timer for '+str(pid)+' by '+str(RDict.shutdownDelay - idleTime)+' seconds')
704          self.shutdownTimer = None
705          self.serverShutdown(pid, RDict.shutdownDelay - idleTime)
706        else:
707          self.writeLogLine('SERVER: Killing server '+str(pid))
708          os.kill(pid, signal.SIGTERM)
709      except Exception as e:
710        self.writeLogLine('SERVER: Exception killing server: '+str(e))
711    return
712
713if __name__ ==  '__main__':
714  import sys
715  try:
716    if len(sys.argv) < 2:
717      print('RDict.py [server | client | clear | insert | remove] [parent]')
718    else:
719      action = sys.argv[1]
720      parent = None
721      if len(sys.argv) > 2:
722        if not sys.argv[2] == 'None': parent = sys.argv[2]
723      if action == 'server':
724        RDict(parentDirectory = parent).serve()
725      elif action == 'client':
726        print('Entries in server dictionary')
727        rdict = RDict(parentDirectory = parent)
728        for key in rdict.types():
729          if not key.startswith('cacheKey') and not key.startswith('stamp-'):
730            print(str(key)+' '+str(rdict.getType(key)))
731      elif action == 'cacheClient':
732        print('Cache entries in server dictionary')
733        rdict = RDict(parentDirectory = parent)
734        for key in rdict.types():
735          if key.startswith('cacheKey'):
736            print(str(key)+' '+str(rdict.getType(key)))
737      elif action == 'stampClient':
738        print('Stamp entries in server dictionary')
739        rdict = RDict(parentDirectory = parent)
740        for key in rdict.types():
741          if key.startswith('stamp-'):
742            print(str(key)+' '+str(rdict.getType(key)))
743      elif action == 'clear':
744        print('Clearing all dictionaries')
745        RDict(parentDirectory = parent).clear()
746      elif action == 'insert':
747        rdict = RDict(parentDirectory = parent)
748        rdict[sys.argv[3]] = sys.argv[4]
749      elif action == 'remove':
750        rdict = RDict(parentDirectory = parent)
751        del rdict[sys.argv[3]]
752      else:
753        sys.exit('Unknown action: '+action)
754  except Exception as e:
755    import traceback
756    print(traceback.print_tb(sys.exc_info()[2]))
757    sys.exit(str(e))
758  sys.exit(0)
759