videoreg 0.1

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/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