honcho 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 45989ba8f3704c6d5172f9518e7bfc1c2201d6db
4
- data.tar.gz: 9e8f3b779d67d975a8ccdb7f30ba30ab933c04da
3
+ metadata.gz: a323fb5a455f095cb25c7678de5528b064163773
4
+ data.tar.gz: f1c5281e293b58abb6ffa79ce3a57f8ef7a8f574
5
5
  SHA512:
6
- metadata.gz: b3ad82f616bc1b8deeeff75a498889f5c7804d20145a58e2dd4b1964dfa4d69bda29efd3773f5d13f92184751736d285abf597bc0095707bbb7fe4927926a62f
7
- data.tar.gz: d541b3ad3601e544e244ef2e1ad1a32bf02e9c892dba2b32f7ea7a09695358e8da50a272fbe58d6f6eab77096177b2e81e96b4d868f1acb9b2f259023d889e03
6
+ metadata.gz: e89facaef2ec526648f8bcc49ce39d71d4b1c91fde373f60f5c081aaafa1f9cf26278828bbd429ffbd61f9ad022347747ac9deaaf0f35916530bbd5e39b7d19a
7
+ data.tar.gz: fa05afec855e45ee374c328d664651f6481bd4ae6287d8800a9cf1c324afb26a2a6ee17564d81daf33e4767f141344c15146a4bb5c7119ae381621559d553cb5
data/bin/honcho CHANGED
@@ -13,6 +13,14 @@ OptionParser.new do |opts|
13
13
  opts.on('-c', '--config path', 'specify config file path') do |path|
14
14
  options[:config] = path
15
15
  end
16
+
17
+ opts.on('--ui', 'enable top-like user interface') do
18
+ options[:ui] = true
19
+ end
16
20
  end.parse!
17
21
 
18
- Honcho::Runner.new(options).run
22
+ if options[:ui]
23
+ Honcho::UIRunner.new(options).run
24
+ else
25
+ Honcho::Runner.new(options).run
26
+ end
@@ -11,6 +11,10 @@ module Honcho
11
11
 
12
12
  attr_reader :config, :redis, :runner, :running, :stopping
13
13
 
14
+ def type
15
+ self.class.name.split(':').last.downcase
16
+ end
17
+
14
18
  def check_for_work
15
19
  if run?
16
20
  start
@@ -86,6 +90,10 @@ module Honcho
86
90
  @running = false
87
91
  end
88
92
 
93
+ def total_count
94
+ queued_count + busy_count
95
+ end
96
+
89
97
  private
90
98
 
91
99
  def log(*args)
@@ -105,11 +113,11 @@ module Honcho
105
113
  end
106
114
 
107
115
  def work_to_do?
108
- raise NotImplementedError, "please define #{this.class.name}##{__method__}"
116
+ queued_count > 0
109
117
  end
110
118
 
111
119
  def work_being_done?
112
- raise NotImplementedError, "please define #{this.class.name}##{__method__}"
120
+ busy_count > 0
113
121
  end
114
122
  end
115
123
  end
@@ -1,17 +1,15 @@
1
1
  module Honcho
2
2
  module Adapters
3
3
  class Resque < Base
4
- private
5
-
6
- def work_to_do?
4
+ def queued_count
7
5
  queues = redis.smembers("#{namespace}:queues")
8
6
  counts = queues.map { |q| redis.llen("#{namespace}:queue:#{q}") }
9
- counts.any?(&:nonzero?)
7
+ counts.inject(&:+) || 0
10
8
  end
11
9
 
12
- def work_being_done?
10
+ def busy_count
13
11
  # No way to tell via redis if work is being done in resque? Booo.
14
- false
12
+ 0
15
13
  end
16
14
 
17
15
  def namespace
@@ -1,20 +1,18 @@
1
1
  module Honcho
2
2
  module Adapters
3
3
  class Sidekiq < Base
4
- private
5
-
6
- def work_to_do?
4
+ def queued_count
7
5
  queues = redis.keys("#{namespace}:queue:*")
