acpc_table_manager 3.0.18 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'acpc_poker_types'
4
+ require 'acpc_poker_basic_proxy'
5
+ require 'acpc_dealer'
6
+ require 'awesome_print'
7
+
8
+ class AcpcTestingBot
9
+ def initialize(port_number, server_host_name='localhost', random=false)
10
+ log(
11
+ __method__,
12
+ port_number: port_number,
13
+ server_host_name: server_host_name,
14
+ random: random
15
+ )
16
+ dealer_info = AcpcDealer::ConnectionInformation.new port_number, server_host_name
17
+ @proxy_bot = AcpcPokerBasicProxy::BasicProxy.new dealer_info
18
+
19
+ log __method__, msg: 'Connected to dealer'
20
+
21
+ match_state = @proxy_bot.receive_match_state!
22
+
23
+ log __method__, msg: 'Got first match state'
24
+
25
+ @counter = 0
26
+ while true do
27
+ begin
28
+ if random
29
+ send_random_action
30
+ else
31
+ send_deterministic_action
32
+ end
33
+ match_state = @proxy_bot.receive_match_state!
34
+
35
+ log __method__, match_state: match_state
36
+
37
+ if match_state.last_action && match_state.last_action.action == 'r'
38
+ @fold_allowed = true
39
+ else
40
+ @fold_allowed = false
41
+ end
42
+ rescue AcpcPokerBasicProxy::DealerStream::UnableToWriteToDealer
43
+ exit
44
+ rescue AcpcPokerBasicProxy::DealerStream::UnableToGetFromDealer
45
+ # Ignore this these since they will always occur at the end of the match
46
+ # since this bot doesn't know anything about the match or turns.
47
+ exit
48
+ rescue => e
49
+ puts "Error in main loop: #{e.message}, backtrace: #{e.backtrace.join("\n")}"
50
+ exit
51
+ end
52
+ end
53
+ end
54
+
55
+ def send_deterministic_action
56
+ log __method__, counter: @counter
57
+
58
+ case (@counter % 3)
59
+ when 0
60
+ @proxy_bot.send_action AcpcPokerTypes::PokerAction::CALL
61
+ when 1
62
+ if @fold_allowed
63
+ @proxy_bot.send_action AcpcPokerTypes::PokerAction::FOLD
64
+ else
65
+ @proxy_bot.send_action AcpcPokerTypes::PokerAction::CALL
66
+ end
67
+ when 2
68
+ @proxy_bot.send_action AcpcPokerTypes::PokerAction.new('r1')
69
+ end
70
+ @counter += 1
71
+ end
72
+
73
+ def send_random_action
74
+ random_number = rand
75
+ @counter = (random_number * 10 ** (random_number.to_s.length-2)).to_i
76
+
77
+ log __method__, counter: @counter
78
+
79
+ send_deterministic_action
80
+ end
81
+
82
+ def log(method, data)
83
+ ap({"#{self.class}##{method}" => data})
84
+ STDOUT.flush
85
+ end
86
+ end
87
+
88
+ def print_usage
89
+ puts "Usage: #{$0} <port number> [server host name] [millisecond timeout] [random]"
90
+ end
91
+
92
+ def proper_usage?
93
+ ARGV.length > 0
94
+ end
95
+
96
+ def run_script
97
+ if ARGV.length > 1
98
+ server_host_name, port = ARGV[0].strip, ARGV[1].strip
99
+ else
100
+ server_host_name, port = 'localhost', ARGV[0].strip
101
+ end
102
+ random = if ARGV.length > 3
103
+ ARGV[3].strip == 'true'
104
+ else
105
+ false
106
+ end
107
+
108
+ AcpcTestingBot.new port, server_host_name, random
109
+ end
110
+
111
+ if proper_usage?
112
+ run_script
113
+ else
114
+ print_usage
115
+ end
@@ -1,16 +1,638 @@
1
- require_relative "acpc_table_manager/version"
2
- require_relative "acpc_table_manager/config"
3
- require_relative "acpc_table_manager/match"
4
- require_relative "acpc_table_manager/match_slice"
5
- require_relative "acpc_table_manager/match_view"
6
- require_relative "acpc_table_manager/monkey_patches"
7
- require_relative "acpc_table_manager/opponents"
8
- require_relative "acpc_table_manager/dealer"
9
- require_relative "acpc_table_manager/proxy"
10
- require_relative "acpc_table_manager/simple_logging"
11
- require_relative "acpc_table_manager/maintainer"
12
- require_relative "acpc_table_manager/table_queue"
13
- require_relative "acpc_table_manager/utils"
1
+ require 'contextual_exceptions'
2
+ require 'rusen'
3
+ require 'acpc_dealer'
4
+ require 'acpc_poker_types'
5
+ require 'redis'
6
+ require 'timeout'
7
+ require 'zaru'
8
+ require 'shellwords'
9
+ require 'yaml'
10
+
11
+ require_relative 'acpc_table_manager/version'
12
+ require_relative 'acpc_table_manager/config'
13
+ require_relative 'acpc_table_manager/monkey_patches'
14
+ require_relative 'acpc_table_manager/simple_logging'
15
+ require_relative 'acpc_table_manager/utils'
16
+ require_relative 'acpc_table_manager/proxy_utils'
17
+
18
+ using AcpcTableManager::SimpleLogging::MessageFormatting
14
19
 
