PageRenderTime 59ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 0ms

/auiserver/new_server.rb

https://gitlab.com/jontow/cpx
Ruby | 763 lines | 607 code | 79 blank | 77 comment | 52 complexity | c011ce4b9f45afed8197071dd1f5b639 MD5 | raw file
  1. =begin license
  2. * Copyright (c) 2006-2008, Fused Solutions, LLC
  3. * All rights reserved.
  4. *
  5. * Redistribution and use in source and binary forms, with or without
  6. * modification, are permitted provided that the following conditions are met:
  7. * * Redistributions of source code must retain the above copyright
  8. * notice, this list of conditions and the following disclaimer.
  9. * * Redistributions in binary form must reproduce the above copyright
  10. * notice, this list of conditions and the following disclaimer in the
  11. * documentation and/or other materials provided with the distribution.
  12. * * Neither the name of the Fused Solutions, LLC nor the
  13. * names of its contributors may be used to endorse or promote products
  14. * derived from this software without specific prior written permission.
  15. *
  16. * THIS SOFTWARE IS PROVIDED BY Fused Solutions, LLC ``AS IS'' AND ANY
  17. * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  18. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  19. * DISCLAIMED. IN NO EVENT SHALL Fused Solutions, LLC BE LIABLE FOR ANY
  20. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  21. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  22. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  23. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  24. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  25. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  26. =end
  27. $0 = 'auiserver'
  28. $LOAD_PATH << File.dirname(__FILE__)
  29. require 'thread'
  30. require 'mysql'
  31. require 'timeout'
  32. require 'uri'
  33. Thread.abort_on_exception = true
  34. load 'version.rb'
  35. begin
  36. load 'config/server.conf'
  37. rescue LoadError => e
  38. STDERR.puts "Error when loading server config file, does config/server.conf exist?"
  39. STDERR.puts "Error message: #{e.message}"
  40. exit(1)
  41. rescue SyntaxError => e
  42. STDERR.puts "Syntax error encountered when loading config/server.conf"
  43. STDERR.puts "Error message: #{e.message}"
  44. exit(2)
  45. end
  46. require 'constants/protocol'
  47. require 'cpxlog'
  48. Cpxlog.open
  49. require 'auth'
  50. require 'cdr'
  51. require 'agents'
  52. require 'queues'
  53. require 'server_events'
  54. require 'client_events'
  55. class SqlConnection
  56. def initialize
  57. begin
  58. connect
  59. rescue MysqlError => e
  60. Cpxlog.err("#{e.errno} #{e.error}")
  61. exit(1)
  62. end
  63. end
  64. def safe_query(querystring)
  65. retries = 0
  66. begin
  67. @dbh.query(querystring)
  68. rescue Mysql::Error => e
  69. case e.errno
  70. when 2006 # MySQL timeout error code
  71. if connect and retries == 0
  72. Cpxlog.warning("MySQL connection timed out, trying to reconnect #{e.errno} #{e.error}")
  73. connect
  74. retries += 1
  75. retry #try again
  76. else
  77. Cpxlog.err("#{e.errno} #{e.error}")
  78. puts e.backtrace
  79. exit(1)
  80. end
  81. else
  82. Cpxlog.err("#{e.errno} #{e.error} -- #{querystring}")
  83. puts e.backtrace
  84. exit(1)
  85. end
  86. end
  87. end
  88. def connect
  89. @dbh = Mysql.real_connect(ServerConfig::MYSQL_CREDENTIALS[:host],
  90. ServerConfig::MYSQL_CREDENTIALS[:username],
  91. ServerConfig::MYSQL_CREDENTIALS[:password],
  92. ServerConfig::MYSQL_CREDENTIALS[:database])
  93. end
  94. def quote(string)
  95. @dbh.quote(string.to_s)
  96. end
  97. end
  98. class Server
  99. def self.new(*args)
  100. if @instance
  101. raise RuntimeError, "Only one server can be run at a time", caller
  102. end
  103. @instance = super(*args)
  104. end
  105. def self.instance
  106. @instance
  107. end
  108. attr_reader :startuptime
  109. def initialize
  110. @startuptime = Time.now
  111. @sockets = []
  112. @salt = {}
  113. end
  114. def connect
  115. Cpxlog.info "Welcome to CPX Agent Server version #{VERSIONSTRING}"
  116. @eventserver = EventServer.new(ServerConfig::AMI_CREDENTIALS[:host], ServerConfig::AMI_CREDENTIALS[:port])
  117. @eventserver.connect
  118. banner = @eventserver.socket.gets
  119. unless banner.chomp.include?('CBX')
  120. STDERR.puts "Invalid CBX banner received: \"#{banner.chomp}\", disconnecting"
  121. exit(-1)
  122. end
  123. Cpxlog.info "Successfully connected to CBX at #{ServerConfig::AMI_CREDENTIALS[:host]}"
  124. @dbh = SqlConnection.new
  125. CDR.connect(@dbh)
  126. @auth = MySQLAuth.new(@dbh)
  127. Agent.sql = @dbh
  128. EventServer.dbh = @dbh
  129. # request bootstrapping info
  130. @eventserver.send_event('Login', 'Username'=>ServerConfig::AMI_CREDENTIALS[:username], 'Password'=>ServerConfig::AMI_CREDENTIALS[:password]) do |reply|
  131. if reply.success?
  132. @eventserver.send_event('Agents') do |reply|
  133. if reply.success?
  134. @eventserver.send_event('QueueStatus') do |reply|
  135. if reply.success?
  136. # use the response to the ping event to determine when the boostrapping is done
  137. @eventserver.send_event('Uptime') do |reply|
  138. if reply.success?
  139. @eventserver.startuptime = Time.at(reply.startuptime.to_i)
  140. Cpxlog.info("CBX startuptime is #{@eventserver.startuptime}")
  141. CDR.handle_orphaned(@eventserver)
  142. @clientserver = TCPServer.new(ServerConfig::LISTEN_HOST, ServerConfig::LISTEN_PORT)
  143. accept_connections #more or less ready to listen for clients
  144. Cpxlog.info("Ready for action!")
  145. else
  146. Cpxlog.err("'Uptime' AMI event failed?!: @#{ServerConfig::AMI_CREDENTIALS[:host]}:#{ServerConfig::AMI_CREDENTIALS[:port]} #{reply.message}")
  147. exit(1)
  148. end
  149. end
  150. else
  151. Cpxlog.err("'QueueStatus' AMI event failed: @#{ServerConfig::AMI_CREDENTIALS[:host]}:#{ServerConfig::AMI_CREDENTIALS[:port]} #{reply.message}")
  152. exit(1)
  153. end
  154. end
  155. else
  156. Cpxlog.err("'Agents' AMI event failed: @#{ServerConfig::AMI_CREDENTIALS[:host]}:#{ServerConfig::AMI_CREDENTIALS[:port]} #{reply.message}")
  157. exit(1)
  158. end
  159. end
  160. else
  161. Cpxlog.err("Failed to connect to the AMI @#{ServerConfig::AMI_CREDENTIALS[:host]}:#{ServerConfig::AMI_CREDENTIALS[:port]} #{reply.message}")
  162. exit(1)
  163. end
  164. end
  165. listen_input
  166. end
  167. def accept_connections
  168. Thread.new do
  169. loop do
  170. socket = @clientserver.accept
  171. remoteaddr = socket.peeraddr[2]
  172. begin
  173. Timeout.timeout(5) do
  174. socket.puts "Agent Server: #{VERSIONSTRING}"
  175. end
  176. Timeout.timeout(5) do
  177. line = socket.gets
  178. next if line.nil?
  179. if md = /^Protocol: ([1-9])\.([0-9]+)(.*)$/.match(line.chomp)
  180. if AgentProtocol::MAJOR != md[1].to_i
  181. socket.puts "2 Protocol major version mismatch. Login denied"
  182. Cpxlog.notice "Client connecting from #{socket.peeraddr[2]} is using an incompatible client"
  183. socket.close
  184. elsif AgentProtocol::MINOR > md[2].to_i
  185. socket.puts "1 You are using an outdated client. Please visit #{ServerConfig::AGENTUIURL} to upgrade your client"
  186. Cpxlog.notice "Client connecting from #{socket.peeraddr[2]} is using an outdated client"
  187. elsif AgentProtocol::MINOR < md[2].to_i
  188. socket.puts "1 Protocol minor version mismatch. Please consider downgrading your client"
  189. Cpxlog.notice "Client connecting from #{socket.peeraddr[2]} is using an client newer than the server"
  190. else
  191. socket.puts "0 OK #{ServerConfig::CRMURL}"
  192. end
  193. else
  194. socket.puts "2 Invalid Response. Login denied"
  195. Cpxlog.notice "Client connecting from #{socket.peeraddr[2]} sent an invalid response to the version string: '#{line.chomp}'"
  196. socket.close
  197. end
  198. end
  199. rescue Errno::ECONNRESET, Errno::EPIPE, IOError => e
  200. Cpxlog.warning("Failed to send versionstring to newly connected client(#{remoteaddr}): #{e.class.name}: #{e.message}")
  201. socket.close
  202. rescue Timeout::Error
  203. Cpxlog.warning("Failed to send versionstring to newly connected client #{remoteaddr} (timed out after 5 seconds)")
  204. socket.close
  205. else
  206. @sockets << socket
  207. end
  208. end
  209. end
  210. end
  211. def listen_input
  212. loop do
  213. sockets = (@sockets+[@eventserver.socket]).compact
  214. sockets.select{|x| x.closed?}.each do |x|
  215. @sockets.delete x
  216. @salt.delete x
  217. end
  218. sockets = sockets.reject{|x| x.closed?}
  219. if result = select(sockets, nil, nil , 0.25)
  220. input, output, err = result
  221. input.each do |i|
  222. if i == @eventserver.socket
  223. handle_server_event(@eventserver)
  224. next
  225. end
  226. begin
  227. line = nil
  228. Timeout.timeout(5) do
  229. line = i.gets
  230. end
  231. rescue Errno::EPIPE, Errno::ECONNRESET, Errno::ETIMEDOUT, IOError
  232. if agent = Agent[i]
  233. terminate_agent(agent)
  234. end
  235. @sockets.delete i
  236. @salt.delete i
  237. next
  238. rescue Timeout::Error
  239. if agent = Agent[i]
  240. Cpxlog.err("Agent #{agent} from #{i.peeraddr[2]} timed out after 5 seconds while reading from their socket")
  241. terminate_agent(agent)
  242. else
  243. Cpxlog.err("Unknown connection from #{i.peeraddr[2]} timed out after 5 seconds while reading")
  244. end
  245. @sockets.delete i
  246. @salt.delete i
  247. next
  248. end
  249. if agent = Agent[i] and line
  250. handle_agent_event(agent, line)
  251. elsif line
  252. # unauthenticated agent, we have to get a bit dirty here
  253. begin
  254. if line.split(' ', 2)[0] == 'LOGIN'
  255. login_agent(line, i)
  256. elsif line.split(' ', 2)[0] == 'GETSALT'
  257. allocate_salt(line, i)
  258. else
  259. counter = line.split(' ')[1]
  260. i.puts "ERR #{counter} This is an unauthenticated connection, the only permitted actions are GETSALT and LOGIN"
  261. end
  262. rescue Errno::ECONNRESET, Errno::EPIPE, IOError
  263. begin
  264. remoteaddr = i.peeraddr[2]
  265. rescue Errno::ENOTCONN
  266. remoteaddr = "<disconnected>"
  267. end
  268. if agent = Agent[i]
  269. terminate_agent(agent)
  270. Cpxlog.warning("Agent #{agent} at #{remoteaddr} disconnected before response returned")
  271. else
  272. Cpxlog.warning("Unknown connection from #{remoteaddr} disconnected before response returned")
  273. end
  274. @sockets.delete i
  275. @salt.delete i
  276. end
  277. else
  278. #dead connection
  279. if agent = Agent[i]
  280. terminate_agent(agent)
  281. end
  282. @sockets.delete i
  283. @salt.delete i
  284. end
  285. end
  286. end
  287. Agent.match_agents(:online=>true) do |agent|
  288. agent.handle_unreplied(60) do |event|
  289. if event[:retries] < 2
  290. Cpxlog.warning("resending event #{event[:counter]} to #{agent} (#{event[:event]})")
  291. agent.outputqueue << event[:event]
  292. event[:retries] += 1
  293. event[:time] = Time.now
  294. else
  295. # expired event
  296. Cpxlog.warning("Resent event #{event[:counter]} to #{agent} twice and received no reply. Disconnecting agent.")
  297. #agent.expire_event event
  298. terminate_agent(agent)
  299. agent.socket.close unless agent.socket.closed?
  300. @sockets.delete agent.socket
  301. @salt.delete agent.socket
  302. break
  303. end
  304. end
  305. while !agent.outputqueue.empty?
  306. begin
  307. agent.socket.puts agent.outputqueue.pop
  308. rescue Errno::EPIPE, Errno::ECONNRESET, IOError
  309. Cpxlog.warning("connection reset when sending to #{agent}")
  310. terminate_agent(agent)
  311. @sockets.delete agent.socket
  312. @salt.delete agent.socket
  313. break
  314. end
  315. end
  316. end
  317. while !@eventserver.outputqueue.empty?
  318. # send cbx anything queued for it
  319. x = @eventserver.outputqueue.pop
  320. begin
  321. @eventserver.socket.puts x
  322. rescue Errno::ECONNRESET, Errno::EPIPE, IOError
  323. Cpxlog.err "Connection to CBX terminated unexpectedly when writing"
  324. exit(1)
  325. end
  326. end
  327. end
  328. end
  329. def handle_server_event(eventserver)
  330. begin
  331. # pull a line out of the queue for processing
  332. line = eventserver.socket.gets("\r\n\r\n")
  333. rescue Errno::ECONNRESET, Errno::EPIPE, IOError
  334. # NOTE-- The eventserver is busted.
  335. Cpxlog.err "Connection to CBX terminated unexpectedly when reading"
  336. exit(1)
  337. end
  338. if line
  339. begin
  340. event = ServerEventFactory.eventfactory(eventserver, line)
  341. rescue ServerEvent::UnknownEventError => e
  342. # Cpxlog.warning("Received unknown event from AMI; #{e.message} (#{e.backtrace[0]})")
  343. else
  344. begin
  345. event.dispatch
  346. rescue ServerEvent::InvalidEventError => e
  347. Cpxlog.warning "Received invalid event from CBX; #{e.message} (#{e.backtrace[1]})"
  348. end
  349. end
  350. else
  351. # NOTE-- eventserver died horribly.
  352. if eventserver.shutdown?
  353. Cpxlog.notice "Connection to CBX closed"
  354. exit
  355. else
  356. Cpxlog.err "Connection to CBX terminated unexpectedly"
  357. exit(1)
  358. end
  359. end
  360. end
  361. def handle_agent_event(agent, line)
  362. # try to create the event
  363. begin
  364. event = ClientEventFactory.eventfactory(agent, @eventserver, line.chomp)
  365. rescue ClientEvent::InsufficientPermissionError => e
  366. counter = line.split(' ')[1]
  367. agent.send_err(counter, e.message)
  368. Cpxlog.warning "Agent #{agent} tried to execute '#{line.chomp}' but they lack permissions to do so"
  369. rescue ClientEvent::UnknownEventError => e
  370. counter = line.split(' ')[1]
  371. agent.send_err(counter, e.message)
  372. Cpxlog.warning "Agent #{agent} sent an unknown event: '#{line.chomp}'"
  373. rescue ClientEvent::IllegalEventError => e
  374. #there's no counter, we're kinda hosed...
  375. agent.send_err(1, e.message)
  376. Cpxlog.warning "Agent #{agent} sent an illegal event: '#{line.chomp}'; #{e.message}"
  377. else
  378. # try dispatching the event
  379. begin
  380. event.dispatch
  381. rescue ClientEvent::InvalidEventError => e
  382. agent.send_err(event.counter, e.message)
  383. Cpxlog.warning "Agent #{agent} sent an invalid event: '#{line.chomp}'; #{e.message}"
  384. else
  385. agent.send_ack(event.counter, event.retdata) if event.ack?
  386. end
  387. end
  388. end
  389. def allocate_salt(line, socket)
  390. salt = rand(4294967295) # 32 bit random number
  391. command, counter, data = line.strip.split(' ', 3)
  392. begin
  393. counter = Integer(counter)
  394. rescue ArgumentError
  395. socket.puts "ERR invalid counter #{counter}"
  396. return
  397. end
  398. socket.puts("ACK #{counter} #{salt}")
  399. @salt[socket] = salt
  400. end
  401. def login_agent(line, socket)
  402. command, counter, data = line.strip.split(' ', 3)
  403. begin
  404. counter = Integer(counter)
  405. rescue ArgumentError
  406. socket.puts "ERR invalid counter #{counter}"
  407. return
  408. end
  409. unless @salt[socket]
  410. socket.puts "ERR #{counter} Please request a salt with GETSALT first"
  411. return
  412. end
  413. logincreds, remotenumber = data.split(' ', 2)
  414. username, password = logincreds.split(':', 2)
  415. if username and password and credentials = @auth.authenticate(username, password, @salt[socket])
  416. credentials[:id] = credentials[:id]+ServerConfig::BASEID
  417. agent = Agent[credentials[:id]]
  418. if remotenumber and !remotenumber.empty?
  419. if [4, 7, 10, 11].include? remotenumber.length and remotenumber =~ /^[0-9]+$/
  420. remdial = "${GLOBAL(TRUNK)}/#{remotenumber}"
  421. elsif remotenumber =~ /^([\w\d\.]+)\:([0-9]+)$/
  422. remdial = "SIP/#{remotenumber}"
  423. else
  424. socket.puts "ERR #{counter} Remote number must be 4, 7, 10 or 11 digits and fully numeric (not #{remotenumber})"
  425. return
  426. end
  427. else
  428. remdial = false
  429. end
  430. if agent and agent.socket
  431. # TODO: ping agent, are they still up and working?
  432. Cpxlog.warning "Agent #{agent} is already marked as logged in"
  433. socket.puts "ERR #{counter} This agent is already logged in"
  434. else
  435. queues = []
  436. @eventserver.send_event('AgentCallbackLogin', {'Agent'=>credentials[:id], 'Exten'=>credentials[:id], 'Context'=>'sip-agents', 'AckCall'=>ServerConfig::ACKCALL.to_s, 'WrapupTime'=>'0'}) do |reply|
  437. if reply.success?
  438. # NOTE: there is a possible inconsistancy in the agent name depending on if the agent
  439. # was populated from the agent list or is newly created. If the agent was created from
  440. # the agent list from the AMI, the agent name will be the one in the agent name table,
  441. # otherwise the name is the name used to login (the one from the authentication table)
  442. unless agent
  443. agent = Agent.new(@eventserver, credentials[:id], socket)
  444. agent.name = username
  445. else
  446. agent.set_socket socket
  447. end
  448. # populate agent dialplan
  449. # TODO - remote agent callback authentication...?
  450. if ! remdial
  451. diallocation = "SIP/#{username}"
  452. else
  453. diallocation = remdial
  454. end
  455. =begin
  456. if remotenumber and !remotenumber.empty?
  457. diallocation = "${GLOBAL(TRUNK)}/#{remotenumber}"
  458. else
  459. diallocation = "SIP/#{username}"
  460. end
  461. =end
  462. @dbh.safe_query("DELETE FROM extensions WHERE context='sip-agents' AND exten='#{credentials[:id]}'")
  463. @dbh.safe_query("INSERT INTO extensions SET context='sip-agents', exten='#{credentials[:id]}', priority=1, app='Set', appdata='REALBRIDGEPEER=Agent/#{agent.id}' ")
  464. @dbh.safe_query("INSERT INTO extensions SET context='sip-agents', exten='#{credentials[:id]}', priority=2, app='Dial', appdata='#{diallocation}' ")
  465. @dbh.safe_query("INSERT INTO extensions SET context='sip-agents', exten='#{credentials[:id]}', priority=3, app='Hangup'")
  466. agent.seclevel = credentials[:seclevel]
  467. begin
  468. #agent.set_defaultprofile(credentials[:defaultprofile])
  469. queues = agent.set_defaultprofile(credentials[:defaultprofile])
  470. rescue Agent::InvalidProfileError
  471. # TODO - infer a sane default profile?
  472. Cpxlog.warning "Agent #{agent} has an invalid default profile #{credentials[:defaultprofile]}"
  473. socket.puts 'ERR #{counter} Your default profile is invalid, please notify the supervisor'
  474. end
  475. socket.puts "ACK #{counter} #{agent.seclevel} #{agent.profile} #{Time.now.to_i}"
  476. @eventserver.send_event('QueuesSync', {'Interface'=>"Agent/#{agent.id}"}) do |reply|
  477. if reply.success?
  478. agent.pause(true) do
  479. #if agent.talkingto
  480. # XXX this is kind of a hack
  481. #agent.state = Agent::ONCALL
  482. #agent.state = Agent::WARMXFER
  483. #else
  484. agent.state = Agent::RELEASED
  485. #end
  486. end
  487. else
  488. Cpxlog.warning "FAILED QUEUESSYNC?"
  489. end
  490. end
  491. Cpxlog.info "Agent #{agent} logged in from #{agent.socket.peeraddr[2]}"
  492. elsif reply.message =~ /^No such agent/
  493. socket.puts "ERR #{counter} This Agent is unknown to CBX"
  494. Cpxlog.warning "Agent #{agent} authenticated to the agent server, but failed to authenticate to CBX"
  495. throw :return
  496. elsif reply.message =~ /Agent already logged in/
  497. # Agent is still logged into cbx - AUIserver restart or
  498. # agent hungup in-call and is reconnecting before the call is done
  499. unless agent
  500. agent = Agent.new(@eventserver, credentials[:id], socket)
  501. agent.name = username
  502. else
  503. agent.set_socket socket
  504. end
  505. agent.seclevel = credentials[:seclevel]
  506. #agent.defaultprofile = credentials[:defaultprofile]
  507. if agent.profile != 0
  508. queues = agent.set_defaultprofile(agent.profile) # fix hosed queue memberships
  509. else
  510. # TODO - possibly use the states table to get the previous profile?
  511. queues = agent.set_defaultprofile(credentials[:defaultprofile])
  512. end
  513. @eventserver.send_event('QueuesSync', {'Interface'=>"Agent/#{agent.id}"}) do |reply|
  514. if !reply.success?
  515. Cpxlog.warning "FAILED QUEUESSYNC?"
  516. end
  517. end
  518. if agent.talkingto
  519. socket.puts "ACK #{counter} #{agent.seclevel} #{agent.profile} #{Time.now.to_i}"
  520. agent.pause(true) do
  521. # This isn't perfect, but it's worth a try to recover calltype this way
  522. if agent.talkingto.type == :outgoing
  523. agent.state = Agent::OUTGOINGCALL
  524. else
  525. agent.state = Agent::ONCALL
  526. end
  527. cid = URI.escape(agent.talkingto.get('CALLERIDNAME') + " <" + agent.talkingto.get('CALLERIDNUM') + ">")
  528. agent.send_event('CALLINFO', agent.talkingto.get('BRANDID'), agent.talkingto.type, cid)
  529. end
  530. # NOTE : this might conflict with CDR recovery...
  531. if agent.talkingto.cdr.last_transaction_end
  532. agent.talkingto.cdr.add_transaction(CDR::INCALL, agent.id, agent.talkingto.cdr.last_transaction_end)
  533. end
  534. Cpxlog.notice "Agent #{agent} re-logged in from #{agent.socket.peeraddr[2]} and is still in-call"
  535. elsif call = Caller.callers.detect{|x| x.ringingto == agent.id}
  536. socket.puts "ACK #{counter} #{agent.seclevel} #{agent.profile} #{Time.now.to_i}"
  537. agent.pause(false) do
  538. agent.state = Agent::RINGING
  539. end
  540. cid = URI.escape(call.get('CALLERIDNAME') + " <" + call.get('CALLERIDNUM') + ">")
  541. agent.send_event('CALLINFO', call.get('BRANDID'), call.type, cid)
  542. Cpxlog.notice "Agent #{agent} re-logged in from #{agent.socket.peeraddr[2]} and is ringing"
  543. else
  544. socket.puts "ACK #{counter} #{agent.seclevel} #{agent.profile} #{Time.now.to_i}"
  545. Cpxlog.warning("agent #{agent} logged in, not talking to anyone after restart?")
  546. agent.pause(true) do
  547. agent.state = Agent::RELEASED
  548. end
  549. end
  550. else
  551. Cpxlog.warning("Failed to login Agent #{credentials[:id]} with error #{reply.message}")
  552. throw :return
  553. end
  554. CallQueue.update_queue_membership(agent, queues)
  555. Agent.match_agents(:seclevel=>2) do |a|
  556. a.send_event('QUEUEMEMBER', agent.id, agent.name, agent.statetime, agent.state, queues.join(':'))
  557. end
  558. end
  559. end
  560. else
  561. Cpxlog.notice "Failed authentication attempt for #{username} from #{socket.peeraddr[2]}"
  562. socket.puts "ERR #{counter} invalid credentials for #{username}"
  563. end
  564. end
  565. def terminate_agent(agent)
  566. return unless agent.socket # don't bother if this agent already got nuked
  567. oldsocket = agent.socket # save socket so we can return it to finish logging out
  568. agent.set_socket(nil) # then nil socket so subsequent call to this method return above,
  569. # and prevent events being sent to an agent about to be logged off
  570. if agent.state == Agent::WRAPUP and agent.talkingto
  571. Cpxlog.warning("Completing wrapup for #{agent.talkingto} on agent #{agent} logoff")
  572. # Ensure we only write ENDWRAPUP if we have an unterminated INWRAPUP
  573. agent.talkingto.cdr.add_transaction(CDR::ENDWRAPUP, agent.id) if agent.talkingto.cdr.get_last_unterminated_transaction(CDR::ENDWRAPUP, agent.id)
  574. # check for other unterminated wrapups here!
  575. if t = agent.talkingto.cdr.get_last_unterminated_transaction(nil, nil)
  576. Cpxlog.warning "Not writing CDREND for #{agent.talkingto} because there are other open transactions (#{CDR::TRANSACTIONNAMES[t[:transaction]]} started at #{Time.at(t[:start])} with data: #{t[:data]})"
  577. else
  578. agent.talkingto.cdr.add_transaction(CDR::CDREND)
  579. end
  580. agent.talkingto = nil
  581. end
  582. @eventserver.send_event('AgentLogoff', {'Agent'=>agent.id, 'Soft'=>'true'}) do |reply|
  583. if reply.success?
  584. agent.logoff
  585. else
  586. Cpxlog.warning("Failed to logoff agent #{agent} : #{reply.message}")
  587. end
  588. end
  589. agent.set_socket(oldsocket,false)
  590. end
  591. end
  592. Server.new
  593. trap('SIGTERM') do
  594. puts caller
  595. # cleanup and exit
  596. Cpxlog.notice "Shutdown requested via the TERM signal"
  597. Cpxlog.notice "Logging off all agents.."
  598. Agent.agents.each do |x|
  599. Server.instance.terminate_agent(x)
  600. end
  601. Cpxlog.notice '..done, exiting'
  602. exit
  603. end
  604. trap('SIGINT') do
  605. puts caller
  606. # cleanup and exit
  607. Cpxlog.notice "Shutdown requested via the INT signal"
  608. Cpxlog.notice "Logging off all agents.."
  609. Agent.agents.each do |x|
  610. Server.instance.terminate_agent(x)
  611. end
  612. Cpxlog.notice '..done, exiting'
  613. exit
  614. end
  615. trap('SIGHUP') do
  616. # reload config (is this worth implementing?)
  617. end
  618. trap('SIGUSR1') do
  619. # reload code
  620. Cpxlog.notice 'Reloading code...'
  621. version = Object.send(:remove_const, :VERSIONSTRING)
  622. begin
  623. load('version.rb')
  624. rescue LoadError, SyntaxError => e
  625. Cpxlog.err("version.rb encountered a #{e.class} when reloading")
  626. VERSIONSTRING = version
  627. end
  628. agentconstants = Object.send(:remove_const, :AgentConstants)
  629. begin
  630. load('constants/agentconstants.rb')
  631. rescue LoadError, SyntaxError => e
  632. Cpxlog.err("constants/agentconstants.rb encountered a #{e.class} when reloading")
  633. AgentConstants = agentconstants
  634. end
  635. cdrconstants = Object.send(:remove_const, :CDRConstants)
  636. begin
  637. load('constants/cdrconstants.rb')
  638. rescue LoadError, SyntaxError => e
  639. Cpxlog.err("constants/cdrconstants.rb encountered a #{e.class} when reloading")
  640. CDRConstants = cdrconstants
  641. end
  642. consts = {}
  643. Cpxlog.constants.each do |const|
  644. consts[const] = Cpxlog.send(:remove_const, const)
  645. end
  646. begin
  647. load('cpxlog.rb')
  648. rescue LoadError, SyntaxError => e
  649. consts.each do |k, v|
  650. Cpxlog.send(:const_set, k, v)
  651. end
  652. Cpxlog.err("cpxlog.rb encountered a #{e.class} when reloading")
  653. end
  654. %w{agents.rb queues.rb cdr.rb client_events.rb server_events.rb}.each do |file|
  655. begin
  656. load(file)
  657. rescue SyntaxError, LoadError => e
  658. Cpxlog.err("#{file} encountered a #{e.class} when reloading")
  659. end
  660. end
  661. #puts 'done'
  662. Cpxlog.notice '..done reloading code'
  663. end
  664. trap('SIGUSR2') do
  665. puts caller
  666. # toggle debugging
  667. end
  668. begin
  669. trap('SIGINFO') do
  670. Cpxlog.increment_log_level
  671. end
  672. rescue ArgumentError
  673. Cpxlog.warning "SIGINFO is not supported on this platform"
  674. begin
  675. trap('SIGPWR') do
  676. Cpxlog.increment_log_level
  677. end
  678. rescue ArgumentError
  679. Cpxlog.warning "SIGPWR is also unsupported on this platform"
  680. else
  681. Cpxlog.notice "Use SIGPWR to change log level instead"
  682. end
  683. end
  684. Server.instance.connect