8
6
  counts = queues.map { |q| redis.llen(q) }
9
- counts.any?(&:nonzero?)
7
+ counts.inject(&:+) || 0
10
8
  end
11
9
 
12
- def work_being_done?
10
+ def busy_count
13
11
  processes = redis.smembers("#{namespace}:processes")
14
12
  counts = processes.map do |process|
15
13
  redis.hget("#{namespace}:#{process}", 'busy').to_i
16
14
  end
17
- counts.any?(&:nonzero?)
15
+ counts.inject(&:+) || 0
18
16
  end
19
17
 
20
18
  def namespace
@@ -0,0 +1,62 @@
1
+ require 'time'
2
+ require 'timeout'
3
+
4
+ module Honcho
5
+ class AppStatus
6
+ def initialize(name, path:)
7
+ @name = name
8
+ @path = path
9
+ end
10
+
11
+ def data
12
+ return {} unless path_exists?
13
+ threads = [
14
+ Thread.new { @sha1 = fetch_sha1 },
15
+ Thread.new { @branch = fetch_branch },
16
+ Thread.new { @commits_ahead = fetch_commits_ahead }
17
+ ]
18
+ threads.each(&:join)
19
+ {
20
+ sha1: @sha1,
21
+ branch: @branch,
22
+ commits_ahead: @commits_ahead
23
+ }
24
+ end
25
+
26
+ private
27
+
28
+ def fetch_sha1
29
+ `cd #{@path} && git rev-parse HEAD 2>/dev/null`.strip
30
+ end
31
+
32
+ def fetch_branch
33
+ `cd #{@path} && git symbolic-ref --short HEAD 2>/dev/null`.strip
34
+ end
35
+
36
+ REMOTE_REF_STALE_TIME = 60 * 60 # 1 hour
37
+ FETCH_ORIGIN_TIMEOUT = 3
38
+
39
+ def fetch_commits_ahead
40
+ remote_branch = `cd #{@path} && git rev-parse --symbolic-full-name --abbrev-ref @{u} 2>/dev/null`.strip
41
+ remote_ref_path = File.join(@path, ".git/refs/remotes/#{remote_branch}")
42
+ return 0 unless File.exist?(remote_ref_path)
43
+ if File.stat(remote_ref_path).mtime < Time.now - REMOTE_REF_STALE_TIME
44
+ begin
45
+ status = Timeout.timeout(FETCH_ORIGIN_TIMEOUT) do
46
+ `cd #{@path} && git fetch origin && git status && touch #{remote_ref_path}`
47
+ end
48
+ rescue Timeout::Error
49
+ status = `cd #{@path} && git status`
50
+ end
51
+ else
52
+ status = `cd #{@path} && git status`
53
+ end
54
+ return 0 unless status =~ /Your branch is behind.*by (\d+) commits?/
55
+ $1.to_i
56
+ end
57
+
58
+ def path_exists?
59
+ File.exist?(@path)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,39 @@
1
+ require 'curses'
2
+
3
+ module Honcho
4
+ module Colors
5
+ COLORS = [
6
+ [Curses::COLOR_BLUE, Curses::A_NORMAL],
7
+ [Curses::COLOR_CYAN, Curses::A_NORMAL],
8
+ [Curses::COLOR_GREEN, Curses::A_NORMAL],
9
+ [Curses::COLOR_MAGENTA, Curses::A_NORMAL],
10
+ [Curses::COLOR_RED, Curses::A_NORMAL],
11
+ [Curses::COLOR_YELLOW, Curses::A_NORMAL],
12
+ [Curses::COLOR_BLUE, Curses::A_BOLD],
13
+ [Curses::COLOR_CYAN, Curses::A_BOLD],
14
+ [Curses::COLOR_GREEN, Curses::A_BOLD],
15
+ [Curses::COLOR_MAGENTA, Curses::A_BOLD],
16
+ [Curses::COLOR_RED, Curses::A_BOLD],
17
+ [Curses::COLOR_YELLOW, Curses::A_BOLD]
18
+ ].freeze
19
+
20
+ def assign_colors_for_curses
21
+ COLORS.each_with_index do |(color, _), index|
22
+ Curses.init_pair(index + 1, color, Curses::COLOR_BLACK)
23
+ end
24
+ apps.keys.each_with_index.each_with_object({}) do |(app, index), hash|
25
+ (_, quality) = COLORS[index]
26
+ hash[app] = [index + 1, quality]
27
+ end
28
+ end
29
+
30
+ def assign_colors_for_ansi
31
+ colors = COLORS.dup
32
+ apps.keys.each_with_object({}) do |app, hash|
33
+ (curses_color_code, curses_color_quality) = colors.shift
34
+ bold = curses_color_quality == Curses::A_BOLD ? 1 : 0
35
+ hash[app] = "#{bold};3#{curses_color_code}"
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,27 @@
1
+ module Honcho
2
+ class PassengerStatus
3
+ def initialize
4
+ @raw = `passenger-status`
5
+ rescue
6
+ raise 'could not execute passenger-status'
7
+ end
8
+
9
+ def data
10
+ return [] if @raw =~ /not serving any applications/
11
+ apps = @raw.split(/\-+ Application groups \-+/).last
12
+ apps.split(/\n\n/).map do |raw_app|
13
+ next unless (root_match = raw_app.match(/App root: (.+)/))
14
+ data = {
15
+ 'root' => root_match[1]
16
+ }
17
+ data['name'] = data['root'].split('/').last
18
+ data['workers'] = raw_app.scan(/\* PID.*\n.*/).map do |worker|
19
+ worker.scan(/([\w ]+?) *: ([\d%MGKhms]+)/).each_with_object({}) do |(key, val), hash|
20
+ hash[key.strip] = val.strip
21
+ end
22
+ end
23
+ data
24
+ end.compact.sort_by { |a| a['name'] }
25
+ end
26
+ end
27
+ end
data/lib/honcho/runner.rb CHANGED
@@ -1,28 +1,15 @@
1
- require 'curses'
2
1
  require 'redis'
