chore 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/bin/chore-client +25 -0
- data/bin/chore-server +27 -0
- data/bin/chore-status +28 -0
- data/lib/chore.rb +110 -0
- data/lib/chore/constants.rb +7 -0
- data/lib/chore/server.rb +101 -0
- data/lib/chore/store.rb +137 -0
- data/lib/chore/time_help.rb +26 -0
- metadata +106 -0
    
        data/bin/chore-client
    ADDED
    
    | @@ -0,0 +1,25 @@ | |
| 1 | 
            +
            #!/usr/bin/env ruby
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'chore'
         | 
| 4 | 
            +
            require 'chore/constants'
         | 
| 5 | 
            +
            require 'trollop'
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            opts = Trollop::options do
         | 
| 8 | 
            +
              banner "chore-client"
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              opt :host, "host name of chore-server", :default => 'localhost'
         | 
| 11 | 
            +
              opt :port, "port of chore-server", :default => Chore::Constants::DEFAULT_LISTEN_PORT
         | 
| 12 | 
            +
             | 
| 13 | 
            +
              opt :chore, "name of chore", :type => :string
         | 
| 14 | 
            +
              opt :action, "action (start, finish, status, fail, etc)", :type => :string
         | 
| 15 | 
            +
            end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            Trollop::die :chore, "chore name required" if !opts[:chore]
         | 
| 18 | 
            +
            Trollop::die :action, "action required" if !opts[:action]
         | 
| 19 | 
            +
             | 
| 20 | 
            +
            server = opts[:host]
         | 
| 21 | 
            +
            port = opts[:port]
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            Chore.set_server(server, port)
         | 
| 24 | 
            +
             | 
| 25 | 
            +
            Chore.public_method(opts[:action]).call(opts[:chore])
         | 
    
        data/bin/chore-server
    ADDED
    
    | @@ -0,0 +1,27 @@ | |
| 1 | 
            +
            #!/usr/bin/env ruby
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'chore'
         | 
| 4 | 
            +
            require 'chore/server'
         | 
| 5 | 
            +
            require 'chore/constants'
         | 
| 6 | 
            +
            require 'trollop'
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            opts = Trollop::options do
         | 
| 9 | 
            +
              banner "chore-server"
         | 
| 10 | 
            +
             | 
| 11 | 
            +
              opt :listen_port, "Port to listen for client status submissions", :default => Chore::Constants::DEFAULT_LISTEN_PORT
         | 
| 12 | 
            +
              opt :cli_port, "Port for chore-status requests", :default => Chore::Constants::DEFAULT_CLI_PORT
         | 
| 13 | 
            +
              opt :web_port, "Port for web status server", :default => Chore::Constants::DEFAULT_WEB_PORT
         | 
| 14 | 
            +
            end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
             | 
| 17 | 
            +
            # TODO: Real command line options instead of this
         | 
| 18 | 
            +
            listen_port = opts[:listen_port]
         | 
| 19 | 
            +
            cli_port = opts[:cli_port]
         | 
| 20 | 
            +
            web_port = opts[:web_port]
         | 
| 21 | 
            +
             | 
| 22 | 
            +
            EventMachine::run do
         | 
| 23 | 
            +
              EventMachine::PeriodicTimer.new(60) { Chore::Store.expire }
         | 
| 24 | 
            +
              EventMachine::open_datagram_socket('0.0.0.0', listen_port, ChoreCollect)
         | 
| 25 | 
            +
              EventMachine::start_server('0.0.0.0', cli_port, ChoreDisplay)
         | 
| 26 | 
            +
              EventMachine::start_server('0.0.0.0', web_port, ChoreWeb)
         | 
| 27 | 
            +
            end
         | 
    
        data/bin/chore-status
    ADDED
    
    | @@ -0,0 +1,28 @@ | |
| 1 | 
            +
            #!/usr/bin/env ruby
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'socket'
         | 
| 4 | 
            +
            require 'chore/constants'
         | 
| 5 | 
            +
            require 'trollop'
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            opts = Trollop::options do
         | 
