runit-man 1.2
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/runit-man.rb +19 -0
- data/i18n/en.yml +35 -0
- data/i18n/ru.yml +37 -0
- data/lib/runit-man/app.rb +77 -0
- data/lib/runit-man/erb-to-erubis.rb +17 -0
- data/lib/runit-man/helpers.rb +47 -0
- data/lib/runit-man/log_location_cache.rb +73 -0
- data/lib/runit-man/partials.rb +20 -0
- data/lib/runit-man/service_info.rb +156 -0
- data/public/css/runit-man.css +22 -0
- data/public/css/tripoli.simple.css +29 -0
- data/public/css/tripoli.simple.ie.css +14 -0
- data/public/css/tripoli.type.css +36 -0
- data/public/css/tripoli.visual.css +119 -0
- data/public/js/jquery-1.4.1.min.js +152 -0
- data/public/js/runit-man.js +66 -0
- data/sv/log/run +3 -0
- data/sv/run +3 -0
- data/views/_service_action.erb +3 -0
- data/views/_service_info.erb +29 -0
- data/views/_services.erb +20 -0
- data/views/index.erb +14 -0
- data/views/layout.erb +33 -0
- data/views/log.erb +9 -0
- metadata +159 -0
data/bin/runit-man.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'optparse'
|
5
|
+
|
6
|
+
require 'runit-man/app'
|
7
|
+
|
8
|
+
RunitMan.set :active_services_directory, '/etc/service'
|
9
|
+
RunitMan.set :all_services_directory, '/etc/sv'
|
10
|
+
|
11
|
+
OptionParser.new { |op|
|
12
|
+
op.on('-s server') { |val| RunitMan.set :server, val }
|
13
|
+
op.on('-p port') { |val| RunitMan.set :port, val.to_i }
|
14
|
+
op.on('-b addr') { |val| RunitMan.set :bind, val }
|
15
|
+
op.on('-a active_services_directory (/etc/service by default)') { |val| RunitMan.set :active_services_directory, val }
|
16
|
+
op.on('-f all_services_directory (/etc/sv by default)') { |val| RunitMan.set :all_services_directory, val }
|
17
|
+
}.parse!(ARGV.dup)
|
18
|
+
|
19
|
+
RunitMan.run!
|
data/i18n/en.yml
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
runit:
|
2
|
+
label: runit web management tool
|
3
|
+
title: "%1 - %2"
|
4
|
+
header: "%1: %2"
|
5
|
+
error: Error encountered while accessing resource
|
6
|
+
loading: "Please wait while loading…"
|
7
|
+
footer: "State of services updated automatically every %1 seconds."
|
8
|
+
services:
|
9
|
+
table:
|
10
|
+
caption: Services
|
11
|
+
headers:
|
12
|
+
pid: PID
|
13
|
+
name: Name
|
14
|
+
stat: Status
|
15
|
+
actions: Actions
|
16
|
+
log_file: Log file
|
17
|
+
values:
|
18
|
+
log_hint: Open tail 100 lines of %1 service log in new window
|
19
|
+
log_absent: Absent
|
20
|
+
footer: Updated at %1
|
21
|
+
subst:
|
22
|
+
inactive: <span class="inactive">inactive</span>
|
23
|
+
down: <span class="down">down<span>
|
24
|
+
run: <span class="run">run<span>
|
25
|
+
actions:
|
26
|
+
start: Start
|
27
|
+
stop: Stop
|
28
|
+
restart: Restart
|
29
|
+
switch_down: Deactivate
|
30
|
+
switch_up: Activate
|
31
|
+
log:
|
32
|
+
title: "Tail 100 lines of service %1 log - %2"
|
33
|
+
header: "Tail 100 lines of service <strong>%1</strong> log: %2"
|
34
|
+
updated: "Page updated at %1"
|
35
|
+
reload: Reload
|
data/i18n/ru.yml
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
runit:
|
2
|
+
label: runit - управляющий web-интерфейс
|
3
|
+
title: "%1 - %2"
|
4
|
+
header: "%1: %2"
|
5
|
+
error: Ошибка обращения к ресурсу
|
6
|
+
loading: "Идёт загрузка…"
|
7
|
+
footer: "Состояние сервисов обновляется автоматически каждые %1 секунд."
|
8
|
+
services:
|
9
|
+
table:
|
10
|
+
caption: Сервисы
|
11
|
+
headers:
|
12
|
+
pid: PID
|
13
|
+
name: Название
|
14
|
+
stat: Состояние
|
15
|
+
actions: Действия
|
16
|
+
log_file: Лог
|
17
|
+
values:
|
18
|
+
log_hint: Отрыть последние 100 строк лога сервиса %1 в новом окне
|
19
|
+
log_absent: Отсутствует
|
20
|
+
footer: Обновлено %1
|
21
|
+
subst:
|
22
|
+
inactive: <span class="inactive">неактивен</span>
|
23
|
+
down: <span class="down">остановлен</span>
|
24
|
+
run: <span class="run">работает</span>
|
25
|
+
got: получен
|
26
|
+
want: ожидается
|
27
|
+
actions:
|
28
|
+
start: Запустить
|
29
|
+
stop: Остановить
|
30
|
+
restart: Перезапустить
|
31
|
+
switch_down: Выключить
|
32
|
+
switch_up: Включить
|
33
|
+
log:
|
34
|
+
title: "Последние 100 строк лога сервиса %1 - %2"
|
35
|
+
header: "Последние 100 строк лога сервиса <strong>%1</strong>: %2"
|
36
|
+
updated: "Страница обновлена %1"
|
37
|
+
reload: Обновить страницу
|
@@ -0,0 +1,77 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# encoding: utf-8
|
3
|
+
|
4
|
+
require 'sinatra/base'
|
5
|
+
require 'sinatra/r18n'
|
6
|
+
require 'runit-man/erb-to-erubis'
|
7
|
+
require 'runit-man/helpers'
|
8
|
+
|
9
|
+
R18n::Filters.on :variables
|
10
|
+
|
11
|
+
CONTENT_TYPES = {
|
12
|
+
:html => 'text/html',
|
13
|
+
:css => 'text/css',
|
14
|
+
:js => 'application/x-javascript',
|
15
|
+
:json => 'application/json'
|
16
|
+
}.freeze
|
17
|
+
|
18
|
+
|
19
|
+
class RunitMan < Sinatra::Base
|
20
|
+
set :environment, :production
|
21
|
+
set :static, true
|
22
|
+
set :logging, true
|
23
|
+
set :dump_errors, true
|
24
|
+
set :raise_errors, false
|
25
|
+
set :root, File.expand_path(File.join('..', '..'), File.dirname(__FILE__))
|
26
|
+
|
27
|
+
register Sinatra::R18n
|
28
|
+
|
29
|
+
helpers do
|
30
|
+
include Helpers
|
31
|
+
end
|
32
|
+
|
33
|
+
before do
|
34
|
+
base_content_type = case request.env['REQUEST_URI']
|
35
|
+
when /\.css$/ then :css
|
36
|
+
when /\.js$/ then :js
|
37
|
+
when /\.json$/ then :json
|
38
|
+
else :html
|
39
|
+
end
|
40
|
+
content_type CONTENT_TYPES[base_content_type], :charset => 'utf-8'
|
41
|
+
end
|
42
|
+
|
43
|
+
get '/' do
|
44
|
+
@scripts = [ 'jquery-1.4.1.min' ]
|
45
|
+
@title = host_name
|
46
|
+
erb :index
|
47
|
+
end
|
48
|
+
|
49
|
+
get '/services' do
|
50
|
+
partial :services
|
51
|
+
end
|
52
|
+
|
53
|
+
get '/:name/log' do |name|
|
54
|
+
srv = ServiceInfo[name]
|
55
|
+
return not_found if srv.nil? || !srv.logged?
|
56
|
+
@scripts = []
|
57
|
+
@title = t.runit.services.log.title(h(name), h(host_name))
|
58
|
+
erb :log, :locals => {
|
59
|
+
:name => name,
|
60
|
+
:text => `tail -n 100 #{srv.log_file_location}`
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
def log_action(name, text)
|
65
|
+
addr = request.env.include?('X_REAL_IP') ? request.env['X_REAL_IP'] : request.env['REMOTE_ADDR']
|
66
|
+
puts "#{addr} - - [#{Time.now}] \"Do #{text} on #{name}\""
|
67
|
+
end
|
68
|
+
|
69
|
+
post '/:name/:action' do |name, action|
|
70
|
+
srv = ServiceInfo[name]
|
71
|
+
action = "#{action}!".to_sym
|
72
|
+
return not_found if srv.nil? || !srv.respond_to?(action)
|
73
|
+
srv.send(action)
|
74
|
+
log_action(name, action)
|
75
|
+
''
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Sinatra::Erb
|
2
|
+
def erb(content, options={})
|
3
|
+
begin
|
4
|
+
require 'erubis'
|
5
|
+
@@erb_class = Erubis::Eruby
|
6
|
+
rescue LoadError
|
7
|
+
require "erb"
|
8
|
+
@@erb_class = ::ERB
|
9
|
+
end
|
10
|
+
render(:erb, content, options)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
def render_erb(content, options = {})
|
15
|
+
@@erb_class.new(content).result(binding)
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'runit-man/service_info'
|
3
|
+
require 'runit-man/partials'
|
4
|
+
require 'sinatra/content_for'
|
5
|
+
|
6
|
+
module Helpers
|
7
|
+
include Rack::Utils
|
8
|
+
include Sinatra::Partials
|
9
|
+
include Sinatra::ContentFor
|
10
|
+
alias_method :h, :escape_html
|
11
|
+
|
12
|
+
attr_accessor :even_or_odd_state
|
13
|
+
|
14
|
+
def host_name
|
15
|
+
unless @host_name
|
16
|
+
@host_name = Socket.gethostbyname(Socket.gethostname).first
|
17
|
+
end
|
18
|
+
@host_name
|
19
|
+
end
|
20
|
+
|
21
|
+
def service_infos
|
22
|
+
ServiceInfo.all
|
23
|
+
end
|
24
|
+
|
25
|
+
def service_action(name, action, label)
|
26
|
+
partial :service_action, :locals => {
|
27
|
+
:name => name,
|
28
|
+
:action => action,
|
29
|
+
:label => label
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def even_or_odd
|
34
|
+
self.even_or_odd_state = !even_or_odd_state
|
35
|
+
even_or_odd_state
|
36
|
+
end
|
37
|
+
|
38
|
+
def stat_subst(s)
|
39
|
+
s.split(/\s/).map do |s|
|
40
|
+
if s =~ /(\w+)/ && t.runit.services.table.subst[$1].translated?
|
41
|
+
s.sub(/\w+/, t.runit.services.table.subst[$1].to_s)
|
42
|
+
else
|
43
|
+
s
|
44
|
+
end
|
45
|
+
end.join(' ')
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
class LogLocationCache
|
2
|
+
TIME_LIMIT = 6000
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
clear
|
6
|
+
end
|
7
|
+
|
8
|
+
def [](pid)
|
9
|
+
pid = pid.to_i
|
10
|
+
unless pids.include?(pid)
|
11
|
+
set_pid_log_location(pid, get_pid_location(pid))
|
12
|
+
end
|
13
|
+
pids[pid][:value]
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
attr_accessor :query_counter
|
18
|
+
attr_accessor :pids
|
19
|
+
|
20
|
+
def clear
|
21
|
+
self.query_counter = 0
|
22
|
+
self.pids = {}
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
def remove_old_values
|
27
|
+
self.query_counter = query_counter + 1
|
28
|
+
if query_counter < 1000
|
29
|
+
return
|
30
|
+
end
|
31
|
+
self.query_counter = 0
|
32
|
+
limit = Time.now - TIME_LIMIT
|
33
|
+
pids.keys.each do |pid|
|
34
|
+
if pids[pid][:time] < limit
|
35
|
+
pids.remove(pid)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
self
|
39
|
+
end
|
40
|
+
|
41
|
+
def get_pid_location(lpid)
|
42
|
+
folder = log_folder(lpid)
|
43
|
+
return nil if folder.nil?
|
44
|
+
File.join(folder, 'current')
|
45
|
+
end
|
46
|
+
|
47
|
+
def log_command(lpid)
|
48
|
+
return nil if lpid.nil?
|
49
|
+
ps_output = `ps -o args -p #{lpid} 2>&1`.split("\n")
|
50
|
+
ps_output.shift
|
51
|
+
cmd = ps_output.first
|
52
|
+
cmd = cmd.chomp unless cmd.nil?
|
53
|
+
cmd = nil if cmd == ''
|
54
|
+
cmd
|
55
|
+
end
|
56
|
+
|
57
|
+
def log_folder(lpid)
|
58
|
+
cmd = log_command(lpid)
|
59
|
+
return nil if cmd.nil?
|
60
|
+
args = cmd.split(/\s+/).select { |arg| arg !~ /^\-/ }
|
61
|
+
return nil if args.shift != 'svlogd'
|
62
|
+
args.shift
|
63
|
+
end
|
64
|
+
|
65
|
+
def set_pid_log_location(pid, log_location)
|
66
|
+
remove_old_values
|
67
|
+
pids[pid.to_i] = {
|
68
|
+
:value => log_location,
|
69
|
+
:time => Time.now
|
70
|
+
}
|
71
|
+
self
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# stolen from http://github.com/cschneid/irclogger/blob/master/lib/partials.rb
|
2
|
+
# and made a lot more robust by me
|
3
|
+
# this implementation uses erb by default. if you want to use any other template mechanism
|
4
|
+
# then replace `erb` on line 13 and line 17 with `haml` or whatever
|
5
|
+
module Sinatra::Partials
|
6
|
+
def partial(template, *args)
|
7
|
+
template_array = template.to_s.split('/')
|
8
|
+
template = template_array[0..-2].join('/') + "/_#{template_array[-1]}"
|
9
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
10
|
+
options.merge!(:layout => false)
|
11
|
+
if collection = options.delete(:collection) then
|
12
|
+
collection.inject([]) do |buffer, member|
|
13
|
+
buffer << erb(:"#{template}", options.merge(:layout =>
|
14
|
+
false, :locals => {template_array[-1].to_sym => member}))
|
15
|
+
end.join("\n")
|
16
|
+
else
|
17
|
+
erb(:"#{template}", options)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
require 'runit-man/log_location_cache'
|
2
|
+
|
3
|
+
class ServiceInfo
|
4
|
+
attr_reader :name
|
5
|
+
|
6
|
+
def initialize(a_name)
|
7
|
+
@name = a_name
|
8
|
+
end
|
9
|
+
|
10
|
+
def logged?
|
11
|
+
File.directory?(log_supervise_folder)
|
12
|
+
end
|
13
|
+
|
14
|
+
def stat
|
15
|
+
return 'inactive' unless supervise?
|
16
|
+
r = 'indeterminate'
|
17
|
+
File.open(File.join(supervise_folder, 'stat'), 'r') { |f| r = f.gets }
|
18
|
+
r
|
19
|
+
end
|
20
|
+
|
21
|
+
def active?
|
22
|
+
File.directory?(active_service_folder) || File.symlink?(active_service_folder)
|
23
|
+
end
|
24
|
+
|
25
|
+
def switchable?
|
26
|
+
File.symlink?(active_service_folder) || File.directory?(inactive_service_folder)
|
27
|
+
end
|
28
|
+
|
29
|
+
def run?
|
30
|
+
stat =~ /\brun\b/
|
31
|
+
end
|
32
|
+
|
33
|
+
def up!
|
34
|
+
send_signal! :u
|
35
|
+
end
|
36
|
+
|
37
|
+
def down!
|
38
|
+
send_signal! :d
|
39
|
+
end
|
40
|
+
|
41
|
+
def switch_down!
|
42
|
+
down!
|
43
|
+
File.unlink(active_service_folder)
|
44
|
+
end
|
45
|
+
|
46
|
+
def switch_up!
|
47
|
+
File.symlink(inactive_service_folder, active_service_folder)
|
48
|
+
end
|
49
|
+
|
50
|
+
def restart!
|
51
|
+
down!
|
52
|
+
up!
|
53
|
+
end
|
54
|
+
|
55
|
+
def pid
|
56
|
+
r = nil
|
57
|
+
if supervise?
|
58
|
+
File.open(File.join(supervise_folder, 'pid'), 'r') { |f| r = f.gets }
|
59
|
+
end
|
60
|
+
r = r.chomp unless r.nil?
|
61
|
+
r = nil if r == ''
|
62
|
+
r
|
63
|
+
end
|
64
|
+
|
65
|
+
def log_pid
|
66
|
+
r = nil
|
67
|
+
if logged?
|
68
|
+
File.open(File.join(log_supervise_folder, 'pid'), 'r') { |f| r = f.gets }
|
69
|
+
end
|
70
|
+
r = r.chomp unless r.nil?
|
71
|
+
r = nil if r == ''
|
72
|
+
r
|
73
|
+
end
|
74
|
+
|
75
|
+
def log_file_location
|
76
|
+
rel_path = self.class.log_location_cache[log_pid]
|
77
|
+
return nil if rel_path.nil?
|
78
|
+
File.expand_path(rel_path, log_run_folder)
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
def inactive_service_folder
|
83
|
+
File.join(RunitMan.all_services_directory, name)
|
84
|
+
end
|
85
|
+
|
86
|
+
def active_service_folder
|
87
|
+
File.join(RunitMan.active_services_directory, name)
|
88
|
+
end
|
89
|
+
|
90
|
+
def supervise_folder
|
91
|
+
File.join(active_service_folder, 'supervise')
|
92
|
+
end
|
93
|
+
|
94
|
+
def log_run_folder
|
95
|
+
File.join(active_service_folder, 'log')
|
96
|
+
end
|
97
|
+
|
98
|
+
def log_supervise_folder
|
99
|
+
File.join(log_run_folder, 'supervise')
|
100
|
+
end
|
101
|
+
|
102
|
+
def supervise?
|
103
|
+
File.directory?(supervise_folder)
|
104
|
+
end
|
105
|
+
|
106
|
+
def send_signal!(signal)
|
107
|
+
return unless supervise?
|
108
|
+
File.open(File.join(supervise_folder, 'control'), 'w') { |f| f.print signal.to_s }
|
109
|
+
end
|
110
|
+
|
111
|
+
class << self
|
112
|
+
def all
|
113
|
+
all_service_names.sort.map do |name|
|
114
|
+
ServiceInfo.new(name)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def [](name)
|
119
|
+
all_service_names.include?(name) ? ServiceInfo.new(name) : nil
|
120
|
+
end
|
121
|
+
|
122
|
+
def log_location_cache
|
123
|
+
unless @log_location_cache
|
124
|
+
@log_location_cache = LogLocationCache.new
|
125
|
+
end
|
126
|
+
@log_location_cache
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
def itself_or_parent?(name)
|
131
|
+
name == '.' || name == '..'
|
132
|
+
end
|
133
|
+
|
134
|
+
def active_service_names
|
135
|
+
return [] unless File.directory?(RunitMan.active_services_directory)
|
136
|
+
Dir.entries(RunitMan.active_services_directory).reject do |name|
|
137
|
+
full_name = File.join(RunitMan.active_services_directory, name)
|
138
|
+
itself_or_parent?(name) || (!File.symlink?(full_name) && !File.directory?(full_name))
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def inactive_service_names
|
143
|
+
return [] unless File.directory?(RunitMan.all_services_directory)
|
144
|
+
actives = active_service_names
|
145
|
+
Dir.entries(RunitMan.all_services_directory).reject do |name|
|
146
|
+
full_name = File.join(RunitMan.all_services_directory, name)
|
147
|
+
itself_or_parent?(name) || !File.directory?(full_name) || actives.include?(name)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def all_service_names
|
152
|
+
(active_service_names + inactive_service_names)
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
end
|