lipsiadmin 3.3.4 → 3.4.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/CHANGELOG +14 -0
- data/README +1 -1
- data/Rakefile +1 -1
- data/lib/controller/ext.rb +1 -1
- data/lib/data_base/attachment/storage.rb +3 -6
- data/lib/loops.rb +277 -0
- data/lib/loops/base.rb +52 -0
- data/lib/loops/daemonize.rb +72 -0
- data/lib/loops/process_manager.rb +101 -0
- data/lib/loops/worker.rb +81 -0
- data/lib/loops/worker_pool.rb +53 -0
- data/lib/utils/html_entities.rb +1 -0
- data/lib/version.rb +2 -2
- data/lib/view/helpers/backend_helper.rb +6 -4
- data/lib/view/helpers/ext/component.rb +1 -0
- data/lib/view/helpers/ext/grid.rb +6 -8
- data/lipsiadmin_generators/backend/REMEMBER +1 -1
- data/lipsiadmin_generators/backend/backend_generator.rb +1 -0
- data/lipsiadmin_generators/backend/templates/models/account.rb +1 -1
- data/lipsiadmin_generators/backend/templates/test/fixtures/accounts.yml +8 -0
- data/lipsiadmin_generators/backend/templates/test/unit/account_test.rb +10 -0
- data/lipsiadmin_generators/loops/loops_generator.rb +27 -0
- data/lipsiadmin_generators/loops/templates/app/loops/APP_README +1 -0
- data/lipsiadmin_generators/loops/templates/app/loops/simple_loop.rb +9 -0
- data/lipsiadmin_generators/loops/templates/config/loops.yml +17 -0
- data/lipsiadmin_generators/loops/templates/script/loops +125 -0
- data/tasks/lipsiadmin_tasks.rake +16 -0
- metadata +20 -106
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'loops/worker'
|
2
|
+
require 'loops/worker_pool'
|
3
|
+
|
4
|
+
module Lipsiadmin
|
5
|
+
module Loops
|
6
|
+
class ProcessManager#:nodoc:
|
7
|
+
attr_reader :logger
|
8
|
+
|
9
|
+
def initialize(config, logger)
|
10
|
+
@config = {
|
11
|
+
'poll_period' => 1,
|
12
|
+
'workers_engine' => 'fork'
|
13
|
+
}.merge(config)
|
14
|
+
|
15
|
+
@logger = logger
|
16
|
+
@worker_pools = {}
|
17
|
+
@shutdown = false
|
18
|
+
end
|
19
|
+
|
20
|
+
def start_workers(name, number, &blk)
|
21
|
+
raise "Need a worker block!" unless block_given?
|
22
|
+
|
23
|
+
logger.debug("Creating a workers pool of #{number} workers for #{name} loop...")
|
24
|
+
@worker_pools[name] = Lipsiadmin::Loops::WorkerPool.new(name, logger, @config['workers_engine'], &blk)
|
25
|
+
@worker_pools[name].start_workers(number)
|
26
|
+
end
|
27
|
+
|
28
|
+
def monitor_workers
|
29
|
+
setup_signals
|
30
|
+
|
31
|
+
logger.debug('Starting workers monitoring code...')
|
32
|
+
loop do
|
33
|
+
logger.debug('Checking workers\' health...')
|
34
|
+
@worker_pools.each do |name, pool|
|
35
|
+
break if @shutdown
|
36
|
+
pool.check_workers
|
37
|
+
end
|
38
|
+
|
39
|
+
break if @shutdown
|
40
|
+
logger.debug("Sleeping for #{@config['poll_period']} seconds...")
|
41
|
+
sleep(@config['poll_period'])
|
42
|
+
end
|
43
|
+
ensure
|
44
|
+
unless wait_for_workers(10)
|
45
|
+
logger.debug("Some workers are still alive after 10 seconds of waiting. Killing them...")
|
46
|
+
stop_workers(true)
|
47
|
+
wait_for_workers(5)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def setup_signals
|
52
|
+
# Zombie rippers
|
53
|
+
trap('CHLD') {}
|
54
|
+
trap('EXIT') {}
|
55
|
+
end
|
56
|
+
|
57
|
+
def wait_for_workers(seconds)
|
58
|
+
seconds.times do
|
59
|
+
logger.debug("Shutting down... waiting for workers to die (we have #{seconds} seconds)...")
|
60
|
+
running_total = 0
|
61
|
+
|
62
|
+
@worker_pools.each do |name, pool|
|
63
|
+
running_total += pool.wait_workers
|
64
|
+
end
|
65
|
+
|
66
|
+
if running_total.zero?
|
67
|
+
logger.debug("All workers are dead. Exiting...")
|
68
|
+
return true
|
69
|
+
end
|
70
|
+
|
71
|
+
logger.debug("#{running_total} workers are still running! Sleeping for a second...")
|
72
|
+
sleep(1)
|
73
|
+
end
|
74
|
+
|
75
|
+
return false
|
76
|
+
end
|
77
|
+
|
78
|
+
def stop_workers(force = false)
|
79
|
+
return unless start_shutdown || force
|
80
|
+
logger.debug("Stopping workers#{force ? '(forced)' : ''}...")
|
81
|
+
|
82
|
+
# Termination loop
|
83
|
+
@worker_pools.each do |name, pool|
|
84
|
+
pool.stop_workers(force)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def stop_workers!
|
89
|
+
return unless start_shutdown
|
90
|
+
stop_workers(false)
|
91
|
+
sleep(1)
|
92
|
+
stop_workers(true)
|
93
|
+
end
|
94
|
+
|
95
|
+
def start_shutdown
|
96
|
+
return false if @shutdown
|
97
|
+
@shutdown = true
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
data/lib/loops/worker.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
module Lipsiadmin
|
2
|
+
module Loops
|
3
|
+
class Worker#:nodoc:
|
4
|
+
attr_reader :logger
|
5
|
+
attr_reader :name
|
6
|
+
attr_reader :pid
|
7
|
+
|
8
|
+
def initialize(name, logger, engine, &blk)
|
9
|
+
raise "Need a worker block!" unless block_given?
|
10
|
+
|
11
|
+
@name = name
|
12
|
+
@logger = logger
|
13
|
+
@engine = engine
|
14
|
+
@worker_block = blk
|
15
|
+
|
16
|
+
@shutdown = false
|
17
|
+
end
|
18
|
+
|
19
|
+
def shutdown?
|
20
|
+
@shutdown
|
21
|
+
end
|
22
|
+
|
23
|
+
def run
|
24
|
+
return if shutdown?
|
25
|
+
if @engine == 'fork'
|
26
|
+
@pid = Kernel.fork do
|
27
|
+
$0 = "loop worker: #{@name}\0"
|
28
|
+
@pid = Process.pid
|
29
|
+
@worker_block.call
|
30
|
+
exit(0)
|
31
|
+
end
|
32
|
+
elsif @engine == 'thread'
|
33
|
+
@thread = Thread.start do
|
34
|
+
@worker_block.call
|
35
|
+
end
|
36
|
+
else
|
37
|
+
raise "Invalid engine name: #{@engine}"
|
38
|
+
end
|
39
|
+
rescue Exception => e
|
40
|
+
logger.error("Exception from worker: #{e} at #{e.backtrace.first}")
|
41
|
+
end
|
42
|
+
|
43
|
+
def running?(verbose = false)
|
44
|
+
return false if shutdown?
|
45
|
+
if @engine == 'fork'
|
46
|
+
return false unless @pid
|
47
|
+
begin
|
48
|
+
Process.waitpid(@pid, Process::WNOHANG)
|
49
|
+
res = Process.kill(0, @pid)
|
50
|
+
logger.debug("KILL(#{@pid}) = #{res}") if verbose
|
51
|
+
return true
|
52
|
+
rescue Errno::ESRCH, Errno::ECHILD, Errno::EPERM => e
|
53
|
+
logger.error("Exception from kill: #{e} at #{e.backtrace.first}") if verbose
|
54
|
+
return false
|
55
|
+
end
|
56
|
+
elsif @engine == 'thread'
|
57
|
+
@thread && @thread.alive?
|
58
|
+
else
|
59
|
+
raise "Invalid engine name: #{@engine}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def stop(force = false)
|
64
|
+
@shutdown = true
|
65
|
+
if @engine == 'fork'
|
66
|
+
begin
|
67
|
+
sig = force ? 'SIGKILL' : 'SIGTERM'
|
68
|
+
logger.debug("Sending #{sig} to ##{@pid}")
|
69
|
+
Process.kill(sig, @pid)
|
70
|
+
rescue Errno::ESRCH, Errno::ECHILD, Errno::EPERM=> e
|
71
|
+
logger.error("Exception from kill: #{e} at #{e.backtrace.first}")
|
72
|
+
end
|
73
|
+
elsif @engine == 'thread'
|
74
|
+
force && !defined?(::JRuby) ? @thread.kill! : @thread.kill
|
75
|
+
else
|
76
|
+
raise "Invalid engine name: #{@engine}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Lipsiadmin
|
2
|
+
module Loops
|
3
|
+
class WorkerPool#:nodoc:
|
4
|
+
attr_reader :logger
|
5
|
+
attr_reader :name
|
6
|
+
|
7
|
+
def initialize(name, logger, engine, &blk)
|
8
|
+
@name = name
|
9
|
+
@logger = logger
|
10
|
+
@worker_block = blk
|
11
|
+
@shutdown = false
|
12
|
+
@engine = engine
|
13
|
+
@workers = []
|
14
|
+
end
|
15
|
+
|
16
|
+
def start_workers(number)
|
17
|
+
logger.debug("Creating #{number} workers for #{name} loop...")
|
18
|
+
number.times do
|
19
|
+
@workers << Lipsiadmin::Loops::Worker.new(name, logger, @engine, &@worker_block)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def check_workers
|
24
|
+
logger.debug("Checking loop #{name} workers...")
|
25
|
+
@workers.each do |worker|
|
26
|
+
next if worker.running? || worker.shutdown?
|
27
|
+
logger.debug("Worker #{worker.name} is not running. Restart!")
|
28
|
+
worker.run
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def wait_workers
|
33
|
+
running = 0
|
34
|
+
@workers.each do |worker|
|
35
|
+
next unless worker.running?
|
36
|
+
running += 1
|
37
|
+
logger.debug("Worker #{name} is still running (#{worker.pid})")
|
38
|
+
end
|
39
|
+
return running
|
40
|
+
end
|
41
|
+
|
42
|
+
def stop_workers(force)
|
43
|
+
return if @shutdown
|
44
|
+
@shutdown = false
|
45
|
+
logger.debug("Stopping loop #{name} workers...")
|
46
|
+
@workers.each do |worker|
|
47
|
+
next unless worker.running?
|
48
|
+
worker.stop(force)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/utils/html_entities.rb
CHANGED
data/lib/version.rb
CHANGED
@@ -22,9 +22,12 @@ module Lipsiadmin
|
|
22
22
|
|
23
23
|
# This method add tab for in your view
|
24
24
|
def tab(name, padding=true, options={}, &block)
|
25
|
-
options[:
|
26
|
-
options[:
|
27
|
-
|
25
|
+
options[:id] ||= name.to_s.downcase.gsub(/[^a-z0-9]+/, '_').gsub(/-+$/, '').gsub(/^-+$/, '')
|
26
|
+
options[:style] = "padding:10px;#{options[:style]}" if padding
|
27
|
+
options[:title] = name
|
28
|
+
options[:tabbed] = true
|
29
|
+
options[:class] = "x-hide-display"
|
30
|
+
concat content_tag(:div, capture(&block), options)
|
28
31
|
end
|
29
32
|
|
30
33
|
# Set the title of the page
|
@@ -49,7 +52,6 @@ module Lipsiadmin
|
|
49
52
|
# Example:
|
50
53
|
#
|
51
54
|
# # in app/views/dossiers/_form.html.haml
|
52
|
-
|
53
55
|
# %tr
|
54
56
|
# %td{:style=>"width:100px"}
|
55
57
|
# %b Customer:
|
@@ -178,6 +178,7 @@ module Lipsiadmin#:nodoc:
|
|
178
178
|
|
179
179
|
def add_object(name, object)
|
180
180
|
if object.class == Component || object.class.superclass == Component
|
181
|
+
@before.delete_if { |b| b.start_with?("var #{object.get_var} = new") }
|
181
182
|
@before << object.to_s
|
182
183
|
@config[name.to_sym] = l(object.get_var)
|
183
184
|
else
|
@@ -157,22 +157,20 @@ module Lipsiadmin
|
|
157
157
|
end
|
158
158
|
|
159
159
|
# Generate or set a new Ext.grid.GroupingView
|
160
|
-
# You can pass
|
160
|
+
# You can pass view :default options that will autocreate a correct GroupingView
|
161
161
|
#
|
162
162
|
# Examples:
|
163
163
|
# view: new Ext.grid.GroupingView({
|
164
164
|
# forceFit:true,
|
165
165
|
# groupTextTpl: '{text} ({[values.rs.length]} {[values.rs.length > 1 ? "Foo" : "Bar"]})'
|
166
166
|
# })
|
167
|
-
# view :forceFit => true, :groupTextTpl =>
|
167
|
+
# view :forceFit => true, :groupTextTpl => '{text} ({[values.rs.length]} {[values.rs.length > 1 ? "Foo" : "Bar"]})'
|
168
168
|
#
|
169
169
|
def view(object=nil, &block)
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
else
|
175
|
-
view = object
|
170
|
+
view = case object
|
171
|
+
when :default then Component.new("Ext.grid.GroupingView", { :forceFit => true })
|
172
|
+
when Hash then Component.new("Ext.grid.GroupingView", { :forceFit => true }.merge(object))
|
173
|
+
else object
|
176
174
|
end
|
177
175
|
add_object(:view, view)
|
178
176
|
end
|
@@ -30,6 +30,7 @@ class BackendGenerator < Rails::Generator::Base
|
|
30
30
|
m.create_all("models", "app/models")
|
31
31
|
m.create_all("views", "app/views")
|
32
32
|
m.create_all("config", "config")
|
33
|
+
m.create_all("test", "test")
|
33
34
|
|
34
35
|
# Using this for prevent raising errors
|
35
36
|
migration = Dir.glob("db/migrate/[0-9]*_*.rb").grep(/[0-9]+_create_accounts.rb$/)
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# This generator bootstraps a Rails project for use with loops
|
2
|
+
class LoopsGenerator < Rails::Generator::Base
|
3
|
+
def manifest
|
4
|
+
record do |m|
|
5
|
+
# Generate app/loops directory and an example loop files
|
6
|
+
m.directory 'app'
|
7
|
+
m.directory 'app/loops'
|
8
|
+
m.file 'app/loops/APP_README', 'app/loops/README'
|
9
|
+
m.file 'app/loops/simple_loop.rb', 'app/loops/simple_loop.rb'
|
10
|
+
|
11
|
+
# Generate script/loops file
|
12
|
+
m.directory 'script'
|
13
|
+
m.file 'script/loops', 'script/loops', :chmod => 0755
|
14
|
+
|
15
|
+
# Generate config/loops.yml file
|
16
|
+
m.directory 'config'
|
17
|
+
m.file 'config/loops.yml', 'config/loops.yml'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
protected
|
22
|
+
|
23
|
+
def banner
|
24
|
+
"Usage: #{$0} loops"
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
This directory should be used to hold all application loops.
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# This file is a configuration file for loops rails plugin
|
2
|
+
#
|
3
|
+
|
4
|
+
# This section is used to control loops manager
|
5
|
+
global:
|
6
|
+
logger: stdout
|
7
|
+
poll_period: 5
|
8
|
+
workers_engine: fork
|
9
|
+
|
10
|
+
# Each record in this section represents one loop which could be ran using loops plugin.
|
11
|
+
# Each loop should have a file in app/loops directory with the same name as its config record.
|
12
|
+
loops:
|
13
|
+
|
14
|
+
# Simple time printing loop
|
15
|
+
simple_loop:
|
16
|
+
workers_number: 1
|
17
|
+
|
@@ -0,0 +1,125 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'rubygems'
|
5
|
+
|
6
|
+
# We need this to be able to start the process w/o being
|
7
|
+
# in the RAILS_ROOT directory
|
8
|
+
ROOT_DIR = File.expand_path('..', File.dirname(__FILE__))
|
9
|
+
Dir.chdir(ROOT_DIR)
|
10
|
+
|
11
|
+
# Default options
|
12
|
+
options = {
|
13
|
+
:daemonize => false,
|
14
|
+
:loops => [],
|
15
|
+
:all_loops => false,
|
16
|
+
:list_loops => false,
|
17
|
+
:pid_file => nil,
|
18
|
+
:stop => false,
|
19
|
+
:framework => 'rails',
|
20
|
+
:environment => nil
|
21
|
+
}
|
22
|
+
|
23
|
+
# Parse command line options
|
24
|
+
opts = OptionParser.new do |opt|
|
25
|
+
opt.banner = "Usage: loops [options]"
|
26
|
+
opt.separator ""
|
27
|
+
opt.separator "Specific options:"
|
28
|
+
|
29
|
+
opt.on('-d', '--daemonize', 'Daemonize when all loops started') { |v| options[:daemonize] = v }
|
30
|
+
opt.on('-s', '--stop', 'Stop daemonized loops if running.') { |v| options[:stop] = v }
|
31
|
+
opt.on('-p', '--pid=file', 'Override loops.yml pid_file option') { |v| options[:pid_file] = v }
|
32
|
+
opt.on('-l', '--loop=loop_name', 'Start specified loop(s) only') { |v| options[:loops] << v }
|
33
|
+
opt.on('-a', '--all', 'Start all loops') { |v| options[:all_loops] = v }
|
34
|
+
opt.on('-L', '--list', 'Shows all available loops with their options') { |v| options[:list_loops] = v }
|
35
|
+
opt.on('-f', '--framework=name', 'Starts within a Rails (rails) or Merb (merb) project. Default is rails.') { |v| options[:framework] = v }
|
36
|
+
opt.on('-e', '--environment=env', 'Set RAILS_ENV value') { |v| options[:environment] = v }
|
37
|
+
|
38
|
+
opt.on_tail("-h", "--help", "Show this message") do
|
39
|
+
puts(opt)
|
40
|
+
exit(0)
|
41
|
+
end
|
42
|
+
|
43
|
+
opt.parse!(ARGV)
|
44
|
+
end
|
45
|
+
|
46
|
+
ENV['RAILS_ENV'] = options[:environment] if options[:environment]
|
47
|
+
|
48
|
+
# Bootstrap Rails
|
49
|
+
require File.join(ROOT_DIR, 'config/boot')
|
50
|
+
require File.join(ROOT_DIR, 'config/environment')
|
51
|
+
|
52
|
+
# Loops default logger
|
53
|
+
LOOPS_DEFAULT_LOGGER = Rails.logger
|
54
|
+
|
55
|
+
#$LOAD_PATH.unshift("vendor/plugins/loops/lib")
|
56
|
+
|
57
|
+
LOOPS_ROOT = ROOT_DIR
|
58
|
+
LOOPS_CONFIG_FILE = File.join(LOOPS_ROOT, 'config/loops.yml')
|
59
|
+
|
60
|
+
require 'loops'
|
61
|
+
require 'loops/daemonize'
|
62
|
+
|
63
|
+
# Load config file
|
64
|
+
puts "Loading config..."
|
65
|
+
Lipsiadmin::Loops.load_config(LOOPS_CONFIG_FILE)
|
66
|
+
options[:pid_file] ||= (Lipsiadmin::Loops.global_config['pid_file'] || "#{Rails.root}/tmp/loops.pid")
|
67
|
+
options[:pid_file] = File.join(LOOPS_ROOT, options[:pid_file]) unless options[:pid_file] =~ /^\//
|
68
|
+
|
69
|
+
# Stop/kill loops if requested
|
70
|
+
if options[:stop]
|
71
|
+
STDOUT.sync = true
|
72
|
+
raise "No pid file or a stale pid file!" unless Lipsiadmin::Loops::Daemonize.check_pid(options[:pid_file])
|
73
|
+
pid = Lipsiadmin::Loops::Daemonize.read_pid(options[:pid_file])
|
74
|
+
print "Killing the process: #{pid}: "
|
75
|
+
|
76
|
+
loop do
|
77
|
+
Process.kill('SIGTERM', pid)
|
78
|
+
sleep(1)
|
79
|
+
break unless Lipsiadmin::Loops::Daemonize.check_pid(options[:pid_file])
|
80
|
+
print(".")
|
81
|
+
end
|
82
|
+
|
83
|
+
puts " Done!"
|
84
|
+
exit(0)
|
85
|
+
end
|
86
|
+
|
87
|
+
# List loops if requested
|
88
|
+
if options[:list_loops]
|
89
|
+
puts "Available loops:"
|
90
|
+
Lipsiadmin::Loops.loops_config.each do |name, config|
|
91
|
+
puts "Loop: #{name}" + (config['disabled'] ? ' (disabled)' : '')
|
92
|
+
config.each do |k,v|
|
93
|
+
puts " - #{k}: #{v}"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
puts
|
97
|
+
exit(0)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Ignore --loop options if --all parameter passed
|
101
|
+
options[:loops] = :all if options[:all_loops]
|
102
|
+
|
103
|
+
# Check what loops we gonna run
|
104
|
+
if options[:loops] == []
|
105
|
+
puts opts
|
106
|
+
exit(0)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Pid file check
|
110
|
+
raise "Can't start, another process exists!" if Lipsiadmin::Loops::Daemonize.check_pid(options[:pid_file])
|
111
|
+
|
112
|
+
# Daemonization
|
113
|
+
app_name = "loops monitor: #{ options[:loops].join(' ') rescue 'all' }\0"
|
114
|
+
Lipsiadmin::Loops::Daemonize.daemonize(app_name) if options[:daemonize]
|
115
|
+
|
116
|
+
# Pid file creation
|
117
|
+
Lipsiadmin::Loops::Daemonize.create_pid(options[:pid_file])
|
118
|
+
|
119
|
+
# Workers processing
|
120
|
+
puts "Starting workers"
|
121
|
+
Lipsiadmin::Loops.start_loops!(options[:loops])
|
122
|
+
|
123
|
+
# Workers exited, cleaning up
|
124
|
+
puts "Cleaning pid file..."
|
125
|
+
File.delete(options[:pid_file]) rescue nil
|