4 # Author:: NABEYA Kenichi, Daigo Moriwaki
5 # Homepage:: http://sourceforge.jp/projects/shogi-server/
8 # Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch)
9 # Copyright (C) 2007-2012 Daigo Moriwaki (daigo at debian dot org)
11 # This program is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
21 # You should have received a copy of the GNU General Public License
22 # along with this program; if not, write to the Free Software
23 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
32 $:.unshift(File.dirname(File.expand_path(__FILE__)))
33 require 'shogi_server'
34 require 'shogi_server/config'
35 require 'shogi_server/util'
36 require 'shogi_server/league/floodgate_thread.rb'
39 #################################################
43 ONE_DAY = 3600 * 24 # in seconds
51 # - nil when a socket is closed
53 def gets_safe(socket, timeout=nil)
54 if r = select([socket], nil, nil, timeout)
55 return r[0].first.gets
59 rescue Exception => ex
60 log_error("gets_safe: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
67 shogi-server - server for CSA server protocol
70 shogi-server [OPTIONS] event_name port_number
73 server for CSA server protocol
77 a prefix of record files.
79 a port number for the server to listen.
81 --least-time-per-move n
82 Least time in second per move: 0, 1 (default 1).
83 - 0: The new rule that CSA introduced in November 2014.
84 - 1: The old rule before it.
86 maximum length of an identifier
88 when a game with the n-th move played does not end, make the game a draw.
89 Default 256. 0 disables this feature.
91 a file path in which a process ID will be written.
92 Use with --daemon option.
94 run as a daemon. Log files will be put in dir.
95 --floodgate-games game_A[,...]
96 enable Floodgate with various game names (separated by a comma)
98 enable to log network messages for players. Log files
99 will be put in the dir.
103 1. % ./shogi-server test 4081
104 Run the shogi-server. Then clients can connect to port#4081.
105 The server output logs to the stdout.
107 2. % ./shogi-server --max-moves 0 --least-time-per-move 1 test 4081
108 Run the shogi-server in compliance with CSA Protocol V1.1.2 or before.
110 3. % ./shogi-server --daemon . --pid-file ./shogi-server.pid \
111 --player-log-dir ./player-logs \
113 Run the shogi-server as a daemon. The server outputs regular logs
114 to shogi-server.log located in the current directory and network
115 messages in ./player-logs directory.
117 4. % ./shogi-server --daemon . --pid-file ./shogi-server.pid \
118 --player-log-dir ./player-logs \
119 --floodgate-games floodgate-900-0,floodgate-3600-0 \
121 Run the shogi-server with two groups of Floodgate games.
122 Configuration files allow you to schedule starting times. Consult
123 floodgate-0-240.conf.sample or shogi_server/league/floodgate.rb
128 A file named "STOP" in the base directory prevents the server from
129 starting new games including Floodgate matches.
130 When you want to stop the server gracefully, first, create a STOP file
134 then wait for a while until all the running games complete.
135 Now you can stop the process with no game interruptted by the 'kill'
138 Note that when a server gets started, a STOP file, if any, will be
139 deleted automatically.
141 FLOODGATE SCHEDULE CONFIGURATIONS
143 You need to set starting times of floodgate groups in
144 configuration files under the top directory. Each floodgate
145 group requires a corresponding configuration file named
146 "<game_name>.conf". The file will be re-read once just after a
149 For example, a floodgate-3600-30 game group requires
150 floodgate-3600-30.conf. However, for floodgate-900-0 and
151 floodgate-3600-0, which were default enabled in previous
152 versions, configuration files are optional if you are happy with
153 default time settings.
156 # This is a comment line
160 DoW := "Sun" | "Mon" | "Tue" | "Wed" | "Thu" | "Fri" | "Sat" |
161 "Sunday" | "Monday" | "Tuesday" | "Wednesday" | "Thursday" |
162 "Friday" | "Saturday"
172 In addition, this configuration file allows to set parameters
173 for the specific Floodaget group. A list of parameters is the
177 Specifies a factory function name generating a pairing
178 method which will be used in a specific Floodgate game.
179 ex. set pairing_factory floodgate_zyunisen
181 Specifies a sacrificed player.
182 ex. set sacrifice gps500+e293220e3f8a3e59f79f6b0efffaa931
185 GPL versoin 2 or later
190 #{ShogiServer::Revision}
216 # Parse command line options. Return a hash containing the option strings
217 # where a key is the option name without the first two slashes. For example,
218 # {"pid-file" => "foo.pid"}.
220 def parse_command_line
222 parser = GetoptLong.new(
223 ["--daemon", GetoptLong::REQUIRED_ARGUMENT],
224 ["--floodgate-games", GetoptLong::REQUIRED_ARGUMENT],
225 ["--least-time-per-move", GetoptLong::REQUIRED_ARGUMENT],
226 ["--max-identifier", GetoptLong::REQUIRED_ARGUMENT],
227 ["--max-moves", GetoptLong::REQUIRED_ARGUMENT],
228 ["--pid-file", GetoptLong::REQUIRED_ARGUMENT],
229 ["--player-log-dir", GetoptLong::REQUIRED_ARGUMENT])
232 parser.each_option do |name, arg|
234 options[name] = arg.dup
238 raise parser.error_message
243 # Check command line options.
244 # If any of them is invalid, exit the process.
246 def check_command_line
247 if (ARGV.length != 2)
252 if $options["daemon"]
253 $options["daemon"] = File.expand_path($options["daemon"], File.dirname(__FILE__))
254 unless is_writable_dir? $options["daemon"]
256 $stderr.puts "Can not create a file in the daemon directory: %s" % [$options["daemon"]]
261 $topdir = $options["daemon"] || File.expand_path(File.dirname(__FILE__))
263 if $options["player-log-dir"]
264 $options["player-log-dir"] = File.expand_path($options["player-log-dir"], $topdir)
265 unless is_writable_dir?($options["player-log-dir"])
267 $stderr.puts "Can not write a file in the player log dir: %s" % [$options["player-log-dir"]]
272 if $options["pid-file"]
273 $options["pid-file"] = File.expand_path($options["pid-file"], $topdir)
274 unless ShogiServer::is_writable_file? $options["pid-file"]
276 $stderr.puts "Can not create the pid file: %s" % [$options["pid-file"]]
281 if $options["floodgate-games"]
282 names = $options["floodgate-games"].split(",")
284 names.select do |name|
285 ShogiServer::League::Floodgate::game_name?(name)
287 if names.size != new_names.size
288 $stderr.puts "Found a wrong Floodgate game: %s" % [names.join(",")]
291 $options["floodgate-games"] = new_names
294 if $options["floodgate-history"]
295 $stderr.puts "WARNING: --floodgate-history has been deprecated."
296 $options["floodgate-history"] = nil
299 $options["max-moves"] ||= ShogiServer::Default_Max_Moves
300 $options["max-moves"] = $options["max-moves"].to_i
302 $options["max-identifier"] ||= ShogiServer::Default_Max_Identifier_Length
303 $options["max-identifier"] = $options["max-identifier"].to_i
305 $options["least-time-per-move"] ||= ShogiServer::Default_Least_Time_Per_Move
306 $options["least-time-per-move"] = $options["least-time-per-move"].to_i
309 # See if a file can be created in the directory.
310 # Return true if a file is writable in the directory, otherwise false.
312 def is_writable_dir?(dir)
313 unless File.directory? dir
320 temp_file = Tempfile.new("dummy-shogi-server", dir)
329 def write_pid_file(file)
330 open(file, "w") do |fh|
335 def mutex_watchdog(mutex, sec)
343 queue.push(Object.new)
346 log_error("mutex watchdog timeout: %d sec" % [sec])
354 def login_loop(client)
357 while r = select([client], nil, nil, ShogiServer::Login_Time) do
360 break unless str = r[0].first.gets
361 rescue Exception => ex
362 # It is posssible that the socket causes an error (ex. Errno::ECONNRESET)
363 log_error("login_loop: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
366 $mutex.lock # guards $league
370 if (ShogiServer::Login::good_login?(str))
371 player = ShogiServer::Player::new(str, client, eol)
372 login = ShogiServer::Login::factory(str, player)
373 if (current_player = $league.find(player.name))
374 # Even if a player is in the 'game' state, when the status of the
375 # player has not been updated for more than a day, it is very
376 # likely that the player is stalling. In such a case, a new player
377 # can override the current player.
378 if (current_player.password == player.password &&
379 (current_player.status != "game" ||
380 Time.now - current_player.last_command_at > ONE_DAY))
381 log_message("player %s login forcibly, nudging the former player" % [player.name])
382 log_message(" the former player was in %s and received the last command at %s" % [current_player.status, current_player.last_command_at])
385 login.incorrect_duplicated_player(str)
393 client.write("LOGIN:incorrect" + eol)
394 client.write("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
400 return [player, login]
403 def setup_logger(log_file)
404 logger = ShogiServer::Logger.new(log_file, 'daily')
405 logger.formatter = ShogiServer::Formatter.new
406 logger.level = $DEBUG ? Logger::DEBUG : Logger::INFO
407 logger.datetime_format = "%Y-%m-%d %H:%M:%S"
411 def setup_watchdog_for_giant_lock
415 mutex_watchdog($mutex, 10)
421 $options = parse_command_line
423 $config = ShogiServer::Config.new $options
425 $league = ShogiServer::League.new($topdir)
427 $league.event = ARGV.shift
430 log_file = $options["daemon"] ? File.join($options["daemon"], "shogi-server.log") : STDOUT
431 $logger = setup_logger(log_file)
433 $league.dir = $topdir
436 config[:BindAddress] = "0.0.0.0"
438 config[:ServerType] = WEBrick::Daemon if $options["daemon"]
439 config[:Logger] = $logger
441 setup_floodgate = nil
443 config[:StartCallback] = Proc.new do
445 if $options["pid-file"]
446 write_pid_file($options["pid-file"])
448 setup_watchdog_for_giant_lock
449 $league.setup_players_database
450 setup_floodgate = ShogiServer::SetupFloodgate.new($options["floodgate-games"])
451 setup_floodgate.start
454 config[:StopCallback] = Proc.new do
455 if $options["pid-file"]
456 FileUtils.rm($options["pid-file"], :force => true)
461 server = WEBrick::GenericServer.new(config)
462 ["INT", "TERM"].each do |signal|
468 unless (RUBY_PLATFORM.downcase =~ /mswin|mingw|cygwin|bccwin/)
473 $stderr.puts("server started as a deamon [Revision: #{ShogiServer::Revision}]") if $options["daemon"]
474 log_message("server started [Revision: #{ShogiServer::Revision}]")
476 if ShogiServer::STOP_FILE.exist?
477 log_message("Deleted the STOP file")
478 ShogiServer::STOP_FILE.delete
481 server.start do |client|
483 # client.sync = true # this is already set in WEBrick
484 client.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
485 # Keepalive time can be set by /proc/sys/net/ipv4/tcp_keepalive_time
486 player, login = login_loop(client) # loop
488 log_error("Detected a timed out login attempt")
492 log_message(sprintf("user %s login", player.name))
494 player.setup_logger($options["player-log-dir"]) if $options["player-log-dir"]
495 player.run(login.csa_1st_str) # loop
499 player.game.kill(player)
502 $league.delete(player)
503 log_message(sprintf("user %s logout", player.name))
507 player.wait_write_thread_finish(1000) # milliseconds
508 rescue Exception => ex
509 log_error("server.start: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
518 TCPSocket.do_not_reverse_lookup = true
519 Thread.abort_on_exception = $DEBUG ? true : false
523 rescue Exception => ex
525 log_error("main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
527 $stderr.puts "main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"