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 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
@@ -0,0 +1,7 @@
1
+ module Chore
2
+ module Constants
3
+ DEFAULT_LISTEN_PORT = 32786
4
+ DEFAULT_WEB_PORT = 43210
5
+ DEFAULT_CLI_PORT = 43211
6
+ end
7
+ end
@@ -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>&nbsp;</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
+
@@ -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
+