Friday, 21 September 2012

Process Watchman

Update: If the code it is running completes before the maximum time, it will block the program from completing until the time runs out. Still needs work! (Read: It doesn't work XD)

I was looking for a process monitor, sort of like monit, or god but these two solutions are for keeping a process alive or restarting a process if it is misbehaving. What I wanted was a simple process watcher that would simply kill a process if it misbehaves or runs out of time, while returning the completed or partial output of the process it was watching.

For this I quickly wrote a small tool to do this on *nix machines.
It is quite hacked together so I must apologise for the mess, it is in its first version here and the program was written as fast as I could imagine without prior design. There will be numerable ways of improving this tool.

Currently it accepts 3 args, a command, an integer value for seconds and a float value for ram megabytes. There is probably a problem with commands that are more than one word long which is most of them, experimentation and improvements are needed.

  1. #Author: Greg Myers
  2. #Date: 30/08/12
  3. @command = ARGV[0]
  4. @time_limit = ARGV[1].to_i #seconds
  5. @ram_limit = ARGV[2].to_f #megabytes
  6.  
  7. def watch_time(sec)
  8. exception = Class.new(Interrupt)
  9. begin
  10. x = Thread.current #x will contain whatever runs in yield
  11. y = Thread.start { #y watches and causes x to throw when timeout.
  12. begin
  13. sleep(sec)
  14. rescue => e #This raises any error x naturally hits
  15. x.raise e
  16. else #This executes if no exceptions happened until now
  17. x.raise "Process ran out of time to execute."
  18. end
  19. }
  20. return yield(sec)
  21. ensure
  22. if y
  23. y.kill
  24. y.join
  25. end
  26. end
  27. end
  28.  
  29. def watch_ram(megabytes, pid)
  30. exception = Class.new(Interrupt)
  31. begin
  32. x = Thread.current #x will contain whatever runs in yield
  33. y = Thread.start { #y watches and causes x to throw when timeout.
  34. begin
  35. loop {
  36. rss_use = `ps -o rss= -p #{pid}`.to_i #use ps to get rss of pid
  37. raise "Hit Ram Limit #{megabytes*1024}kb, with #{rss_use}kb" if megabytes*1024 < rss_use
  38. sleep(0.5)
  39. }
  40. rescue => e #This raises any error x naturally hits
  41. x.raise e
  42. end
  43. }
  44. return yield
  45. ensure
  46. if y
  47. y.kill
  48. y.join
  49. end
  50. end
  51. end
  52.  
  53. def get_payload_child_pid(pid)
  54. pipe = IO.popen("ps -ef | grep #{pid}")
  55. pipe.readlines[2] =~ /\w+\s+(?\d+)\s+(?\d+)/ #Always line 3, Line 1 = spawn cmd, Line 2 = IO.popen, Line 3 = ruby, Line 4 = grep
  56. pipe.close
  57. return $~[:child_pid]
  58. end
  59. if @command
  60. if @time_limit > 1
  61. if @ram_limit > 0
  62. watch_time(@time_limit){
  63. require 'pty'
  64. PTY.spawn("#{@command} 2>&1") do |r,w,p|
  65. child_pid = get_payload_child_pid(p)
  66. watch_ram(@ram_limit, child_pid){ loop { puts r.gets } }
  67. end
  68. }
  69. else
  70. puts "Invalid memory limit #{ARGV[2]}"
  71. end
  72. else
  73. puts "Invalid time limit #{ARGV[1]}"
  74. end
  75. else
  76. puts "Invalid command #{ARGV[0]}"
  77. end
I will be putting this on github publicly shortly.

No comments:

Post a Comment