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