videoreg 0.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/videoreg.rb ADDED
@@ -0,0 +1,232 @@
1
+ Dir[File.dirname(File.expand_path(__FILE__))+"/videoreg/*.rb"].each { |f| require f }
2
+ require 'rubygems'
3
+ require 'logger'
4
+ require 'ostruct'
5
+ require 'amqp'
6
+ require 'json'
7
+ require_relative './videoreg/util'
8
+
9
+ ####################################################
10
+ # Main videoreg module
11
+ module Videoreg
12
+
13
+ # configurable constants
14
+ VERSION = "0.1"
15
+ DAEMON_NAME = "videoreg"
16
+ MAX_THREAD_WAIT_LIMIT_SEC = 600 # time while daemon waits until capture thread exists
17
+ UDEV_RULES_FILE = '/etc/udev/rules.d/50-udev-videoreg.rules'
18
+ DEV_SYMLINK = "webcam" # name of the devices symlinks
19
+
20
+ # internal constants
21
+ ALLOWED_CONFIG_OPTIONS = %w[mq_host mq_queue pid_path log_path device]
22
+ MSG_HALT = 'HALT'
23
+ MSG_RECOVER = 'RECOVER'
24
+ MSG_PAUSE = 'PAUSE'
25
+ MSG_RESUME = 'RESUME'
26
+ MSG2ACTION = {MSG_HALT => :halt!, MSG_PAUSE => :pause!, MSG_RESUME => :resume!, MSG_RECOVER => :recover!}
27
+
28
+ @registered_regs = {}
29
+ @time_started = Time.now
30
+ @run_options = OpenStruct.new(
31
+ :device => :all,
32
+ :action => :run,
33
+ :log_path => 'videoreg.log',
34
+ :pid_path => '/tmp/videoreg.pid',
35
+ :mq_host => '127.0.0.1',
36
+ :mq_queue => 'ifree.videoreg.server'
37
+ )
38
+
39
+ class << self
40
+ # Version info
41
+ def version_info
42
+ "#{DAEMON_NAME} v.#{VERSION}"
43
+ end
44
+
45
+ # Disconnect from RabbitMQ
46
+ def mq_disconnect(connection)
47
+ logger.info "Disconnecting from RabbitMQ..."
48
+ connection.close { EM.stop { exit } }
49
+ end
50
+
51
+ # Listen the incoming messages from RabbitMQ
52
+ def mq_listen(&block)
53
+ Thread.new {
54
+ begin
55
+ logger.info "New messaging thread created for RabbitMQ #{opt.mq_host} / #{opt.mq_queue}"
56
+ AMQP.start(:host => opt.mq_host) do |connection|
57
+ q = AMQP::Channel.new(connection).queue(opt.mq_queue)
58
+ q.subscribe do |msg|
59
+ Videoreg::Base.logger.info "Received message from RabbitMQ #{msg}..."
60
+ block.call(connection, msg) if block_given?
61
+ end
62
+ Signal.add_trap("TERM") { q.delete; mq_disconnect(connection) }
63
+ Signal.add_trap(0) { q.delete; mq_disconnect(connection) }
64
+ end
65
+ rescue => e
66
+ logger.error "Error during establishing the connection to RabbitMQ: #{e.message}"
67
+ @dante_runner.stop if @dante_runner
68
+ end
69
+ }
70
+ end
71
+
72
+ # Send a message to the RabbitMQ
73
+ def mq_send(message, arg = nil)
74
+ AMQP.start(:host => opt.mq_host) do |connection|
75
+ channel = AMQP::Channel.new(connection)
76
+ logger.info "Publish message to RabbitMQ '#{message}' with arg '#{arg}' to '#{opt.mq_queue}'..."
77
+ channel.default_exchange.publish({:msg => message, :arg => arg}.to_json, :routing_key => opt.mq_queue)
78
+ EM.add_timer(0.5) { mq_disconnect(connection) }
79
+ end
80
+ end
81
+
82
+ def run_options
83
+ @run_options
84
+ end
85
+
86
+ def registrars
87
+ @registered_regs
88
+ end
89
+
90
+ def logger
91
+ Videoreg::Base.logger
92
+ end
93
+
94
+
95
+ ####################################################
96
+ # Options
97
+ def opt(*args)
98
+ if !args.empty? && args[0].is_a?(Hash)
99
+ op = args[0].flatten
100
+ run_options.send("#{op[0]}=", op[1])
101
+ else
102
+ run_options
103
+ end
104
+ end
105
+
106
+ # Capture some other opts
107
+ ALLOWED_CONFIG_OPTIONS.each do |op|
108
+ self.send(:define_method, op) do |value|
109
+ opt.send("#{op}=".to_sym, value)
110
+ end
111
+ end
112
+
113
+ # Shortcut to create new registrar's configuration
114
+ def reg(&block)
115
+ r = Registrar.new
116
+ Signal.add_trap(0) { r.safe_release! }
117
+ r.logger = opt.logger if opt.logger
118
+ r.config.instance_eval(&block)
119
+ registrars[r.config.device] = r
120
+ end
121
+
122
+ # Calculate current registrars list
123
+ def calc_reg_list(device = :all)
124
+ registrars.find_all { |dev, reg| device.to_sym == :all || device.to_s == dev }.map { |vp| vp[1] }
125
+ end
126
+
127
+ # Initiate new dante runner
128
+ def init_dante_runner
129
+ dante_opts = {:pid_path => '/tmp/videoreg.pid', :log_path => opt.log_path}
130
+ dante_opts.merge!(:kill => true) if opt.action == :kill
131
+ dante_opts.merge!(:pid_path => opt.pid_path) if opt.pid_path
132
+ Dante::Runner.new(DAEMON_NAME, dante_opts)
133
+ end
134
+
135
+ # Run daemon
136
+ def run_daemon(regs)
137
+ Signal.add_trap("TERM") { File.unlink(opt.pid_path) if File.exists?(opt.pid_path) }
138
+ @time_started = Time.now
139
+ # Run message thread
140
+ mq_listen do |connection, message|
141
+ begin
142
+ raise "Unexpected message struct received: #{message}!" unless (message = JSON.parse(message)).is_a?(Hash)
143
+ opt.device = message["arg"] if message["arg"]
144
+ if (action = MSG2ACTION[message["msg"]])
145
+ logger.info "#{message["msg"]} MESSAGE RECEIVED!"
146
+ calc_reg_list(opt.device).each { |reg| reg.send(action) }
147
+ else
148
+ logger.error "UNKNOWN MESSAGE RECEIVED!"
149
+ end
150
+ rescue => e
151
+ logger.error "Exception during incoming message processing: #{e.message}: \n#{e.backtrace.join("\n")}"
152
+ end
153
+ end
154
+ # Run main thread
155
+ regs.map { |reg|
156
+ logger.info "Starting continuous registration from device #{reg.device}..."
157
+ {:reg => reg, :thread => reg.continuous}
158
+ }.each { |reg_hash|
159
+ while true do # avoid deadlock exception
160
+ reg_hash[:thread].join(MAX_THREAD_WAIT_LIMIT_SEC)
161
+ break if reg_hash[:reg].terminated? # break if thread was terminated
162
+ end if reg_hash[:reg] && reg_hash[:thread]
163
+ }
164
+ @time_ended = Time.now
165
+ logger.info "Daemon finished execution. Uptime #{@time_ended - @time_started} sec"
166
+ end
167
+
168
+ # Shortcut to run action on registrar(s)
169
+ def run(device = :all, action = opt.action)
170
+
171
+ # Input
172
+ @registrars = calc_reg_list(device)
173
+ @dante_runner = init_dante_runner
174
+ opt.action, action = :run, :run if @dante_runner.daemon_stopped? && opt.action == :recover
175
+
176
+ # Main actions switch
177
+ puts "Running command '#{opt.action}' for device(s): '#{device}'..."
178
+ case action
179
+ when :kill then
180
+ @dante_runner.stop
181
+ when :pause then
182
+ mq_send(MSG_PAUSE, device) if @dante_runner.daemon_running?
183
+ when :resume then
184
+ mq_send(MSG_RESUME, device) if @dante_runner.daemon_running?
185
+ when :recover then
186
+ mq_send(MSG_RECOVER, device) if @dante_runner.daemon_running?
187
+ when :ensure then
188
+ uptime = (@dante_runner.daemon_running?) ? Time.now - File.stat(opt.pid_path).ctime : 0
189
+ [{:daemon_running? => @dante_runner.daemon_running?, :uptime => uptime}] + @registrars.map { |reg|
190
+ {
191
+ :device => reg.device,
192
+ :device_exists? => reg.device_exists?,
193
+ :process_alive? => reg.process_alive?,
194
+ :paused? => reg.paused?
195
+ }
196
+ }
197
+ when :halt then
198
+ mq_send(MSG_HALT, device) if @dante_runner.daemon_running?
199
+ when :run then
200
+ @dante_runner.execute(:daemonize => true) {
201
+ logger.info "Starting daemon with options: #{opt.marshal_dump}"
202
+ run_daemon(@registrars)
203
+ }
204
+ when :reset then
205
+ @registrars.each { |reg|
206
+ reg.force_release_lock!
207
+ }
208
+ logger.info "Forced to release pidfile #{opt.pid_path}"
209
+ File.unlink(opt.pid_path) if File.exists?(opt.pid_path)
210
+ else
211
+ raise "Unsupported action #{action} provided to runner!"
212
+ end
213
+ end
214
+
215
+ end
216
+
217
+
218
+ end
219
+
220
+ ####################################################
221
+ # Extend ruby signal to support several listeners
222
+ def Signal.add_trap(sig, &block)
223
+ @added_signals = {} unless @added_signals
224
+ @added_signals[sig] = [] unless @added_signals[sig]
225
+ @added_signals[sig] << block
226
+ end
227
+
228
+ # catch interrupt signal && call all listeners
229
+ Signal.trap("TERM", proc { @added_signals && @added_signals[0].each { |p| p.call } })
230
+
231
+
232
+
@@ -0,0 +1,41 @@
1
+ require 'logger'
2
+ require 'stringio'
3
+ require_relative 'util'
4
+ module Videoreg
5
+ class Base
6
+ @@logger = ::Logger.new(STDOUT)
7
+
8
+ def logger
9
+ self.class.logger
10
+ end
11
+
12
+ def logger=(log)
13
+ self.class.logger=log
14
+ end
15
+
16
+ def self.logger=(log)
17
+ @@logger = log
18
+ end
19
+
20
+ def self.logger
21
+ @@logger
22
+ end
23
+
24
+ # Applies current context to the templated string
25
+ def tpl(str)
26
+ eval("\"#{str}\"")
27
+ end
28
+
29
+ # Check if process is alive
30
+ def proc_alive?(pid)
31
+ Videoreg::Util.proc_alive?(pid)
32
+ end
33
+
34
+ # Cross-platform way of finding an executable in the $PATH.
35
+ #
36
+ # which('ruby') #=> /usr/bin/ruby
37
+ def which(cmd)
38
+ Videoreg::Util.which(cmd)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,55 @@
1
+ require_relative 'base'
2
+ module Videoreg
3
+ class Config < Base
4
+
5
+ def initialize
6
+ @device = '/dev/video1'
7
+ @resolution = '320x240'
8
+ @fps = 25
9
+ @duration = 10
10
+ @filename = '#{time}-#{devname}.DEFAULT.avi'
11
+ @command = 'ffmpeg -r #{fps} -s #{resolution} -f video4linux2 -r ntsc -i #{device} -vcodec mjpeg -t #{duration} -an -y #{outfile}'
12
+ @storage = '/tmp/#{devname}'
13
+ @lockfile = '/tmp/videoreg.#{devname}.DEFAULT.lck'
14
+ @store_max = 50
15
+ end
16
+
17
+ def time
18
+ "#{Time.new.strftime("%Y%m%d-%H%M%S%L")}"
19
+ end
20
+
21
+ def logger
22
+ @logger || self.class.logger
23
+ end
24
+
25
+ def store_max(*args)
26
+ (args.length > 0) ? (self.store_max=(args.shift)) : (@store_max.to_i)
27
+ end
28
+
29
+ def outfile
30
+ "#{storage}/#{filename}"
31
+ end
32
+
33
+ def base_cmd
34
+ command.split(" ").first
35
+ end
36
+
37
+ # set or get inner variable value
38
+ # depending on arguments count
39
+ def method_missing(*args)
40
+ if args.length == 2 || args[0] =~ /=$/
41
+ mname = args[0].to_s.gsub(/=/, '')
42
+ value = args.last
43
+ eval("@#{mname}='#{value}'")
44
+ elsif args.length == 1
45
+ tpl(eval("@#{args[0]}"))
46
+ else
47
+ super
48
+ end
49
+ end
50
+
51
+ def devname
52
+ device.split('/').last
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,100 @@
1
+ module Videoreg
2
+ #
3
+ # Lockfile.rb -- Implement a simple lock file method
4
+ #
5
+ class Lockfile
6
+ attr_accessor :lockfile
7
+
8
+ KEY_LENGTH = 80
9
+ @lockcode = ""
10
+
11
+ def initialize(lckf)
12
+ @lockfile = lckf
13
+ end
14
+
15
+ def lock(initial_lockcode = nil)
16
+ @initial_lockcode = initial_lockcode
17
+ if File.exists?(@lockfile)
18
+ return false
19
+ else
20
+ create
21
+ ## verify that we indeed did get the lock
22
+ verify
23
+ end
24
+ end
25
+
26
+ def verify
27
+ if not File.exists?(@lockfile)
28
+ return false
29
+ end
30
+ if readlock == @lockcode.to_s
31
+ return true
32
+ else
33
+ return false
34
+ end
35
+ end
36
+
37
+ def lockcode
38
+ readlock
39
+ end
40
+
41
+ def release
42
+ if self.verify
43
+ begin
44
+ File.delete(@lockfile)
45
+ @lockcode = ""
46
+ rescue Exception => e
47
+ return false
48
+ end
49
+ return true
50
+ else
51
+ return false
52
+ end
53
+ end
54
+
55
+ def get_or_create_key
56
+ @initial_lockcode || create_key
57
+ end
58
+
59
+ def create_key
60
+ alpha = [('a'..'z'), ('A'..'Z')].map { |i| i.to_a }.flatten
61
+ return (0..KEY_LENGTH).map { alpha[rand(alpha.length)] }.join
62
+ end
63
+
64
+ def finalize(id)
65
+ #
66
+ # Ensure lock file is erased when object dies before being released
67
+ #
68
+ File.delete(@lockfile)
69
+ end
70
+
71
+ ##-----------------##
72
+ private
73
+ ##-----------------##
74
+
75
+ def create
76
+ @lockcode = get_or_create_key
77
+ begin
78
+ g = File.open(@lockfile, "w")
79
+ g.write @lockcode
80
+ g.close
81
+ rescue Exception => e
82
+ return false
83
+ end
84
+ return true
85
+ end
86
+
87
+ def readlock
88
+ code = ""
89
+ begin
90
+ g = File.open(@lockfile, "r")
91
+ code = g.read
92
+ g.close
93
+ rescue
94
+ return ""
95
+ end
96
+ return code
97
+ end
98
+
99
+ end
100
+ end
@@ -0,0 +1,196 @@
1
+ require 'open4'
2
+ require 'pathname'
3
+
4
+ require_relative "base"
5
+
6
+ module Videoreg
7
+ class Registrar < Videoreg::Base
8
+ DELEGATE_TO_CONFIG = [:command, :outfile, :resolution, :fps, :duration, :device, :storage, :base_cmd, :store_max]
9
+ attr_reader :config
10
+
11
+ #################################
12
+ public # Public methods:
13
+
14
+ def initialize(&block)
15
+ @config = Videoreg::Config.new
16
+ @pid = nil
17
+ @halted_mutex = nil
18
+ @terminated = false
19
+ configure(&block) if block_given?
20
+ end
21
+
22
+ def configure
23
+ @thread = nil
24
+ yield @config
25
+ end
26
+
27
+ def method_missing(m)
28
+ DELEGATE_TO_CONFIG.include?(m.to_sym) ? config.send(m) : super
29
+ end
30
+
31
+ def continuous
32
+ logger.info "Starting the continuous capture..."
33
+ @terminated = false
34
+ @thread = Thread.new do
35
+ while true do
36
+ unless @halted_mutex.nil?
37
+ logger.info "Registrar (#{device}) HALTED. Waiting for the restore message..."
38
+ @halted_mutex.lock
39
+ end
40
+ unless device_exists?
41
+ logger.error "Capture failed! Device #{device} does not exist!"
42
+ terminate!
43
+ end
44
+ begin
45
+ logger.info "Cleaning old files from storage (#{storage})... (MAX: #{config.store_max})"
46
+ clean_old_files!
47
+ logger.info "Waiting for registrar (#{device}) to finish the part (#{outfile})..."
48
+ run # perform one registration
49
+ logger.info "Registrar (#{device}) has finished to capture the part (#{outfile})..."
50
+ rescue RuntimeError => e
51
+ logger.error(e.message)
52
+ logger.info "Registrar (#{device}) has failed to capture the part (#{outfile})..."
53
+ terminate!
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ def run
60
+ logger.info "Spawning a new process to capture video from device '#{device}'..."
61
+ raise "Lockfile already exists '#{config.lockfile}'..." if File.exist?(config.lockfile)
62
+ logger.info "Running the command: '#{command}'..."
63
+ raise "#{base_cmd} not found on your system. Please install it or add it to your PATH" if which(base_cmd).nil?&& !File.exists?(base_cmd)
64
+ Open4::popen4(command) do |pid, stdin, stdout, stderr|
65
+ @pid = pid
66
+ raise "Cannot lock the lock-file '#{config.lockfile}'..." unless lock(pid)
67
+ output = stdout.read + stderr.read
68
+ raise "FATAL ERROR: Cannot capture video: \n #{output}" if error?(output)
69
+ end
70
+ ensure
71
+ release
72
+ end
73
+
74
+ def pid
75
+ @pid || ((rpid = lockfile.lockcode) ? rpid.to_i : nil)
76
+ end
77
+
78
+ def clean_old_files!
79
+ all_saved_files = Dir[Pathname.new(storage).join("*#{File.extname(config.filename)}").to_s].sort_by { |c|
80
+ File.stat(c).ctime
81
+ }.reverse
82
+ if all_saved_files.length > config.store_max.to_i
83
+ all_saved_files[config.store_max.to_i..-1].each do |saved_file|
84
+ logger.info "Removing saved file #{saved_file}..."
85
+ File.unlink(saved_file) if File.exists?(saved_file)
86
+ end
87
+ end
88
+ end
89
+
90
+ def halt!
91
+ logger.info "Registrar #{device} HALTED! Killing process..."
92
+ @halted_mutex = Mutex.new
93
+ @halted_mutex.lock
94
+ kill_process!
95
+ end
96
+
97
+ def pause!
98
+ logger.info "Registrar #{device} pausing process with pid #{pid}..."
99
+ Process.kill("STOP", pid) if process_alive?
100
+ end
101
+
102
+ def recover!
103
+ logger.info "Registrar #{device} UNHALTED! Recovering process..."
104
+ @halted_mutex.unlock if @halted_mutex && @halted_mutex.locked?
105
+ @halted_mutex = nil
106
+ end
107
+
108
+ def resume!
109
+ logger.info "Registrar #{device} resuming process with pid #{pid}..."
110
+ Process.kill("CONT", pid) if process_alive?
111
+ end
112
+
113
+ # Kill just the underlying process
114
+ def kill_process!
115
+ begin
116
+ logger.info("Killing the process for #{device} : #{pid}")
117
+ Process.kill("KILL", pid) if process_alive?
118
+ Process.getpgid
119
+ rescue => e
120
+ logger.warn("An attempt to kill already killed process (#{pid}): #{e.message}")
121
+ end
122
+ end
123
+
124
+ # Kill completely
125
+ def kill!
126
+ terminate!
127
+ kill_process! if process_alive?
128
+ ensure
129
+ safe_release!
130
+ end
131
+
132
+ # Terminate the main thread
133
+ def terminate!
134
+ @terminated = true
135
+ @thread.kill if @thread
136
+ ensure
137
+ safe_release!
138
+ end
139
+
140
+ def safe_release!
141
+ release if File.exists?(config.lockfile)
142
+ end
143
+
144
+ def force_release_lock!
145
+ logger.info("Forced to release lockfile #{config.lockfile}...")
146
+ File.unlink(config.lockfile) if File.exists?(config.lockfile)
147
+ end
148
+
149
+ def device_exists?
150
+ File.exists?(device)
151
+ end
152
+
153
+ def self_alive?
154
+ process_alive? && lockfile.readlock
155
+ end
156
+
157
+ def process_alive?
158
+ !pid.to_s.empty? && pid.to_i != 0 && proc_alive?(pid)
159
+ end
160
+
161
+ def terminated?
162
+ @terminated
163
+ end
164
+
165
+ def paused?
166
+ process_alive? && (`ps -p #{pid} -o stat=`.chomp == "T")
167
+ end
168
+
169
+ #################################
170
+ private # Private methods:
171
+
172
+ def error?(output)
173
+ if output =~ /No such file or directory/ || output =~ /I\/O error occurred/ ||
174
+ output =~ /Input\/output error/ || output =~ /ioctl\(VIDIOC_QBUF\)/
175
+ output.split("\n").last
176
+ else
177
+ nil
178
+ end
179
+ end
180
+
181
+ def lockfile
182
+ @lockfile ||= Lockfile.new(config.lockfile)
183
+ end
184
+
185
+ def lock(pid)
186
+ logger.info "Locking registrar's #{config.device} (PID: #{pid}) lock file #{config.lockfile}..."
187
+ lockfile.lock(pid)
188
+ end
189
+
190
+ def release
191
+ logger.info "Releasing registrar's #{config.device} lock file #{config.lockfile}..."
192
+ lockfile.release
193
+ end
194
+
195
+ end
196
+ end