honcho 1.1.0 → 1.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.
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