| 8 | 
            +
              banner "chore-status"
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              opt :host, "host name of chore-server", :default => 'localhost'
         | 
| 11 | 
            +
              opt :port, "port of chore-server", :default => Chore::Constants::DEFAULT_CLI_PORT
         | 
| 12 | 
            +
            end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            server = opts[:host]
         | 
| 15 | 
            +
            port = opts[:port]
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            begin
         | 
| 18 | 
            +
              sock = TCPSocket.open(server,port)
         | 
| 19 | 
            +
              sock.puts ".\r\n"
         | 
| 20 | 
            +
             | 
| 21 | 
            +
              while next_line = sock.gets
         | 
| 22 | 
            +
                puts next_line
         | 
| 23 | 
            +
              end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
              sock.close
         | 
| 26 | 
            +
            rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT => ex
         | 
| 27 | 
            +
              puts "Couldn't connect to chore-server at #{server}:#{port}"
         | 
| 28 | 
            +
            end
         | 
    
        data/lib/chore.rb
    ADDED
    
    | @@ -0,0 +1,110 @@ | |
| 1 | 
            +
            require 'socket'
         | 
| 2 | 
            +
            require 'json'
         | 
| 3 | 
            +
            require 'chore/constants'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            # Client module to access the server.  Basic usage is something like:
         | 
| 6 | 
            +
            # 
         | 
| 7 | 
            +
            #    Chore.monitor('task') do
         | 
| 8 | 
            +
            #      # ...
         | 
| 9 | 
            +
            #    end
         | 
| 10 | 
            +
            # 
         | 
| 11 | 
            +
            # Refer to the various methods for additional options
         | 
| 12 | 
            +
            module Chore
         | 
| 13 | 
            +
             | 
| 14 | 
            +
              # Override the default server settings
         | 
| 15 | 
            +
              def self.set_server ip, port
         | 
| 16 | 
            +
                @@server_ip = ip
         | 
| 17 | 
            +
                @@server_port = port
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
              
         | 
| 20 | 
            +
              # Let the server know that you've started a task.
         | 
| 21 | 
            +
              # Options you can include are:
         | 
| 22 | 
            +
              #
         | 
| 23 | 
            +
              # [:do_every]     Indicate that the task should run every X seconds.
         | 
| 24 | 
            +
              #                 If this does not happen, show task status in RED.
         | 
| 25 | 
            +
              #
         | 
| 26 | 
            +
              # [:grace_period] Allow a grace period for the above option.  If we
         | 
| 27 | 
            +
              #                 are late but withing the grace period, show task
         | 
| 28 | 
            +
              #                 status in YELLOW.
         | 
| 29 | 
            +
              #
         | 
| 30 | 
            +
              # [:finish_in]    Indicate that the task should finish in X seconds.
         | 
| 31 | 
            +
              #                 If we haven't received a finish message by then,
         | 
| 32 | 
            +
              #                 show the task in RED.
         | 
| 33 | 
            +
              #
         | 
| 34 | 
            +
              # [:expire_in]    Remove the task after X seconds.  This may be useful
         | 
| 35 | 
            +
              #                 to keep the task list clean.
         | 
| 36 | 
            +
              def self.start task, opts={}
         | 
| 37 | 
            +
                opts[:start_time] ||= Time.now().to_i
         | 
| 38 | 
            +
                send( [:start, task, opts] )
         | 
| 39 | 
            +
              end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
              # Provide an optional status message that can be updated.
         | 
| 42 | 
            +
              # Only the last status message is retained.
         | 
| 43 | 
            +
              def self.status task, message
         | 
| 44 | 
            +
                send( [:status_update, task, { :status_note => message}] )
         | 
| 45 | 
            +
              end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
              # Manually indicate that a task has finished
         | 
| 48 | 
            +
              def self.finish task, opts={}
         | 
| 49 | 
            +
                opts[:finish_time] ||= Time.now().to_i
         | 
| 50 | 
            +
                send( [:finish, task, opts] )
         | 