15
20
  module AcpcTableManager
21
+ class UninitializedError < StandardError
22
+ include ContextualExceptions::ContextualError
23
+ end
24
+ class NoPortForDealerAvailable < StandardError
25
+ include ContextualExceptions::ContextualError
26
+ end
27
+ class MatchAlreadyEnqueued < StandardError
28
+ include ContextualExceptions::ContextualError
29
+ end
30
+ class NoBotRunner < StandardError
31
+ include ContextualExceptions::ContextualError
32
+ end
33
+ class RequiresTooManySpecialPorts < StandardError
34
+ include ContextualExceptions::ContextualError
35
+ end
36
+ class SubscribeTimeout < StandardError
37
+ include ContextualExceptions::ContextualError
38
+ end
39
+
40
+ class CommunicatorComponent
41
+ attr_reader :channel
42
+ def initialize(id)
43
+ @channel = self.class.channel_from_id(id)
44
+ @redis = AcpcTableManager.new_redis_connection()
45
+ end
46
+ def del() @redis.del @channel end
47
+ end
48
+
49
+ class Receiver < CommunicatorComponent
50
+ def subscribe_with_timeout
51
+ list, message = @redis.blpop(
52
+ @channel,
53
+ timeout: AcpcTableManager.config.maintenance_interval_s
54
+ )
55
+ if message
56
+ yield JSON.parse(message)
57
+ else
58
+ raise SubscribeTimeout
59
+ end
60
+ end
61
+ end
62
+
63
+ class TableManagerReceiver < Receiver
64
+ def self.channel_from_id(id) id end
65
+ end
66
+
67
+ class Sender < CommunicatorComponent
68
+ def self.channel_from_id(id) "#{id}-from-proxy" end
69
+ def publish(data)
70
+ @redis.rpush @channel, data
71
+ @redis.publish @channel, data
72
+ end
73
+ end
74
+
75
+ class ProxyReceiver < Receiver
76
+ def self.channel_from_id(id) "#{id}-to-proxy" end
77
+ end
78
+
79
+ class ProxyCommunicator
80
+ def initialize(id)
81
+ @sender = Sender.new(id)
82
+ @receiver = ProxyReceiver.new(id)
83
+ end
84
+ def publish(data) @sender.publish(data) end
85
+ def subscribe_with_timeout
86
+ @receiver.subscribe_with_timeout { |on| yield on }
87
+ end
88
+ def send_channel() @sender.channel end
89
+ def receive_channel() @receiver.channel end
90
+ def del_saved
91
+ @receiver.del
92
+ @sender.del
93
+ end
94
+ end
95
+
96
+ module TimeRefinement
97
+ refine Time.class() do
98
+ def now_as_string
99
+ now.strftime('%b%-d_%Y-at-%-H_%-M_%-S')
100
+ end
101
+ end
102
+ end
103
+ using AcpcTableManager::TimeRefinement
104
+
105
+ def self.shell_sanitize(string)
106
+ Zaru.sanitize!(Shellwords.escape(string.gsub(/\s+/, '_')))
107
+ end
108
+
109
+ def self.raise_uninitialized
110
+ raise UninitializedError.new(
111
+ "Unable to complete with AcpcTableManager uninitialized. Please initialize AcpcTableManager with configuration settings by calling AcpcTableManager.load! with a (YAML) configuration file name."
112
+ )
113
+ end
114
+
115
+ @@config = nil
116
+
117
+ def self.config
118
+ if @@config
119
+ @@config
120
+ else
121
+ raise_uninitialized
122
+ end
123
+ end
124
+
125
+ @@exhibition_config = nil
126
+ def self.exhibition_config
127
+ if @@exhibition_config
128
+ @@exhibition_config
129
+ else
130
+ raise_uninitialized
131
+ end
132
+ end
133
+
134
+ @@is_initialized = false
135
+
136
+ @@redis_config_file = nil
137
+ def self.redis_config_file() @@redis_config_file end
138
+
139
+ @@config_file = nil
140
+ def self.config_file() @@config_file end
141
+
142
+ @@notifier = nil
143
+ def self.notifier() @@notifier end
144
+
145
+ def self.load_config!(config_data, yaml_directory = File.pwd)
146
+ interpolation_hash = {
147
+ pwd: yaml_directory,
148
+ home: Dir.home,
149
+ :~ => Dir.home,
150
+ dealer_directory: AcpcDealer::DEALER_DIRECTORY
151
+ }
152
+ config = interpolate_all_strings(config_data, interpolation_hash)
153
+ interpolation_hash[:pwd] = File.dirname(config['table_manager_constants'])
154
+
155
+ @@config = Config.new(
156
+ config['table_manager_constants'],
157
+ config['log_directory'],
158
+ config['match_log_directory'],
159
+ config['data_directory'],
160
+ interpolation_hash
161
+ )
162
+
163
+ interpolation_hash[:pwd] = File.dirname(config['exhibition_constants'])
164
+ @@exhibition_config = ExhibitionConfig.new(
165
+ config['exhibition_constants'],
166
+ interpolation_hash,
167
+ Logger.from_file_name(File.join(@@config.my_log_directory, 'exhibition_config.log'))
168
+ )
169
+
170
+ if config['error_report']
171
+ Rusen.settings.sender_address = config['error_report']['sender']
172
+ Rusen.settings.exception_recipients = config['error_report']['recipients']
173
+
174
+ Rusen.settings.outputs = config['error_report']['outputs'] || [:pony]
175
+ Rusen.settings.sections = config['error_report']['sections'] || [:backtrace]
176
+ Rusen.settings.email_prefix = config['error_report']['email_prefix'] || '[ERROR] '
177
+ Rusen.settings.smtp_settings = config['error_report']['smtp']
178
+
179
+ @@notifier = Rusen
180
+ else
181
+ @@config.log(
182
+ __method__,
183
+ {
184
+ warning: "Email reporting disabled. Please set email configuration to enable this feature."
185
+ },
186
+ Logger::Severity::WARN
187
+ )
188
+ end
189
+ @@redis_config_file = config['redis_config_file'] || 'default'
190
+
191
+ FileUtils.mkdir(opponents_log_dir) unless File.directory?(opponents_log_dir)
192
+
193
+ @@is_initialized = true
194
+
195
+ @@exhibition_config.games.keys.each do |game|
196
+ d = data_directory(game)
197
+ FileUtils.mkdir_p d unless File.directory?(d)
198
+ q = enqueued_matches_file(game)
199
+ FileUtils.touch q unless File.exist?(q)
200
+ r = running_matches_file(game)
201
+ FileUtils.touch r unless File.exist?(r)
202
+ end
203
+ end
204
+
205
+ def self.new_redis_connection(options = {})
206
+ if @@redis_config_file && @@redis_config_file != 'default'
207
+ redis_config = YAML.load_file(@@redis_config_file).symbolize_keys
208
+ options.merge!(redis_config[:default].symbolize_keys)
209
+ Redis.new(
210
+ if config['redis_environment_mode'] && redis_config[config['redis_environment_mode'].to_sym]
211
+ options.merge(redis_config[config['redis_environment_mode'].to_sym].symbolize_keys)
212
+ else
213
+ options
214
+ end
215
+ )
216
+ else
217
+ Redis.new options
218
+ end
219
+ end
220
+
221
+ def self.load!(config_file_path)
222
+ @@config_file = config_file_path
223
+ load_config! YAML.load_file(config_file_path), File.dirname(config_file_path)
224
+ end
225
+
226
+ def self.notify(exception)
227
+ @@notifier.notify(exception) if @@notifier
228
+ end
229
+
230
+ def self.initialized?
231
+ @@is_initialized
232
+ end
233
+
234
+ def self.raise_if_uninitialized
235
+ raise_uninitialized unless initialized?
236
+ end
237
+
238
+ def self.new_log(log_file_name, log_directory_ = nil)
239
+ raise_if_uninitialized
240
+ log_directory_ ||= @@config.my_log_directory
241
+ FileUtils.mkdir_p(log_directory_) unless File.directory?(log_directory_)
242
+ Logger.from_file_name(File.join(log_directory_, log_file_name)).with_metadata!
243
+ end
244
+
245
+ def self.unload!
246
+ @@is_initialized = false
247
+ end
248
+
249
+ def self.opponents_log_dir
250
+ File.join(AcpcTableManager.config.log_directory, 'opponents')
251
+ end
252
+
253
+ def self.data_directory(game = nil)
254
+ raise_if_uninitialized
255
+ if game
256
+ File.join(@@config.data_directory, shell_sanitize(game))
257
+ else
258
+ @@config.data_directory
259
+ end
260
+ end
261
+
262
+ def self.enqueued_matches_file(game)
263
+ File.join(data_directory(game), 'enqueued_matches.yml')
264
+ end
265
+
266
+ def self.running_matches_file(game)
267
+ File.join(data_directory(game), 'running_matches.yml')
268
+ end
269
+
270
+ def self.enqueued_matches(game)
271
+ YAML.load_file(enqueued_matches_file(game)) || []
272
+ end
273
+
274
+ def self.running_matches(game)
275
+ saved_matches = YAML.load_file(running_matches_file(game))
276
+ return [] unless saved_matches
277
+
278
+ checked_matches = []
279
+ saved_matches.each do |match|
280
+ if AcpcDealer::process_exists?(match[:dealer][:pid])
281
+ checked_matches << match
282
+ end
283
+ end
284
+ if checked_matches.length != saved_matches.length
285
+ update_running_matches game, checked_matches
286
+ end
287
+ checked_matches
288
+ end
289
+
290
+ def self.sanitized_player_names(names)
291
+ names.map { |name| Shellwords.escape(name.gsub(/\s+/, '_')) }
292
+ end
293
+
294
+ def self.match_name(players: nil, game_def_key: nil, time: true)
295
+ name = "match"
296
+ name += ".#{sanitized_player_names(players).join('.')}" if players
297
+ if game_def_key
298
+ name += ".#{game_def_key}.#{exhibition_config.games[game_def_key]['num_hands_per_match']}h"
299
+ end
300
+ name += ".#{Time.now_as_string}" if time
301
+ shell_sanitize name
302
+ end
303
+
304
+ def self.dealer_arguments(game, name, players, random_seed)
305
+ {
306
+ match_name: shell_sanitize(name),
307
+ game_def_file_name: Shellwords.escape(
308
+ exhibition_config.games[game]['file']
309
+ ),
310
+ hands: Shellwords.escape(
311
+ exhibition_config.games[game]['num_hands_per_match']
312
+ ),
313
+ random_seed: Shellwords.escape(random_seed.to_s),
314
+ player_names: sanitized_player_names(players).join(' '),
315
+ options: exhibition_config.dealer_options.join(' ')
316
+ }
317
+ end
318
+
319
+ def self.proxy_player?(player_name, game_def_key)
320
+ exhibition_config.games[game_def_key]['opponents'][player_name].nil?
321
+ end
322
+
323
+ def self.start_dealer(game, name, players, random_seed, port_numbers)
324
+ config.log __method__, name: name
325
+ args = dealer_arguments game, name, players, random_seed
326
+
327
+ config.log __method__, {
328
+ dealer_arguments: args,
329
+ log_directory: ::AcpcTableManager.config.match_log_directory,
330
+ port_numbers: port_numbers,
331
+ command: AcpcDealer::DealerRunner.command(
332
+ args,
333
+ port_numbers
334
+ )
335
+ }
336
+
337
+ Timeout::timeout(3) do
338
+ AcpcDealer::DealerRunner.start(
339
+ args,
340
+ config.match_log_directory,
341
+ port_numbers
342
+ )
343
+ end
344
+ end
345
+
346
+ def self.start_proxy(game, proxy_id, port, seat)
347
+ config.log __method__, msg: "Starting proxy"
348
+
349
+ args = [
350
+ "-t #{config_file}",
351
+ "-i #{proxy_id}",
352
+ "-p #{port}",
353
+ "-s #{seat}",
354
+ "-g #{game}"
355
+ ]
356
+ command = "#{File.expand_path('../../exe/acpc_proxy', __FILE__)} #{args.join(' ')}"
357
+ start_process command
358
+ end
359
+
360
+ # @todo This method looks broken
361
+ # def self.bots(game_def_key, player_names, dealer_host)
362
+ # bot_info_from_config_that_match_opponents = exhibition_config.bots(
363
+ # game_def_key,
364
+ # *opponent_names(player_names)
365
+ # )
366
+ # bot_opponent_ports = opponent_ports_with_condition do |name|
367
+ # bot_info_from_config_that_match_opponents.keys.include? name
368
+ # end
369
+ #
370
+ # raise unless (
371
+ # port_numbers.length == player_names.length ||
372
+ # bot_opponent_ports.length == bot_info_from_config_that_match_opponents.length
373
+ # )
374
+ #
375
+ # bot_opponent_ports.zip(
376
+ # bot_info_from_config_that_match_opponents.keys,
377
+ # bot_info_from_config_that_match_opponents.values
378
+ # ).reduce({}) do |map, args|
379
+ # port_num, name, info = args
380
+ # map[name] = {
381
+ # runner: (if info['runner'] then info['runner'] else info end),
382
+ # host: dealer_host, port: port_num
383
+ # }
384
+ # map
385
+ # end
386
+ # end
387
+
388
+ # @return [Integer] PID of the bot started
389
+ def self.start_bot(id, bot_info, port)
390
+ runner = bot_info['runner'].to_s
391
+ if runner.nil? || runner.strip.empty?
392
+ raise NoBotRunner, %Q{Bot "#{id}" with info #{bot_info} has no runner.}
393
+ end
394
+ args = [runner, config.dealer_host.to_s, port.to_s]
395
+ log_file = File.join(opponents_log_dir, "#{id}.log")
396
+ command_to_run = args.join(' ')
397
+
398
+ config.log(
399
+ __method__,
400
+ {
401
+ starting_bot: id,
402
+ args: args,
403
+ log_file: log_file
404
+ }
405
+ )
406
+ start_process command_to_run, log_file
407
+ end
408
+
409
+ def self.enqueue_match(game, players, seed)
410
+ sanitized_name = match_name(
411
+ game_def_key: game,
412
+ players: players,
413
+ time: true
414
+ )
415
+ enqueued_matches_ = enqueued_matches game
416
+ if enqueued_matches_.any? { |e| e[:name] == sanitized_name }
417
+ raise(
418
+ MatchAlreadyEnqueued,
419
+ %Q{Match "#{sanitized_name}" already enqueued.}
420
+ )
421
+ end
422
+ enqueued_matches_ << (
423
+ {
424
+ name: sanitized_name,
425
+ game_def_key: game,
426
+ players: sanitized_player_names(players),
427
+ random_seed: seed
428
+ }
429
+ )
430
+ update_enqueued_matches game, enqueued_matches_
431
+ end
432
+
433
+ def self.player_id(game, player_name, seat)
434
+ shell_sanitize(
435
+ "#{match_name(game_def_key: game, players: [player_name], time: false)}.#{seat}"
436
+ )
437
+ end
438
+
439
+ def self.available_special_ports(ports_in_use)
440
+ if exhibition_config.special_ports_to_dealer
441
+ exhibition_config.special_ports_to_dealer - ports_in_use
442
+ else
443
+ []
444
+ end
445
+ end
446
+
447
+ def self.next_special_port(ports_in_use)
448
+ available_ports_ = available_special_ports(ports_in_use)
449
+ port_ = available_ports_.pop
450
+ until port_.nil? || AcpcDealer.port_available?(port_)
451
+ port_ = available_ports_.pop
452
+ end
453
+ unless port_
454
+ raise NoPortForDealerAvailable, "None of the available special ports (#{available_special_ports(ports_in_use)}) are open."
455
+ end
456
+ port_
457
+ end
458
+
459
+ def self.start_matches_if_allowed(game = nil)
460
+ if game
461
+ running_matches_ = running_matches(game)
462
+ skipped_matches = []
463
+ enqueued_matches_ = enqueued_matches(game)
464
+ start_matches_in_game_if_allowed(
465
+ game,
466
+ running_matches_,
467
+ skipped_matches,
468
+ enqueued_matches_
469
+ )
470
+ unless enqueued_matches_.empty? && skipped_matches.empty?
471
+ update_enqueued_matches game, skipped_matches + enqueued_matches_
472
+ end
473
+ else
474
+ exhibition_config.games.keys.each do |game|
475
+ start_matches_if_allowed game
476
+ end
477
+ end
478
+ end
479
+
480
+ def self.update_enqueued_matches(game, enqueued_matches_)
481
+ write_yml enqueued_matches_file(game), enqueued_matches_
482
+ end
483
+
484
+ def self.update_running_matches(game, running_matches_)
485
+ write_yml running_matches_file(game), running_matches_
486
+ end
487
+
488
+ def self.start_match(
489
+ game,
490
+ name,
491
+ players,
492
+ seed,
493
+ port_numbers
494
+ )
495
+ dealer_info = start_dealer(
496
+ game,
497
+ name,
498
+ players,
499
+ seed,
500
+ port_numbers
501
+ )
502
+ port_numbers = dealer_info[:port_numbers]
503
+
504
+ player_info = []
505
+ players.each_with_index do |player_name, i|
506
+ player_info << (
507
+ {
508
+ name: player_name,
509
+ pid: (
510
+ if exhibition_config.games[game]['opponents'][player_name]
511
+ start_bot(
512
+ player_id(game, player_name, i),
513
+ exhibition_config.games[game]['opponents'][player_name],
514
+ port_numbers[i]
515
+ )
516
+ else
517
+ start_proxy(
518
+ game,
519
+ player_id(game, player_name, i),
520
+ port_numbers[i],
521
+ i
522
+ )
523
+ end
524
+ )
525
+ }
526
+ )
527
+ end
528
+ return dealer_info, player_info
529
+ end
530
+
531
+ def self.allocate_ports(players, game, ports_in_use)
532
+ num_special_ports_for_this_match = 0
533
+ max_num_special_ports = if exhibition_config.special_ports_to_dealer.nil?
534
+ 0
535
+ else
536
+ exhibition_config.special_ports_to_dealer.length
537
+ end
538
+ players.map do |player|
539
+ bot_info = exhibition_config.games[game]['opponents'][player]
540
+ if bot_info && bot_info['requires_special_port']
541
+ num_special_ports_for_this_match += 1
542
+ if num_special_ports_for_this_match > max_num_special_ports
543
+ raise(
544
+ RequiresTooManySpecialPorts,
545
+ %Q{At least #{num_special_ports_for_this_match} special ports are required but only #{max_num_special_ports} ports were declared.}
546
+ )
547
+ end
548
+ special_port = next_special_port(ports_in_use)
549
+ ports_in_use << special_port
550
+ special_port
551
+ else
552
+ 0
553
+ end
554
+ end
555
+ end
556
+
557
+ private
558
+
559
+ def self.write_yml(f, obj)
560
+ File.open(f, 'w') { |f| f.write YAML.dump(obj) }
561
+ end
562
+
563
+ def self.start_matches_in_game_if_allowed(
564
+ game,
565
+ running_matches_,
566
+ skipped_matches,
567
+ enqueued_matches_
568
+ )
569
+ while running_matches_.length < exhibition_config.games[game]['max_num_matches']
570
+ next_match = enqueued_matches_.shift
571
+ break unless next_match
572
+
573
+ ports_in_use = running_matches_.map do |m|
574
+ m[:dealer][:port_numbers]
575
+ end.flatten
576
+
577
+ begin
578
+ port_numbers = allocate_ports(next_match[:players], game, ports_in_use)
579
+
580
+ dealer_info, player_info = start_match(
581
+ game,
582
+ next_match[:name],
583
+ next_match[:players],
584
+ next_match[:random_seed],
585
+ port_numbers
586
+ )
587
+ rescue NoPortForDealerAvailable => e
588
+ config.log(
589
+ __method__,
590
+ {
591
+ message: e.message,
592
+ skipping_match: next_match[:name],
593
+ backtrace: e.backtrace
594
+ },
595
+ Logger::Severity::WARN
596
+ )
597
+ skipped_matches << next_match
598
+ rescue RequiresTooManySpecialPorts, Timeout::Error => e
599
+ config.log(
600
+ __method__,
601
+ {
602
+ message: e.message,
603
+ deleting_match: next_match[:name],
604
+ backtrace: e.backtrace
605
+ },
606
+ Logger::Severity::ERROR
607
+ )
608
+ else
609
+ running_matches_.push(
610
+ name: next_match[:name],
611
+ dealer: dealer_info,
612
+ players: player_info
613
+ )
614
+ update_running_matches game, running_matches_
615
+ end
616
+ update_enqueued_matches game, enqueued_matches_
617
+ end
618
+ end
619
+
620
+ def self.start_process(command, log_file = nil)
621
+ config.log __method__, running_command: command
622
+
623
+ options = {chdir: AcpcDealer::DEALER_DIRECTORY}
624
+ if log_file
625
+ options[[:err, :out]] = [log_file, File::CREAT|File::WRONLY|File::APPEND]
626
+ end
627
+
628
+ pid = Timeout.timeout(3) do
629
+ pid = Process.spawn(command, options)
630
+ Process.detach(pid)
631
+ pid
632
+ end
633
+
634
+ config.log __method__, ran_command: command, pid: pid
635
+
636
+ pid
637
+ end
16
638
  end