3
2
  require 'time'
4
3
  require 'stringio'
5
4
  require 'yaml'
6
5
  require_relative './adapters'
6
+ require_relative './colors'
7
7
 
8
8
  Thread.abort_on_exception = true
9
9
 
10
10
  module Honcho
11
11
  class Runner
12
- COLORS = {
13
- red: '0;31',
14
- green: '0;32',
15
- yellow: '0;33',
16
- blue: '0;34',
17
- magenta: '0;35',
18
- cyan: '0;36',
19
- bright_red: '1;31',
20
- bright_green: '1;32',
21
- bright_yellow: '1;33',
22
- bright_blue: '1;34',
23
- bright_magenta: '1;35',
24
- bright_cyan: '1;36'
25
- }.freeze
12
+ include Colors
26
13
 
27
14
  def initialize(options)
28
15
  @config_file_path = options[:config]
@@ -30,11 +17,12 @@ module Honcho
30
17
  @running = {}
31
18
  @stopping = {}
32
19
  @redis = Redis.new
33
- @adapters = build_adapters
34
- @colors = assign_colors
20
+ @adapters_by_app = build_adapters
21
+ @adapters = @adapters_by_app.values.flatten
22
+ @colors = assign_colors_for_ansi
35
23
  end
36
24
 
37
- attr_reader :config_file_path, :root_path, :adapters, :running, :stopping, :redis, :colors
25
+ attr_reader :config_file_path, :root_path, :adapters_by_app, :adapters, :running, :stopping, :redis, :colors
38
26
 
39
27
  def run
40
28
  trap(:INT) { term_all && exit }
@@ -93,19 +81,12 @@ module Honcho
93
81
  @label_width ||= apps.keys.map(&:size).max
94
82
  end
95
83
 
96
- def assign_colors
97
- color_values = COLORS.values
98
- apps.keys.each_with_object({}) do |app, hash|
99
- hash[app] = color_values.shift
100
- end
101
- end
102
-
103
84
  def build_adapters
