runcached.py 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. # runcached
  4. # Execute commands while caching their output for subsequent calls.
  5. # Command output will be cached for $cacheperiod and replayed for subsequent calls
  6. #
  7. # Author Spiros Ioannou sivann <at> inaccess.com
  8. #
  9. cacheperiod=27 #seconds
  10. cachedir="/tmp"
  11. maxwaitprev=5 #seconds to wait for previous same command to finish before quiting
  12. minrand=0 #random seconds to wait before running cmd
  13. maxrand=0
  14. import os
  15. import sys
  16. import errno
  17. import subprocess
  18. import time
  19. import hashlib
  20. import random
  21. import atexit
  22. import syslog
  23. argskip=1
  24. if (len(sys.argv) < 2) or (len(sys.argv) == 2 and sys.argv[1] == '-c'):
  25. sys.exit('Usage: %s [-c cacheperiod] <command to execute with args>' % sys.argv[0])
  26. if sys.argv[1] == '-c':
  27. cacheperiod=int(sys.argv[2])
  28. argskip=3
  29. cmd=" ".join(sys.argv[argskip:])
  30. #hash of executed command w/args
  31. m = hashlib.md5()
  32. m.update(cmd)
  33. cmdmd5=m.hexdigest()
  34. #random sleep to avoid racing condition of creating the pid on the same time
  35. if maxrand-minrand > 0:
  36. time.sleep(random.randrange(minrand,maxrand))
  37. pidfile=cachedir+os.sep+cmdmd5+'-runcached.pid'
  38. cmddatafile=cachedir+os.sep+cmdmd5+'.data'
  39. cmdexitcode=cachedir+os.sep+cmdmd5+'.exitcode'
  40. cmdfile=cachedir+os.sep+cmdmd5+'.cmd'
  41. def cleanup():
  42. if os.path.isfile(pidfile):
  43. os.remove(pidfile)
  44. def myexcepthook(exctype, value, traceback):
  45. #if exctype == KeyboardInterrupt:
  46. # print "Handler code goes here"
  47. cleanup()
  48. syslog.syslog(syslog.LOG_ERR, value.__str__())
  49. _old_excepthook(exctype, value, traceback)
  50. def runit(cmd,cmddatafile,cmdexitcode,cmdfile):
  51. f_stdout=open(cmddatafile, 'w')
  52. #f_stderr=open('stderr.txt', 'w')
  53. f_stderr=f_stdout
  54. p = subprocess.Popen(cmd, stdout=f_stdout, stderr=f_stderr,shell=True)
  55. p.communicate()
  56. f_stdout.close()
  57. exitcode=p.returncode
  58. with open(cmdfile, 'w') as f:
  59. f.write(cmd)
  60. with open(cmdexitcode, 'w') as f:
  61. f.write(str(exitcode))
  62. def file_get_contents(filename):
  63. with open(filename) as f:
  64. return f.read()
  65. #install cleanup hook
  66. _old_excepthook = sys.excepthook
  67. sys.excepthook = myexcepthook
  68. atexit.register(cleanup)
  69. #don't run the same command in parallel, wait the previous one to finish
  70. count=maxwaitprev
  71. while os.path.isfile(pidfile):
  72. #remove stale pid if left there without a running process
  73. prevpid=file_get_contents(pidfile).strip()
  74. if not os.path.exists("/proc/"+prevpid):
  75. os.remove(pidfile)
  76. time.sleep(1)
  77. count-=1
  78. if count == 0:
  79. sys.stderr.write("timeout waiting for '%s' to finish. (pid: %s)\n" % (cmd,pidfile))
  80. sys.exit (1)
  81. #write pidfile
  82. mypid=os.getpid()
  83. with open(pidfile, 'w') as f:
  84. f.write(str(mypid))
  85. #if not cached before, run it
  86. if not os.path.isfile(cmddatafile):
  87. runit(cmd,cmddatafile,cmdexitcode,cmdfile)
  88. #if too old, re-run it
  89. currtime=int(time.time())
  90. lastrun=int(os.path.getmtime(cmddatafile))
  91. diffsec=currtime - lastrun
  92. if (diffsec > cacheperiod):
  93. runit(cmd,cmddatafile,cmdexitcode,cmdfile)
  94. with open(cmddatafile, 'r') as f:
  95. sys.stdout.write(f.read())
  96. cleanup()