smsRuby 1.0.0-x86-linux

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,539 @@
1
+ require 'smsruby/connection'
2
+ require 'singleton'
3
+ require 'thread'
4
+ require 'logger'
5
+ require 'yaml'
6
+
7
+ #
8
+ # The AdmConnection class represent the SMS connection management layer for
9
+ # handling and controling connections (in case a pool of connections exist).
10
+ # It will also control the flow and the balance of the messages to be
11
+ # deliver through all active connections
12
+ #
13
+ class AdmConnection
14
+
15
+ include Singleton
16
+
17
+ # Represent a hash containing all connections availables to send or receive
18
+ @@connections = {}
19
+ # Reference all the consumer threads
20
+ @@consu =[]
21
+ # Reference a syncronization object to control access to critical resources
22
+ attr_reader :sync
23
+ # Represent the number of items produced into de queue
24
+ attr_reader :produced
25
+ # Represent the number of items consumed of the total produced
26
+ attr_reader :consumed
27
+ # Represent a hash that groups all SMS delivery options
28
+ attr_reader :options
29
+ # Reference the log system to register the events
30
+ attr_reader :log
31
+ # Reference the config file for smsRuby
32
+ attr_reader :config
33
+ # Represent all the possible ports
34
+ attr_reader :ports
35
+ # Specify if there is iniialized connections or not
36
+ attr_reader :avlconn
37
+
38
+
39
+ #
40
+ # Initialize the admin variables, the system log and the synchronization object
41
+ #
42
+ def initialize
43
+ @sync=Synchronize.new
44
+ @options={}
45
+ @log=Logger.new('sms.log')
46
+ @ports=[]
47
+ @produced=0
48
+ @consumed=0
49
+ @avlconn=false
50
+ end
51
+
52
+ #
53
+ # obtains all available connections an execute a code block for each connection
54
+ # found if a code block is received
55
+ #
56
+ def get_connections
57
+ @@connections.each{ |i,c| yield c if c.status!="disable"} if block_given?
58
+ return @@connections
59
+ end
60
+
61
+ #
62
+ # Set specific ports to checked. Try to open all specified connections and create.
63
+ # Raise an exception if not functional connection is found. A call to open_profile
64
+ # is made to open connections in the given ports
65
+ #
66
+ def open_ports(ports=nil)
67
+ open=false
68
+ #if RUBY_PLATFORM =~ /(win|w)32$/
69
+ #9.times { |i| @ports.push("COM#{i}")}
70
+ if RUBY_PLATFORM =~ /linux/ and ports.nil?
71
+ 9.times { |i|
72
+ @ports.push("/dev/ttyUSB#{i}")
73
+ @ports.push("/dev/ttyACM#{i}")
74
+ }
75
+ else
76
+ if ports.nil?
77
+ @log.error "No ports were specified"
78
+ raise "No ports were specified"
79
+ else
80
+ ports.each{|p| @ports.push(p)}
81
+ end
82
+ end
83
+ open = open_profiles
84
+ @log.error "No active connections found or available connections fail" unless open
85
+ raise 'No active connections found or available connections fail. See log for details' unless open
86
+ end
87
+
88
+ #
89
+ # Used to open connections and load configuration file. Return true if at least
90
+ # one connection could be open satisfactorily, false otherwise
91
+ #
92
+ def open_profiles
93
+ begin
94
+ open=false
95
+ save_file(@ports)
96
+ path = 'config_sms.yml'
97
+ parse=YAML::parse(File.open(path))
98
+ @config=parse.transform
99
+ @ports.size.times do |i|
100
+ open=open_conn("telf"+i.to_s,@ports[i]) || open
101
+ end
102
+ @avlconn=true if open
103
+ open
104
+ rescue Exception => e
105
+ @log.error "Error openning connections. Detail #{e.message}"
106
+ end
107
+ end
108
+
109
+ #
110
+ # Check if a new connection had been attach.
111
+ #
112
+ def update_connections
113
+ begin
114
+ @@connections.each{ |i,c| t=c.test; c.status = "disable" if (t!=0 and t!=22 and t!=23)}
115
+ @ports.select{ |i| (!@@connections.inject(false){|res,act| (act[1].port == i and act[1].status!="disable") || res }) }.each{|i|
116
+ open_conn("telf"+@ports.index(i).to_s,i)
117
+ }
118
+ rescue Exception => e
119
+ puts "An error has occurred updating connections. Exception:: #{e.message}\n"
120
+ @log.error "An error has occurred updating connections. Exception:: #{e.message}" unless @log.nil?
121
+ end
122
+ end
123
+
124
+ #
125
+ # reload the configuration file to update changes
126
+ #
127
+ def reload_file
128
+ parse=YAML::parse(File.open('config_sms.yml'))
129
+ @config=parse.transform
130
+ end
131
+
132
+ #
133
+ # Save the configuration file used for gnokii to load phone profiles and open
134
+ # connections. The required information is specifyed in array
135
+ #
136
+ def save_file (array)
137
+ begin
138
+ i = 0
139
+ if RUBY_PLATFORM =~ /(win|w)32$/
140
+ path = ENV['userprofile']+'/_gnokiirc'
141
+ elsif RUBY_PLATFORM =~ /linux/
142
+ path = ENV['HOME']+'/.gnokiirc'
143
+ end
144
+
145
+ File.open(path, 'w') do |f2|
146
+ array.each do |pos|
147
+ f2.puts "[phone_telf" + i.to_s + "]"
148
+ f2.puts "port = " + pos
149
+ f2.puts "model = AT"
150
+ f2.puts "connection = serial"
151
+ i = i+1
152
+ end
153
+ f2.puts "[global]"
154
+ f2.puts "port = COM3"
155
+ f2.puts "model = AT"
156
+ f2.puts "connection = serial"
157
+ f2.puts '[logging]'
158
+ f2.puts 'debug = off'
159
+ f2.puts 'rlpdebug = off'
160
+ end
161
+ rescue SystemCallError
162
+ @log.error "Problem writing the configuration file" unless @log.nil?
163
+ raise "Problem writing the configuration file"
164
+ end
165
+ end
166
+
167
+ #
168
+ # Initialize a new connection with the specifyed name and port and add it to the
169
+ # connections hash, wich holds all available connections. If an exception is
170
+ # thrown an error will be register in the system log
171
+ #
172
+ def open_conn(name,port)
173
+ begin
174
+ open=true
175
+ n = @@connections.size
176
+ con = Connection.new(name,port)
177
+ (@config['send']['imeis']).each { |item|con.typec = 's' if con.phone_imei.eql?(item.to_s)}
178
+ (@config['receive']['imeis']).each {|item|
179
+ (con.typec == 's' ? con.typec ='sr' : con.typec = 'r') if con.phone_imei.eql?(item.to_s)
180
+ }
181
+ con.typec = 's' if (con.typec!= 'r' and con.typec!='sr')
182
+ puts ":: satisfactorily open a connection in port #{port} imei is #{con.phone_imei} and connection type is: #{con.typec} ::\n"
183
+ @@connections.merge!({n => con})
184
+ rescue ErrorHandler::Error => e
185
+ #@log.info "Openning connection in #{port}.. #{e.message} Not Device Found " unless @log.nil?
186
+ open=false
187
+ rescue Exception => e
188
+ @log.info "Openning connection in port #{port}.. #{e.message}" unless @log.nil?
189
+ open=false
190
+ end
191
+ open
192
+ end
193
+
194
+ #
195
+ # The internal send function for the Connection Administrator. The config value
196
+ # specify the option values for the SMS messages to be send. It starts producer
197
+ # and consumers to distribute the messages to the diferent available connections. A
198
+ # recovery send is started if at least one message fail to deliver in the first attempt.
199
+ #
200
+ def send(config)
201
+ @options = config
202
+ config[:dst].delete_if {|x| (!check_phone(x) and (@log.error "Incorrect phone number format for #{x}"if !check_phone(x)))} if !config[:dst].empty?
203
+ if !config[:dst].empty?
204
+ bool= !(@@connections.inject(true){|res,act| (act[1].status == "disable" or (act[1].typec=='r' or (act[1].typec=='sr' and act[1].status=="receiving"))) and res }) if !@@connections.empty?
205
+ if !@@connections.empty? and bool
206
+ @log.info "Starting send.. #{config[:dst].size} messages to be sent. " unless @log.nil?
207
+ prod = Thread.new{producer(config[:dst])}
208
+ conn=Thread.new{verify_connection(5)}
209
+ pos=0
210
+ @@connections.each do |i,c|
211
+ unless c.typec=='r' or (c.typec=='sr' and c.status=="receiving") or c.status =="disable"
212
+ @@consu[pos] = Thread.new{consumer(i,config[:dst].size)}
213
+ pos+=1
214
+ end
215
+ end
216
+ prod.join
217
+ @@consu.each { |c|
218
+ (c.stop? and c[:wfs]==1) ? (@@connections[c[:connid]].status="available"; c.exit) : c.join
219
+ }
220
+ @@consu.clear
221
+ unless @sync.eq.empty?
222
+ pos=0
223
+ @@connections.each do |i,c|
224
+ unless c.typec=='r' or (c.typec=='sr' and c.status=="receiving" ) or c.status =="disable"
225
+ @@consu[pos]=Thread.new{send_emergency(i,config[:dst].size)}
226
+ pos+=1
227
+ end
228
+ end
229
+ @@consu.each { |c|
230
+ (c.stop? and c[:wfs]==1) ? (@@connections[c[:connid]].status="available"; c.exit) : c.join
231
+ }
232
+ check=0
233
+ while(!@sync.eq.empty?)
234
+ check=1
235
+ @log.error "Message: #{config[:msj][0,15]}... to #{@sync.eq.pop} couldn't be sent." unless @log.nil?
236
+ end
237
+ conn.exit
238
+ error = "Message #{config[:msj][0,15]}... couldn't be sent to some destinations. See log for details." if !check
239
+ yield error if block_given?
240
+ raise error if check
241
+ end
242
+ conn.exit
243
+ else
244
+ warn = "Message: #{config[:msj][0,15]}... couldn't be sent. There are no active or available to send connections"
245
+ @log.warn warn unless @log.nil?
246
+ yield warn if block_given?
247
+ raise warn
248
+ end
249
+ else
250
+ warn = "Message: #{config[:msj][0,15]}... couldn't be sent. Bad format or no destination number were specified"
251
+ @log.warn warn unless @log.nil?
252
+ yield warn if block_given?
253
+ raise warn
254
+ end
255
+ end
256
+
257
+ #
258
+ # Put all destination numbers (produce) into a shared buffer. A synchronization
259
+ # with all active consumers is required to avoid data loss and incoherence. The
260
+ # buffer has a limited size, so is up to the producer to handle this matter
261
+ #
262
+ def producer(dest)
263
+ dest.each do |i|
264
+ begin
265
+ @sync.mutex.synchronize{
266
+ @sync.full.wait(@sync.mutex) if (@sync.count == @sync.max)
267
+ @sync.queue.push i.to_s
268
+ #puts "Producer: #{i} produced"+"\n"
269
+ @sync.mutexp.synchronize{
270
+ @produced += 1
271
+ }
272
+ @sync.empty.signal if @sync.count == 1
273
+ }
274
+ end
275
+ end
276
+ end
277
+
278
+ #
279
+ # Extract a destination number from the shared buffer and passed along with SMS
280
+ # message option values to the excetute connection function. A synchronization
281
+ # with the producer and all other consumers is required to avoid data loss and
282
+ # incoherence
283
+ #
284
+ def consumer(n,max)
285
+ Thread.current[:wfs]=0
286
+ Thread.current[:type]='s'
287
+ Thread.current[:connid]=n
288
+ loop do
289
+ @sync.mutexp.synchronize{
290
+ (@@connections[n].status="available"; Thread.exit) if (@produced >= max && @sync.queue.empty?)
291
+ }
292
+ begin
293
+ @sync.mutex.synchronize{
294
+ while (@sync.count == 0)
295
+ Thread.current[:wfs]=1
296
+ @sync.empty.wait(@sync.mutex)
297
+ Thread.current[:wfs]=0
298
+ end
299
+ Thread.current[:v] = @sync.queue.pop
300
+ #puts ":: Consumer: in connection #{n} #{Thread.current[:v]} consumed \n"
301
+ @sync.full.signal if (@sync.count == (@sync.max - 1))
302
+ }
303
+ retryable(:tries => 2, :on => ErrorHandler::Error) do
304
+ @@connections[n].execute(to_hash(Thread.current[:v].to_s))
305
+ end
306
+ @consumed+=1
307
+ @log.info "Message: #{@options[:msj][0,15]}... to #{Thread.current[:v].to_s} sent succsefull from connection with imei #{@@connections[n].phone_imei}." unless @log.nil?
308
+ rescue ErrorHandler::Error => ex
309
+ @log.error "Connection in port #{@@connections[n].port} fail sending message to #{Thread.current[:v]}, Message sent to emergency queue. Exception:: #{ex.message}" unless @log.nil?
310
+ @sync.eq.push(Thread.current[:v])
311
+ rescue Exception => ex
312
+ @log.error "Connection in port #{@@connections[n].port} fail sending message to #{Thread.current[:v]}. Exception:: #{ex.message}" unless @log.nil?
313
+ end
314
+ end
315
+ end
316
+
317
+ #
318
+ # Handles all unsend messages from the consumers due to diferent exceptions (no
319
+ # signal in phone, error in sim card..). Try to send the messages recovering it
320
+ # from an emergency queue and discarting it only if none of the active connections
321
+ # is able to send the message.
322
+ #
323
+ def send_emergency(n,max)
324
+ Thread.current[:wfs]=0
325
+ Thread.current[:type]='s'
326
+ Thread.current[:connid]=n
327
+ loop do
328
+ begin
329
+ @sync.mutexe.synchronize{
330
+ if (@sync.eq.size == 0 and @consumed < max)
331
+ Thread.current[:wfs] = 1
332
+ @sync.emptye.wait(@sync.mutexe)
333
+ Thread.current[:wfs] = 0
334
+ elsif (@consumed == max)
335
+ @@connections[n].status="available";
336
+ Thread.exit
337
+ end
338
+ unless @sync.eq.empty?
339
+ Thread.current[:v]=@sync.eq.pop
340
+ retryable(:tries => 2, :on => ErrorHandler::Error) do
341
+ @@connections[n].execute(to_hash(Thread.current[:v].to_s))
342
+ end
343
+ @consumed+=1
344
+ @log.info "EMessage: #{@options[:msj][0,15]}... to #{Thread.current[:v].to_s} sent succsefull from connection with imei #{@@connections[n].phone_imei}." unless @log.nil?
345
+ p ':: Emergency message consumed '+(max-@consumed).to_s+' left'
346
+ (Thread.list.each{|t| @sync.emptye.signal if (t!=Thread.current and t!=Thread.main and t[:wfs]==1)}) if @consumed == max
347
+ end
348
+ }
349
+ rescue Exception => e
350
+ @sync.mutexe.synchronize{
351
+ @log.error "Connection in port #{@@connections[n].port} fail sending message to #{Thread.current[:v]} at emergency function. Exception:: #{e.message}" unless @log.nil?
352
+ @sync.eq << Thread.current[:v]
353
+ @sync.emptye.signal if @sync.eq.size==1
354
+ }
355
+ @@connections[n].status="available";
356
+ Thread.exit
357
+ end
358
+ end
359
+ end
360
+
361
+ #
362
+ # Check if a new connection had been attach while sending a message. This allows
363
+ # to start new consumers dinamically and increase the performance
364
+ #
365
+ def verify_connection(seconds)
366
+ begin
367
+ n=0
368
+ verify(0,seconds){
369
+ @ports.select{ |i| (!@@connections.inject(false){|res,act| (act[1].port == i) || res }) }.each{|i|
370
+ n=@@connections.size
371
+ if open_conn("telf"+@ports.index(i).to_s,i)
372
+ unless @@connections[n].typec=='r' or (@@connections[n].typec=='sr' and @@connections[n].status="receiving")
373
+ @@consu[n]=Thread.new{consumer(n,@options[:dst].size)}
374
+ end
375
+ end
376
+ }
377
+ }
378
+ rescue Exception => e
379
+ puts "An error has occurred during execution of verify connection function. Exception:: #{e.message}"
380
+ @log.error "An error has occurred during execution of verify connection function. Exception:: #{e.message}" unless @log.nil?
381
+ end
382
+ end
383
+
384
+ #
385
+ # Excecute every "seconds" for a period of "total" seconds a given code block.
386
+ # If "total" is 0 the function will loop forever
387
+ #
388
+ def verify(total,seconds)
389
+ start_total=Time.now
390
+ loop do
391
+ start_time = Time.now
392
+ puts "Task started. #{start_time}"
393
+ yield
394
+ time_spent=Time.now - start_time
395
+ puts "Task donde. #{Time.now}"+ "and spend #{time_spent}"
396
+ break if ((Time.now - start_total) >= total and total != 0)
397
+ sleep(seconds - time_spent) if time_spent < seconds
398
+ end
399
+ end
400
+
401
+ #
402
+ # The internal receive function for the Connection Administrator.
403
+ #
404
+ def receive(hash)
405
+ begin
406
+ conn=nil
407
+ @@connections.each{|i,c|
408
+ if c.phone_imei==hash[:imei]
409
+ @log.info "Start receiving messages from connection with imei: #{hash[:imei]}." unless log.nil?
410
+ conn=c
411
+ break
412
+ end
413
+ }
414
+ unless !conn
415
+ if hash[:receivetype]==0
416
+ verify(hash[:time],10){
417
+ list = conn.execute(hash)
418
+ unless !list
419
+ list.each do |msj|
420
+ @log.info "Message received in connection with imei #{conn.phone_imei} from #{msj.source_number}. #{msj.text}."
421
+ yield msj,conn.phone_imei if block_given?
422
+ end
423
+ end
424
+ }
425
+ else
426
+ list = conn.execute(hash)
427
+ unless !list
428
+ list.each do |msj|
429
+ @log.info "Message received in connection with imei #{conn.phone_imei} from #{msj.source_number}. #{msj.text}."
430
+ yield msj if block_given?
431
+ end
432
+ end
433
+ end
434
+ conn.status='available'
435
+ end
436
+ rescue ErrorHandler::Error => e
437
+ error = "Fail to receive more messages from connecion with imei #{hash[:imei]}. Detail: #{e.message}"
438
+ @log.error error unless @log.nil?
439
+ conn.status='available'
440
+ raise error
441
+ rescue Exception => e
442
+ error = "Exception receiving messages from connecion with imei #{hash[:imei]}. Detail: #{e.message}"
443
+ @log.error error unless @log.nil?
444
+ conn.status='available'
445
+ raise error
446
+ end
447
+ end
448
+
449
+ #
450
+ # Handles retry for a particular code block. The default numbers of retrys is set
451
+ # to 1. The retry will be executed on any exception unless a type of error is
452
+ # specified
453
+ #
454
+ def retryable(options = {}, &block)
455
+
456
+ opts = { :tries => 1, :on => Exception }.merge(options)
457
+
458
+ retry_exception, retries = opts[:on], opts[:tries]
459
+
460
+ begin
461
+ return yield
462
+ rescue retry_exception
463
+ retry if (retries -= 1) > 0
464
+ end
465
+ yield
466
+ end
467
+
468
+ #
469
+ # Check the validity of the phone number format
470
+ #
471
+ def check_phone(phone)
472
+ phone_re = /^(\+\d{1,3}\d{3}\d{7})|(0\d{3})\d{7}$/
473
+ m = phone_re.match(phone.to_s)
474
+ m ? true : false
475
+ end
476
+
477
+ #
478
+ # Combine all option values into a hash to relate them
479
+ #
480
+ def to_hash(num)
481
+ { :type => 'send',
482
+ :dst => num,
483
+ :msj => self.options[:msj],
484
+ :smsc =>self.options[:smsc],
485
+ :report => self.options[:report],
486
+ :validity => self.options[:validity]}
487
+ end
488
+ end
489
+
490
+
491
+ #
492
+ # The Synchronize class contains all required variables to handle synchronization
493
+ # between producer - consumers and to protect critical resourses from concurrent
494
+ # access.
495
+ #
496
+ class Synchronize
497
+
498
+ # Handle mutual exclution for queue
499
+ attr_accessor :mutex
500
+ # Handle mutual exclution for the produced variable (protect the variable)
501
+ attr_accessor :mutexp
502
+ # Handle mutual exclution for eq
503
+ attr_accessor :mutexe
504
+ # Represent the condition variable for handling an empty queue "queue"
505
+ attr_accessor :empty
506
+ # Represent the condition variable for handling an empty queue "eq"
507
+ attr_accessor :emptye
508
+ # Represent the condition variable for handling a full queue "queue"
509
+ attr_accessor :full
510
+ # Reference a queue that contains all produced items by the producer
511
+ attr_accessor :queue
512
+ # Reference a queue that contains destination numbers to wich the SMS messages couldn't be send
513
+ attr_accessor :eq
514
+ # Represent the max number of items that queue can retain
515
+ attr_accessor :max
516
+
517
+ #
518
+ # initialize all variables for the class
519
+ #
520
+ def initialize
521
+ @mutex = Mutex.new
522
+ @mutexp=Mutex.new
523
+ @mutexe=Mutex.new
524
+ @empty = ConditionVariable.new
525
+ @emptye = ConditionVariable.new
526
+ @full = ConditionVariable.new
527
+ @queue = Queue.new
528
+ @eq = Queue.new
529
+ @max = 10
530
+ end
531
+
532
+ #
533
+ # Get the number of items for the queue "queue"
534
+ #
535
+ def count
536
+ @queue.size
537
+ end
538
+
539
+ end