| 51 | 
            +
              end
         | 
| 52 | 
            +
              
         | 
| 53 | 
            +
              # Remove a task from monitoring.
         | 
| 54 | 
            +
              def self.pop task, opts={}
         | 
| 55 | 
            +
                send( [:pop, task, opts] )
         | 
| 56 | 
            +
              end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
              # Manually indicate that a task has failed.
         | 
| 59 | 
            +
              #
         | 
| 60 | 
            +
              # [:error] optional error message
         | 
| 61 | 
            +
              def self.fail task, opts={}
         | 
| 62 | 
            +
                opts[:fail_time] ||= Time.now().to_i
         | 
| 63 | 
            +
                send( [:fail, task, opts] )
         | 
| 64 | 
            +
              end
         | 
| 65 | 
            +
              
         | 
| 66 | 
            +
              # Automatically run Chore.start, execute a code block, and
         | 
| 67 | 
            +
              # automatically run Chore.finish (or Chore.fail in the case
         | 
| 68 | 
            +
              # of an exception) when the block finishes.
         | 
| 69 | 
            +
              #
         | 
| 70 | 
            +
              # All options from .start, .finish, and .fail may be passed
         | 
| 71 | 
            +
              # in as options.
         | 
| 72 | 
            +
              #
         | 
| 73 | 
            +
              # In addition to normal opts, :pop => true
         | 
| 74 | 
            +
              # will automatically remove the task from the store
         | 
| 75 | 
            +
              def self.monitor task, opts={}, &code
         | 
| 76 | 
            +
                pop = false
         | 
| 77 | 
            +
                if opts[:pop]
         | 
| 78 | 
            +
                  pop = true
         | 
| 79 | 
            +
                  opts.delete(:pop)
         | 
| 80 | 
            +
                end
         | 
| 81 | 
            +
                
         | 
| 82 | 
            +
                Chore.start(task, opts)
         | 
| 83 | 
            +
                begin
         | 
| 84 | 
            +
                  code.call()
         | 
| 85 | 
            +
                  if pop
         | 
| 86 | 
            +
                    Chore.pop(task)
         | 
| 87 | 
            +
                  else
         | 
| 88 | 
            +
                    Chore.finish(task)
         | 
| 89 | 
            +
                  end
         | 
| 90 | 
            +
                rescue Exception => ex
         | 
| 91 | 
            +
                  Chore.fail(task, :error => "#{ex.class} - #{ex.message}")
         | 
| 92 | 
            +
                  raise
         | 
| 93 | 
            +
                end
         | 
| 94 | 
            +
              end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
            private
         | 
| 97 | 
            +
              
         | 
| 98 | 
            +
              @@server_ip = '127.0.0.1'
         | 
| 99 | 
            +
              @@server_port = Chore::Constants::DEFAULT_LISTEN_PORT
         | 
| 100 | 
            +
             | 
| 101 | 
            +
              def self.send msg
         | 
| 102 | 
            +
                UDPSocket.new.send(sanitize(msg).to_s, 0, @@server_ip, @@server_port)
         | 
| 103 | 
            +
                nil
         | 
| 104 | 
            +
              end
         | 
| 105 | 
            +
             | 
| 106 | 
            +
              #only allow good options
         | 
| 107 | 
            +
              def self.sanitize msg
         | 
| 108 | 
            +
                msg.to_json
         | 
| 109 | 
            +
              end
         | 
| 110 | 
            +
            end
         | 
    
        data/lib/chore/server.rb
    ADDED
    
    | @@ -0,0 +1,101 @@ | |
| 1 | 
            +
            require 'eventmachine'
         | 
| 2 | 
            +
            require 'evma_httpserver'
         | 
| 3 | 
            +
            require 'chore/store'
         | 
| 4 | 
            +
            require 'json'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            # Process submissions from a client and save it in the store.
         | 
| 7 | 
            +
            module ChoreCollect
         | 
| 8 | 
            +
              @@data_collector = EM.spawn do |chore_info|
         | 
