chore 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
+