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