| 9 | 
            +
                Chore::Store.update_chore(chore_info)
         | 
| 10 | 
            +
              end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
              # Sends data to the data_collector spawned process to add
         | 
| 13 | 
            +
              # to the data store.
         | 
| 14 | 
            +
              def chore_collect chore_info
         | 
| 15 | 
            +
                @@data_collector.notify chore_info
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
              def receive_data(data)
         | 
| 19 | 
            +
                chore_info = JSON.parse(data)
         | 
| 20 | 
            +
                chore_collect chore_info
         | 
| 21 | 
            +
              end
         | 
| 22 | 
            +
            end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
            # Provide colorized text output for the CLI interface.
         | 
| 25 | 
            +
            module ChoreDisplay
         | 
| 26 | 
            +
                def colorize str, color
         | 
| 27 | 
            +
                  color_code = case color
         | 
| 28 | 
            +
                               when :red then 31
         | 
| 29 | 
            +
                               when :green then 32
         | 
| 30 | 
            +
                               when :yellow then 33
         | 
| 31 | 
            +
                               else raise "BAD COLOR #{str} #{color}"
         | 
| 32 | 
            +
                               end
         | 
| 33 | 
            +
                  "\033[#{color_code}m#{str}\033[0m"
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                def text_statuses
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  status_lines = []
         | 
| 39 | 
            +
                  Chore::Store.iterate_statuses do |status|
         | 
| 40 | 
            +
                    status_line = "#{status[:job]} - #{status[:status]}ed #{Time.at(status[:start_time])}"
         | 
| 41 | 
            +
                    status_line += " (#{status[:notes].join(', ')})" if !status[:notes].empty?
         | 
| 42 | 
            +
                    status_lines << colorize(status_line, status[:state])
         | 
| 43 | 
            +
                  end
         | 
| 44 | 
            +
                  
         | 
| 45 | 
            +
                  status_lines.join("\n") + "\n"
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
                
         | 
| 48 | 
            +
             | 
| 49 | 
            +
              def receive_data(data)
         | 
| 50 | 
            +
                send_data(text_statuses)
         | 
| 51 | 
            +
                close_connection_after_writing
         | 
| 52 | 
            +
              end
         | 
| 53 | 
            +
            end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
            # A basic webserver that provides a single web page with chore
         | 
| 56 | 
            +
            # statues
         | 
| 57 | 
            +
            class ChoreWeb < EventMachine::Connection
         | 
| 58 | 
            +
              include EventMachine::HttpServer
         | 
| 59 | 
            +
             | 
| 60 | 
            +
              def process_http_request
         | 
| 61 | 
            +
                resp = EventMachine::DelegatedHttpResponse.new(self)
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                html = <<-html
         | 
| 64 | 
            +
            <html>
         | 
| 65 | 
            +
            <head>
         | 
| 66 | 
            +
            <style type="text/css">
         | 
