smsRuby 1.0.0-x86-linux

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.
@@ -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