sessions.rb 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620
  1. # rubocop:disable Rails/TimeZone
  2. require 'json'
  3. require 'session_helper'
  4. module Sessions
  5. # get application root directory
  6. @root = Dir.pwd.to_s
  7. if !@root || @root.empty? || @root == '/'
  8. @root = Rails.root
  9. end
  10. # get working directories
  11. @path = "#{@root}/tmp/websocket"
  12. # create global vars for threads
  13. @@client_threads = {} # rubocop:disable Style/ClassVars
  14. =begin
  15. start new session
  16. Sessions.create( client_id, session_data, { :type => 'websocket' } )
  17. returns
  18. true|false
  19. =end
  20. def self.create( client_id, session, meta )
  21. path = "#{@path}/#{client_id}"
  22. path_tmp = "#{@path}/tmp/#{client_id}"
  23. session_file = "#{path_tmp}/session"
  24. # collect session data
  25. meta[:last_ping] = Time.now.utc.to_i.to_s
  26. data = {
  27. user: session,
  28. meta: meta,
  29. }
  30. content = data.to_json
  31. # store session data in session file
  32. FileUtils.mkpath path_tmp
  33. File.open( session_file, 'wb' ) { |file|
  34. file.write content
  35. }
  36. # destory old session if needed
  37. if File.exist?( path )
  38. Sessions.destory(client_id)
  39. end
  40. # move to destination directory
  41. FileUtils.mv( path_tmp, path )
  42. # send update to browser
  43. if session && session['id']
  44. send(
  45. client_id,
  46. {
  47. event: 'ws:login',
  48. data: { success: true },
  49. }
  50. )
  51. end
  52. end
  53. =begin
  54. list of all session
  55. client_ids = Sessions.sessions
  56. returns
  57. ['4711', '4712']
  58. =end
  59. def self.sessions
  60. path = "#{@path}/"
  61. # just make sure that spool path exists
  62. if !File.exist?( path )
  63. FileUtils.mkpath path
  64. end
  65. data = []
  66. Dir.foreach( path ) do |entry|
  67. next if entry == '.'
  68. next if entry == '..'
  69. next if entry == 'tmp'
  70. next if entry == 'spool'
  71. data.push entry.to_s
  72. end
  73. data
  74. end
  75. =begin
  76. list of all session
  77. Sessions.session_exists?(client_id)
  78. returns
  79. true|false
  80. =end
  81. def self.session_exists?(client_id)
  82. client_ids = sessions
  83. client_ids.include? client_id.to_s
  84. end
  85. =begin
  86. list of all session with data
  87. client_ids_with_data = Sessions.list
  88. returns
  89. {
  90. '4711' => {
  91. :user => {
  92. 'id' => 123,
  93. },
  94. :meta => {
  95. :type => 'websocket',
  96. :last_ping => time_of_last_ping,
  97. }
  98. },
  99. '4712' => {
  100. :user => {
  101. 'id' => 124,
  102. },
  103. :meta => {
  104. :type => 'ajax',
  105. :last_ping => time_of_last_ping,
  106. }
  107. },
  108. }
  109. =end
  110. def self.list
  111. client_ids = sessions
  112. session_list = {}
  113. client_ids.each { |client_id|
  114. data = get(client_id)
  115. next if !data
  116. session_list[client_id] = data
  117. }
  118. session_list
  119. end
  120. =begin
  121. destroy session
  122. Sessions.destory(client_id)
  123. returns
  124. true|false
  125. =end
  126. def self.destory( client_id )
  127. path = "#{@path}/#{client_id}"
  128. FileUtils.rm_rf path
  129. end
  130. =begin
  131. destroy idle session
  132. list_of_client_ids = Sessions.destory_idle_sessions
  133. returns
  134. ['4711', '4712']
  135. =end
  136. def self.destory_idle_sessions(idle_time_in_sec = 240)
  137. list_of_closed_sessions = []
  138. clients = Sessions.list
  139. clients.each { |client_id, client|
  140. if !client[:meta] || !client[:meta][:last_ping] || ( client[:meta][:last_ping].to_i + idle_time_in_sec ) < Time.now.utc.to_i
  141. list_of_closed_sessions.push client_id
  142. Sessions.destory( client_id )
  143. end
  144. }
  145. list_of_closed_sessions
  146. end
  147. =begin
  148. touch session
  149. Sessions.touch(client_id)
  150. returns
  151. true|false
  152. =end
  153. def self.touch( client_id )
  154. data = get(client_id)
  155. return false if !data
  156. path = "#{@path}/#{client_id}"
  157. data[:meta][:last_ping] = Time.now.utc.to_i.to_s
  158. content = data.to_json
  159. File.open( path + '/session', 'wb' ) { |file|
  160. file.write content
  161. }
  162. true
  163. end
  164. =begin
  165. get session data
  166. data = Sessions.get(client_id)
  167. returns
  168. {
  169. :user => {
  170. 'id' => 123,
  171. },
  172. :meta => {
  173. :type => 'websocket',
  174. :last_ping => time_of_last_ping,
  175. }
  176. }
  177. =end
  178. def self.get( client_id )
  179. session_dir = "#{@path}/#{client_id}"
  180. session_file = "#{session_dir}/session"
  181. data = nil
  182. if !File.exist? session_dir
  183. destory(client_id)
  184. Rails.logger.error "missing session directory for '#{client_id}', remove session."
  185. return
  186. end
  187. if !File.exist? session_file
  188. destory(client_id)
  189. Rails.logger.errror "missing session file for '#{client_id}', remove session."
  190. return
  191. end
  192. begin
  193. File.open( session_file, 'rb' ) { |file|
  194. file.flock( File::LOCK_EX )
  195. all = file.read
  196. file.flock( File::LOCK_UN )
  197. data_json = JSON.parse( all )
  198. if data_json
  199. data = symbolize_keys(data_json)
  200. data[:user] = data_json['user'] # for compat. reasons
  201. end
  202. }
  203. rescue => e
  204. Rails.logger.error e.inspect
  205. destory(client_id)
  206. Rails.logger.error "ERROR: reading session file '#{session_file}', remove session."
  207. return
  208. end
  209. data
  210. end
  211. =begin
  212. send message to client
  213. Sessions.send(client_id_of_recipient, data)
  214. returns
  215. true|false
  216. =end
  217. def self.send( client_id, data )
  218. path = "#{@path}/#{client_id}/"
  219. filename = "send-#{ Time.now.utc.to_f }"
  220. check = true
  221. count = 0
  222. while check
  223. if File.exist?( path + filename )
  224. count += 1
  225. filename = "#{filename}-#{count}"
  226. else
  227. check = false
  228. end
  229. end
  230. return false if !File.directory? path
  231. File.open( path + 'a-' + filename, 'wb' ) { |file|
  232. file.flock( File::LOCK_EX )
  233. file.write data.to_json
  234. file.flock( File::LOCK_UN )
  235. file.close
  236. }
  237. return false if !File.exist?( path + 'a-' + filename )
  238. FileUtils.mv( path + 'a-' + filename, path + filename )
  239. true
  240. end
  241. =begin
  242. send message to recipient client
  243. Sessions.send_to(user_id, data)
  244. returns
  245. true|false
  246. =end
  247. def self.send_to( user_id, data )
  248. # list all current clients
  249. client_list = sessions
  250. client_list.each {|client_id|
  251. session = Sessions.get(client_id)
  252. next if !session
  253. next if !session[:user]
  254. next if !session[:user]['id']
  255. next if session[:user]['id'].to_i != user_id.to_i
  256. Sessions.send( client_id, data )
  257. }
  258. true
  259. end
  260. =begin
  261. send message to all client
  262. Sessions.broadcast(data)
  263. returns
  264. true|false
  265. =end
  266. def self.broadcast( data )
  267. # list all current clients
  268. client_list = sessions
  269. client_list.each {|client_id|
  270. Sessions.send( client_id, data )
  271. }
  272. true
  273. end
  274. =begin
  275. get messages for client
  276. messages = Sessions.queue(client_id_of_recipient)
  277. returns
  278. [
  279. {
  280. key1 => 'some data of message 1',
  281. key2 => 'some data of message 1',
  282. },
  283. {
  284. key1 => 'some data of message 2',
  285. key2 => 'some data of message 2',
  286. },
  287. ]
  288. =end
  289. def self.queue( client_id )
  290. path = "#{@path}/#{client_id}/"
  291. data = []
  292. files = []
  293. Dir.foreach( path ) {|entry|
  294. next if entry == '.'
  295. next if entry == '..'
  296. files.push entry
  297. }
  298. files.sort.each {|entry|
  299. filename = "#{path}/#{entry}"
  300. if /^send/.match( entry )
  301. data.push Sessions.queue_file_read( path, entry )
  302. end
  303. }
  304. data
  305. end
  306. def self.queue_file_read( path, filename )
  307. file_old = "#{path}#{filename}"
  308. file_new = "#{path}a-#{filename}"
  309. FileUtils.mv( file_old, file_new )
  310. all = ''
  311. File.open( file_new, 'rb' ) { |file|
  312. all = file.read
  313. }
  314. File.delete( file_new )
  315. JSON.parse( all )
  316. end
  317. def self.cleanup
  318. path = "#{@path}/spool/"
  319. FileUtils.rm_rf path
  320. path = "#{@path}/tmp/"
  321. FileUtils.rm_rf path
  322. end
  323. def self.spool_create( msg )
  324. path = "#{@path}/spool/"
  325. FileUtils.mkpath path
  326. file_path = path + "/#{Time.now.utc.to_f}-#{rand(99_999)}"
  327. File.open( file_path, 'wb' ) { |file|
  328. data = {
  329. msg: msg,
  330. timestamp: Time.now.utc.to_i,
  331. }
  332. file.write data.to_json
  333. }
  334. end
  335. def self.spool_list( timestamp, current_user_id )
  336. path = "#{@path}/spool/"
  337. FileUtils.mkpath path
  338. data = []
  339. to_delete = []
  340. files = []
  341. Dir.foreach( path ) {|entry|
  342. next if entry == '.'
  343. next if entry == '..'
  344. files.push entry
  345. }
  346. files.sort.each {|entry|
  347. filename = "#{path}/#{entry}"
  348. next if !File.exist?( filename )
  349. File.open( filename, 'rb' ) { |file|
  350. all = file.read
  351. spool = JSON.parse( all )
  352. begin
  353. message_parsed = JSON.parse( spool['msg'] )
  354. rescue => e
  355. Rails.logger.error "can't parse spool message: #{ message }, #{ e.inspect }"
  356. next
  357. end
  358. # ignore message older then 48h
  359. if spool['timestamp'] + (2 * 86_400) < Time.now.utc.to_i
  360. to_delete.push "#{path}/#{entry}"
  361. next
  362. end
  363. # add spool attribute to push spool info to clients
  364. message_parsed['spool'] = true
  365. # only send not already now messages
  366. if !timestamp || timestamp < spool['timestamp']
  367. # spool to recipient list
  368. if message_parsed['recipient'] && message_parsed['recipient']['user_id']
  369. message_parsed['recipient']['user_id'].each { |user_id|
  370. next if current_user_id != user_id
  371. item = {
  372. type: 'direct',
  373. message: message_parsed,
  374. }
  375. data.push item
  376. }
  377. # spool to every client
  378. else
  379. item = {
  380. type: 'broadcast',
  381. message: message_parsed,
  382. }
  383. data.push item
  384. end
  385. end
  386. }
  387. }
  388. to_delete.each {|file|
  389. File.delete(file)
  390. }
  391. data
  392. end
  393. def self.jobs
  394. # just make sure that spool path exists
  395. if !File.exist?( @path )
  396. FileUtils.mkpath @path
  397. end
  398. Thread.abort_on_exception = true
  399. loop do
  400. client_ids = sessions
  401. client_ids.each { |client_id|
  402. # connection already open, ignore
  403. next if @@client_threads[client_id]
  404. # get current user
  405. session_data = Sessions.get( client_id )
  406. next if !session_data
  407. next if !session_data[:user]
  408. next if !session_data[:user]['id']
  409. user = User.lookup( id: session_data[:user]['id'] )
  410. next if !user
  411. # start client thread
  412. next if @@client_threads[client_id]
  413. @@client_threads[client_id] = true
  414. @@client_threads[client_id] = Thread.new {
  415. thread_client(client_id)
  416. @@client_threads[client_id] = nil
  417. Rails.logger.debug "close client (#{client_id}) thread"
  418. ActiveRecord::Base.connection.close
  419. }
  420. sleep 0.5
  421. }
  422. # system settings
  423. sleep 0.5
  424. end
  425. end
  426. =begin
  427. check if thread for client_id is running
  428. Sessions.thread_client_exists?(client_id)
  429. returns
  430. thread
  431. =end
  432. def self.thread_client_exists?(client_id)
  433. @@client_threads[client_id]
  434. end
  435. =begin
  436. start client for browser
  437. Sessions.thread_client(client_id)
  438. returns
  439. thread
  440. =end
  441. def self.thread_client(client_id, try_count = 0, try_run_time = Time.now.utc)
  442. Rails.logger.debug "LOOP #{client_id} - #{try_count}"
  443. begin
  444. Sessions::Client.new(client_id)
  445. rescue => e
  446. Rails.logger.error "thread_client #{client_id} exited with error #{ e.inspect }"
  447. Rails.logger.error e.backtrace.join("\n ")
  448. sleep 10
  449. begin
  450. ActiveRecord::Base.connection_pool.release_connection
  451. rescue => e
  452. Rails.logger.error "Can't reconnect to database #{ e.inspect }"
  453. end
  454. try_run_max = 10
  455. try_count += 1
  456. # reset error counter if to old
  457. if try_run_time + ( 60 * 5 ) < Time.now.utc
  458. try_count = 0
  459. end
  460. try_run_time = Time.now.utc
  461. # restart job again
  462. if try_run_max > try_count
  463. thread_client(client_id, try_count, try_run_time)
  464. else
  465. raise "STOP thread_client for client #{client_id} after #{try_run_max} tries"
  466. end
  467. end
  468. Rails.logger.debug "/LOOP #{client_id} - #{try_count}"
  469. end
  470. def self.symbolize_keys(hash)
  471. hash.each_with_object({}) {|(key, value), result|
  472. new_key = case key
  473. when String then key.to_sym
  474. else key
  475. end
  476. new_value = case value
  477. when Hash then symbolize_keys(value)
  478. else value
  479. end
  480. result[new_key] = new_value
  481. }
  482. end
  483. end