104
- apps.flat_map do |app, config|
105
- config.map do |type, worker_config|
85
+ apps.each_with_object({}) do |(app, config), hash|
86
+ hash[app] = config.map do |type, worker_config|
106
87
  build_adapter(app, config, type, worker_config)
107
- end
108
- end.compact
88
+ end.compact
89
+ end
109
90
  end
110
91
 
111
92
  def build_adapter(app, config, type, worker_config)
@@ -0,0 +1,53 @@
1
+ require 'curses'
2
+
3
+ module Honcho
4
+ module UI
5
+ class Table
6
+ def initialize(headings:, width:, top:, left:)
7
+ @headings = headings
8
+ @width = width
9
+ @top = top
10
+ @left = left
11
+ end
12
+
13
+ attr_accessor :width, :headings, :top, :left
14
+ attr_reader :columns
15
+
16
+ def draw(data)
17
+ draw_headings
18
+ draw_data(data)
19
+ Curses.refresh
20
+ end
21
+
22
+ private
23
+
24
+ def draw_headings
25
+ @columns = []
26
+ column = left
27
+ Curses.setpos(top, left)
28
+ headings.each_with_index do |heading, index|
29
+ @columns[index] = column
30
+ column += heading.size
31
+ Curses.addstr(heading)
32
+ end
33
+ end
34
+
35
+ def draw_data(data)
36
+ data.each_with_index do |row, row_index|
37
+ row.each_with_index do |(cell, color_index, color_quality), cell_index|
38
+ cell_start = columns[cell_index]
39
+ cell_width = (columns[cell_index + 1] || width) - cell_start
40
+ Curses.setpos(top + row_index + 1, cell_start)
41
+ if color_index && color_quality
42
+ Curses.attron(Curses.color_pair(color_index) | color_quality) do
43
+ Curses.addstr(cell.to_s.ljust(cell_width))
44
+ end
45
+ else
46
+ Curses.addstr(cell.to_s.ljust(cell_width))
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,159 @@
1
+ require 'stringio'
2
+ require 'curses'
3
+ require 'sys-uptime'
4
+ require_relative './passenger_status'
5
+ require_relative './ui_runner/table'
6
+
7
+ module Honcho
8
+ class UIRunner < Runner
9
+ def run
10
+ setup_curses
11
+ @colors = assign_colors_for_curses
12
+ init_log_window
13
+ draw
14
+ super
15
+ ensure
16
+ unsetup_curses
17
+ end
18
+
19
+ def log(name, message)
20
+ (color_index, color_quality) = colors[name]
21
+ @log.attron(Curses.color_pair(color_index) | color_quality) do
22
+ @log.addstr(name.rjust(label_width))
23
+ end
24
+ @log.addstr(': ')
25
+ @log.addstr(message)
26
+ @log.refresh
27
+ end
28
+
29
+ private
30
+
31
+ def init_log_window
32
+ top = adapters.size + 3
33
+ @log = Curses::Window.new(lines - top, cols, top, 0)
34
+ @log.scrollok(true)
35
+ end
36
+
37
+ def draw
38
+ draw_queues
39
+ draw_uptime
40
+ end
41
+
42
+ def draw_queues
43
+ adapter_names = adapters.map(&:name)
44
+ max_name_width = adapter_names.map(&:size).max
45
+ bar_width = cols - max_name_width - 80
46
+ table = UI::Table.new(
47
+ headings: [
48
+ 'app'.ljust(max_name_width + 2),
49
+ 'webs ',
50
+ "req's ",
51
+ 'sidekiq ',
52
+ 'resque ',
53
+ ' ',
54
+ 'work queue'.ljust(bar_width),
55
+ ' '
56
+ ],
57
+ width: cols - 100,
58
+ top: 1,
59
+ left: 2
60
+ )
61
+ pstatus = passenger_status
62
+ data = adapters_by_app.map do |app, adapters|
63
+ @pstatus_for_app = pstatus[app]
64
+ count = adapters.map(&:total_count).inject(&:+)
65
+ sidekiq = adapters.detect { |a| a.type == 'sidekiq' }
66
+ resque = adapters.detect { |a| a.type == 'resque' }
67
+ [
68
+ [app, *colors[app]],
69
+ [count_web_servers],
70
+ [count_web_requests],
71
+ [sidekiq && sidekiq.running? ? 'running' : ''],
72
+ [resque && resque.running? ? 'running' : ''],
73
+ ['['],
74
+ [bar(count, bar_width), 2, 0],
75
+ [']']
76
+ ]
77
+ end
78
+ table.draw(data)
79
+ end
80
+
81
+ def count_web_servers
82
+ return unless @pstatus_for_app
83
+ return unless (busy_webs = @pstatus_for_app['workers'].select { |s| s['Uptime'] }).any?
84
+ busy_webs.size
85
+ end
86
+
87
+ def count_web_requests
88
+ return unless @pstatus_for_app
89
+ @pstatus_for_app['workers'].map { |s| s['Processed'].to_i }.inject(&:+)
90
+ end
91
+
92
+ def draw_uptime
93
+ x = cols - 38
94
+ uptime = `uptime`
95
+ loadavg = uptime.match(/load averages: (.*)/)[1]
96
+ Curses.setpos(2, x)
97
+ Curses.attron(Curses.color_pair(2)) do
98
+ Curses.addstr('Load average: ')
99
+ end
100
+ Curses.addstr(loadavg)
101
+ time = uptime.match(/up (.*), \d+ users/)[1]
102
+ Curses.setpos(3, x)
103
+ Curses.attron(Curses.color_pair(2)) do
104
+ Curses.addstr('Uptime: ')
105
+ end
106
+ Curses.addstr(time)
107
+ end
108
+
109
+ def print(text, y = nil, x = nil)
110
+ if y && x
111
+ $stdout.print("\033[#{y};#{x}f#{text}")
112
+ else
113
+ $stdout.print(text)
114
+ end
115
+ end
116
+
117
+ def bar(count, width)
118
+ ('|' * count).ljust(width)[0...width]
119
+ end
120
+
121
+ def check_for_work
122
+ draw
123
+ super
124
+ end
125
+
126
+ def lines
127
+ Curses.lines
128
+ end
129
+
130
+ def cols
131
+ Curses.cols
132
+ end
133
+
134
+ def setup_curses
135
+ Curses.init_screen
136
+ Curses.start_color
137
+ Curses.cbreak
138
+ Curses.noecho
139
+ Curses.curs_set(0)
140
+ Curses.stdscr.keypad(true)
141
+ end
142
+
143
+ def unsetup_curses
144
+ Curses.close_screen
145
+ end
146
+
147
+ def show_passenger?
148
+ return @show_passenger unless @show_passenger.nil?
149
+ @show_passenger = system('which passenger-status &>/dev/null')
150
+ end
151
+
152
+ def passenger_status
153
+ return {} unless show_passenger?
154
+ PassengerStatus.new.data.each_with_object({}) do |app, hash|
155
+ hash[app['name']] = app
156
+ end
157
+ end
158
+ end
159
+ end
data/lib/honcho.rb CHANGED
@@ -1 +1,2 @@
1
1
  require_relative './honcho/runner'
2
+ require_relative './honcho/ui_runner'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: honcho
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tim Morgn
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-08-23 00:00:00.000000000 Z
11
+ date: 2016-08-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -52,7 +52,12 @@ files:
52
52
  - lib/honcho/adapters/base.rb
53
53
  - lib/honcho/adapters/resque.rb
54
54
  - lib/honcho/adapters/sidekiq.rb
55
+ - lib/honcho/app_status.rb
56
+ - lib/honcho/colors.rb
57
+ - lib/honcho/passenger_status.rb
55
58
  - lib/honcho/runner.rb
59
+ - lib/honcho/ui_runner.rb
60
+ - lib/honcho/ui_runner/table.rb
56
61
  homepage: https://github.com/seven1m/honcho
57
62
  licenses:
58
63
  - MIT