| 67 | 
            +
            body {font-family:monospace;background-color:#CCCCCC;}
         | 
| 68 | 
            +
            .red {color:red;}
         | 
| 69 | 
            +
            .yellow {color:yellow;}
         | 
| 70 | 
            +
            .green {color:green;}
         | 
| 71 | 
            +
            table, th, td { border: 1px solid black;}
         | 
| 72 | 
            +
            </style>
         | 
| 73 | 
            +
            </head>
         | 
| 74 | 
            +
            <body>
         | 
| 75 | 
            +
            <h1>Chores</h1>
         | 
| 76 | 
            +
            <table>
         | 
| 77 | 
            +
            <tr><th>Job</th><th>Status</th><th>Time</th><th>Notes</th></tr>
         | 
| 78 | 
            +
            html
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                Chore::Store.iterate_statuses do |status|
         | 
| 81 | 
            +
                  row = "<tr class='#{status[:state]}'><td>#{status[:job]}</td><td>#{status[:status]}ed</td><td>#{Time.at(status[:start_time])}</td>"
         | 
| 82 | 
            +
                  if !status[:notes].empty?
         | 
| 83 | 
            +
                    row += "<td>(#{status[:notes].join(', ')})</td>"
         | 
| 84 | 
            +
                  else
         | 
| 85 | 
            +
                    row += "<td> </td>"
         | 
| 86 | 
            +
                  end
         | 
| 87 | 
            +
                  
         | 
| 88 | 
            +
                  row += "</tr>\n"
         | 
| 89 | 
            +
                  html << row 
         | 
| 90 | 
            +
                end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                html << "</body></html>"
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                resp.status = 200
         | 
| 95 | 
            +
                resp.content = html
         | 
| 96 | 
            +
                resp.send_response
         | 
| 97 | 
            +
              end
         | 
| 98 | 
            +
              
         | 
| 99 | 
            +
            end
         | 
| 100 | 
            +
             | 
| 101 | 
            +
             | 
    
        data/lib/chore/store.rb
    ADDED
    
    | @@ -0,0 +1,137 @@ | |
| 1 | 
            +
            require 'eventmachine'
         | 
| 2 | 
            +
            require 'chore/time_help'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module Chore
         | 
| 5 | 
            +
              # A semi-persistant store for all of our chore data.  Right now
         | 
| 6 | 
            +
              # it's just a hash that won't survive a server restart.
         | 
| 7 | 
            +
              module Store
         | 
| 8 | 
            +
                #
         | 
| 9 | 
            +
                # Process data with a spawned process in the background 
         | 
| 10 | 
            +
                #
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                def self.update_chore chore_info
         | 
| 13 | 
            +
                  state = chore_info[0]
         | 
| 14 | 
            +
                  chore = chore_info[1]
         | 
| 15 | 
            +
                  opts = chore_info[2]
         | 
| 16 | 
            +
                  opts['status'] = state
         | 
| 17 | 
            +
                  
         | 
| 18 | 
            +
                  if state == "pop"
         | 
| 19 | 
            +
                    Store.get.delete(chore)
         | 
| 20 | 
            +
                  else
         | 
| 21 | 
            +
                    if Store.get[chore].nil?
         | 
| 22 | 
            +
                      Store.get[chore] = {}
         | 
| 23 | 
            +
                    end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                    Store.get[chore] = Store.get[chore].merge(opts)
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                # Remove anything that's currently expired from the store.
         | 
| 30 | 
            +
                def self.expire
         | 
| 31 | 
            +
                  expired_tasks = []
         | 
| 32 | 
            +
                  
         | 
| 33 | 
            +
                  Chore::Store.get.each_pair do |task, params|
         | 
| 34 | 
            +
                    if params['expire_in']
         | 
| 35 | 
            +
                      start_time = params['start_time'].to_i
         | 
| 36 | 
            +
                      expire_in = params['expire_in'].to_i
         | 
| 37 | 
            +
                      expire_time = start_time + expire_in
         | 
| 38 | 
            +
                      
         | 
| 39 | 
            +
                      if expire_time < Time.now().to_i
         | 
| 40 | 
            +
                        expired_tasks << task
         | 
| 41 | 
            +
                      end
         | 
| 42 | 
            +
                    end
         | 
| 43 | 
            +
                  end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                  expired_tasks.each do |task|
         | 
| 46 | 
            +
                    Chore::Store.get.delete(task)
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
                end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                # get status of a single chore
         | 
| 51 | 
            +
                def self.get_chore chore_name
         | 
| 52 | 
            +
                  chore_name = chore_name.to_s
         | 
| 53 | 
            +
                  chore_data = Store.get[chore_name]
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  return nil if chore_data.nil?
         | 
| 56 | 
            +
                  
         | 
| 57 | 
            +
                  build_status(chore_name, chore_data)
         | 
| 58 | 
            +
                end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                # Climb through the internal store and return a processed and
         | 
| 61 | 
            +
                # abstracted list of tasks to the consumer.
         | 
| 62 | 
            +
                def self.iterate_statuses
         | 
| 63 | 
            +
                  ret = []
         | 
| 64 | 
            +
                  Store.get.keys.each do |chore_name|
         | 
| 65 | 
            +
                    yield get_chore(chore_name)
         | 
| 66 | 
            +
                  end
         | 
| 67 | 
            +
                end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
              private
         | 
| 70 | 
            +
                @@store = {}
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                def self.get
         | 
| 73 | 
            +
                  @@store
         | 
| 74 | 
            +
                end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                def self.build_status chore_name, status_info
         | 
| 77 | 
            +
                    status = status_info['status'].to_sym
         | 
| 78 | 
            +
                    run_time = status_info['start_time']
         | 
| 79 | 
            +
                    run_time = 0 if !run_time
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                    current_time = Time.now.to_i
         | 
| 82 | 
            +
                    do_every = status_info['do_every']
         | 
| 83 | 
            +
                    grace_period = status_info['grace_period']
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                    notes = []
         | 
| 86 | 
            +
                    state = :red
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                    if status == :fail
         | 
| 89 | 
            +
                      state = :red
         | 
| 90 | 
            +
                      if status_info['error']
         | 
| 91 | 
            +
                        notes << status_info['error']
         | 
| 92 | 
            +
                      else
         | 
| 93 | 
            +
                        notes << "FAILED!!!"
         | 
| 94 | 
            +
                      end
         | 
| 95 | 
            +
                      
         | 
| 96 | 
            +
                    elsif status == :finish
         | 
| 97 | 
            +
                      finish_time = status_info['finish_time']
         | 
| 98 | 
            +
                      finish_in = status_info['finish_in']
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                      if finish_in.nil?
         | 
| 101 | 
            +
                        state = :green
         | 
| 102 | 
            +
                      elsif (run_time + finish_in) >= finish_time
         | 
| 103 | 
            +
                        state = :green
         | 
| 104 | 
            +
                      else
         | 
| 105 | 
            +
                        state = :red
         | 
| 106 | 
            +
                        notes << "Finished, but #{finish_time - (run_time + finish_in)} seconds late!!!"
         | 
| 107 | 
            +
                      end
         | 
| 108 | 
            +
                    elsif status == :start || status == :status_update
         | 
| 109 | 
            +
                      if do_every
         | 
| 110 | 
            +
                        if run_time + do_every >= current_time
         | 
| 111 | 
            +
                          state = :green
         | 
| 112 | 
            +
                          notes << "Should run every #{Chore::TimeHelp.elapsed_human_time(do_every)}"
         | 
| 113 | 
            +
                        elsif grace_period && run_time + do_every + grace_period > current_time
         | 
| 114 | 
            +
                          state = :yellow
         | 
| 115 | 
            +
                          notes << "Job should run every #{Chore::TimeHelp.elapsed_human_time(do_every)}, but has a grace period of #{Chore::TimeHelp.elapsed_human_time(grace_period)}"
         | 
| 116 | 
            +
                        else
         | 
| 117 | 
            +
                          state = :red
         | 
| 118 | 
            +
                          notes << "Job should run every #{Chore::TimeHelp.elapsed_human_time(do_every)}, but hasn't run since #{Time.at(run_time)}"
         | 
| 119 | 
            +
                        end
         | 
| 120 | 
            +
                      else
         | 
| 121 | 
            +
                        state = :green
         | 
| 122 | 
            +
                      end
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                      if status_info['expire_in']
         | 
| 125 | 
            +
                        expire_in = Time.at(status_info['start_time'] + status_info['expire_in'].to_i)
         | 
| 126 | 
            +
                        notes << "Will expire in #{expire_in}"
         | 
| 127 | 
            +
                      end
         | 
| 128 | 
            +
             | 
| 129 | 
            +
                      notes << "Status: #{status_info['status_note']}" if status_info['status_note']
         | 
| 130 | 
            +
                    end
         | 
| 131 | 
            +
                    
         | 
| 132 | 
            +
                    info = {:job => chore_name, :state => state, :status => status, :start_time => run_time, :notes => notes}
         | 
| 133 | 
            +
                end
         | 
| 134 | 
            +
             | 
| 135 | 
            +
             | 
| 136 | 
            +
              end
         | 
| 137 | 
            +
            end
         | 
| @@ -0,0 +1,26 @@ | |
| 1 | 
            +
            module Chore
         | 
| 2 | 
            +
              module TimeHelp
         | 
| 3 | 
            +
                # Show stuff like "7 weeks, 3 days, 4 hours" instead of 
         | 
| 4 | 
            +
                # 13252363477 seconds since epoch
         | 
| 5 | 
            +
                def self.elapsed_human_time seconds
         | 
| 6 | 
            +
                  remaining_ticks = seconds
         | 
| 7 | 
            +
                  human_text = ""
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                  [[60,:seconds],[60,:minutes],[24,:hours],[7, :days]].each do |ticks, unit|
         | 
| 10 | 
            +
                    above = remaining_ticks / ticks
         | 
| 11 | 
            +
                    below = remaining_ticks % ticks
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                    if below != 0
         | 
| 14 | 
            +
                      unit = unit[0..-2] if below == 1
         | 
| 15 | 
            +
                      human_text = "#{below} #{unit} " + human_text
         | 
| 16 | 
            +
                    end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                    remaining_ticks = above
         | 
| 19 | 
            +
                    if above == 0
         | 
| 20 | 
            +
                      break
         | 
| 21 | 
            +
                      end
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
                  human_text.strip
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
              end
         | 
| 26 | 
            +
            end
         | 
    
        metadata
    ADDED
    
    | @@ -0,0 +1,106 @@ | |
| 1 | 
            +
            --- !ruby/object:Gem::Specification 
         | 
| 2 | 
            +
            name: chore
         | 
| 3 | 
            +
            version: !ruby/object:Gem::Version 
         | 
| 4 | 
            +
              prerelease: 
         | 
| 5 | 
            +
              version: 0.2.0
         | 
| 6 | 
            +
            platform: ruby
         | 
| 7 | 
            +
            authors: 
         | 
| 8 | 
            +
            - Pikimal, LLC
         | 
| 9 | 
            +
            autorequire: 
         | 
| 10 | 
            +
            bindir: bin
         | 
| 11 | 
            +
            cert_chain: []
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            date: 2012-06-15 00:00:00 Z
         | 
| 14 | 
            +
            dependencies: 
         | 
| 15 | 
            +
            - !ruby/object:Gem::Dependency 
         | 
| 16 | 
            +
              name: eventmachine
         | 
| 17 | 
            +
              prerelease: false
         | 
| 18 | 
            +
              requirement: &id001 !ruby/object:Gem::Requirement 
         | 
| 19 | 
            +
                none: false
         | 
| 20 | 
            +
                requirements: 
         | 
| 21 | 
            +
                - - ">="
         | 
| 22 | 
            +
                  - !ruby/object:Gem::Version 
         | 
| 23 | 
            +
                    version: 0.2.10
         | 
| 24 | 
            +
              type: :runtime
         | 
| 25 | 
            +
              version_requirements: *id001
         | 
| 26 | 
            +
            - !ruby/object:Gem::Dependency 
         | 
| 27 | 
            +
              name: eventmachine_httpserver
         | 
| 28 | 
            +
              prerelease: false
         | 
| 29 | 
            +
              requirement: &id002 !ruby/object:Gem::Requirement 
         | 
| 30 | 
            +
                none: false
         | 
| 31 | 
            +
                requirements: 
         | 
| 32 | 
            +
                - - ">="
         | 
| 33 | 
            +
                  - !ruby/object:Gem::Version 
         | 
| 34 | 
            +
                    version: 0.2.1
         | 
| 35 | 
            +
              type: :runtime
         | 
| 36 | 
            +
              version_requirements: *id002
         | 
| 37 | 
            +
            - !ruby/object:Gem::Dependency 
         | 
| 38 | 
            +
              name: trollop
         | 
| 39 | 
            +
              prerelease: false
         | 
| 40 | 
            +
              requirement: &id003 !ruby/object:Gem::Requirement 
         | 
| 41 | 
            +
                none: false
         | 
| 42 | 
            +
                requirements: 
         | 
| 43 | 
            +
                - - ">="
         | 
| 44 | 
            +
                  - !ruby/object:Gem::Version 
         | 
| 45 | 
            +
                    version: 1.16.2
         | 
| 46 | 
            +
              type: :runtime
         | 
| 47 | 
            +
              version_requirements: *id003
         | 
| 48 | 
            +
            - !ruby/object:Gem::Dependency 
         | 
| 49 | 
            +
              name: rspec
         | 
| 50 | 
            +
              prerelease: false
         | 
| 51 | 
            +
              requirement: &id004 !ruby/object:Gem::Requirement 
         | 
| 52 | 
            +
                none: false
         | 
| 53 | 
            +
                requirements: 
         | 
| 54 | 
            +
                - - ">="
         | 
| 55 | 
            +
                  - !ruby/object:Gem::Version 
         | 
| 56 | 
            +
                    version: "0"
         | 
| 57 | 
            +
              type: :development
         | 
| 58 | 
            +
              version_requirements: *id004
         | 
| 59 | 
            +
            description: Monitor chorse
         | 
| 60 | 
            +
            email: grant@pikimal.com
         | 
| 61 | 
            +
            executables: 
         | 
| 62 | 
            +
            - chore-server
         | 
| 63 | 
            +
            - chore-status
         | 
| 64 | 
            +
            - chore-client
         | 
| 65 | 
            +
            extensions: []
         | 
| 66 | 
            +
             | 
| 67 | 
            +
            extra_rdoc_files: []
         | 
| 68 | 
            +
             | 
| 69 | 
            +
            files: 
         | 
| 70 | 
            +
            - lib/chore.rb
         | 
| 71 | 
            +
            - lib/chore/server.rb
         | 
| 72 | 
            +
            - lib/chore/time_help.rb
         | 
| 73 | 
            +
            - lib/chore/store.rb
         | 
| 74 | 
            +
            - lib/chore/constants.rb
         | 
| 75 | 
            +
            - bin/chore-server
         | 
| 76 | 
            +
            - bin/chore-status
         | 
| 77 | 
            +
            - bin/chore-client
         | 
| 78 | 
            +
            homepage: http://github.com/pikimal/chore
         | 
| 79 | 
            +
            licenses: []
         | 
| 80 | 
            +
             | 
| 81 | 
            +
            post_install_message: 
         | 
| 82 | 
            +
            rdoc_options: []
         | 
| 83 | 
            +
             | 
| 84 | 
            +
            require_paths: 
         | 
| 85 | 
            +
            - lib
         | 
| 86 | 
            +
            required_ruby_version: !ruby/object:Gem::Requirement 
         | 
| 87 | 
            +
              none: false
         | 
| 88 | 
            +
              requirements: 
         | 
| 89 | 
            +
              - - ">="
         | 
| 90 | 
            +
                - !ruby/object:Gem::Version 
         | 
| 91 | 
            +
                  version: "0"
         | 
| 92 | 
            +
            required_rubygems_version: !ruby/object:Gem::Requirement 
         | 
| 93 | 
            +
              none: false
         | 
| 94 | 
            +
              requirements: 
         | 
| 95 | 
            +
              - - ">="
         | 
| 96 | 
            +
                - !ruby/object:Gem::Version 
         | 
| 97 | 
            +
                  version: "0"
         | 
| 98 | 
            +
            requirements: []
         | 
| 99 | 
            +
             | 
| 100 | 
            +
            rubyforge_project: 
         | 
| 101 | 
            +
            rubygems_version: 1.8.11
         | 
| 102 | 
            +
            signing_key: 
         | 
| 103 | 
            +
            specification_version: 3
         | 
| 104 | 
            +
            summary: Monitor chores
         | 
| 105 | 
            +
            test_files: []
         | 
| 106 | 
            +
             |