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'
40 #################################################
44 ONE_DAY = 3600 * 24 # in seconds
52 # - nil when a socket is closed
54 def gets_safe(socket, timeout=nil)
55 if r = select([socket], nil, nil, timeout)
56 return r[0].first.gets
60 rescue Exception => ex
61 log_error("gets_safe: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
68 shogi-server - server for CSA server protocol
71 shogi-server [OPTIONS] event_name port_number
74 server for CSA server protocol
78 a prefix of record files.
80 a port number for the server to listen.
82 --least-time-per-move n
83 Least time in second per move: 0, 1 (default 1).
84 - 0: The new rule that CSA introduced in November 2014.
85 - 1: The old rule before it.
87 maximum length of an identifier
89 when a game with the n-th move played does not end, make the game a draw.
90 Default 256. 0 disables this feature.
92 a file path in which a process ID will be written.
93 Use with --daemon option.
95 run as a daemon. Log files will be put in dir.
96 --floodgate-games game_A[,...]
97 enable Floodgate with various game names (separated by a comma)
99 enable to log network messages for players. Log files
100 will be put in the dir.
104 1. % ./shogi-server test 4081
105 Run the shogi-server. Then clients can connect to port#4081.
106 The server output logs to the stdout.
108 2. % ./shogi-server --max-moves 0 --least-time-per-move 1 test 4081
109 Run the shogi-server in compliance with CSA Protocol V1.1.2 or before.
111 3. % ./shogi-server --daemon . --pid-file ./shogi-server.pid \
112 --player-log-dir ./player-logs \
114 Run the shogi-server as a daemon. The server outputs regular logs
115 to shogi-server.log located in the current directory and network
116 messages in ./player-logs directory.
118 4. % ./shogi-server --daemon . --pid-file ./shogi-server.pid \
119 --player-log-dir ./player-logs \
120 --floodgate-games floodgate-900-0,floodgate-3600-0 \
122 Run the shogi-server with two groups of Floodgate games.
123 Configuration files allow you to schedule starting times. Consult
124 floodgate-0-240.conf.sample or shogi_server/league/floodgate.rb
129 A file named "STOP" in the base directory prevents the server from
130 starting new games including Floodgate matches.
131 When you want to stop the server gracefully, first, create a STOP file
135 then wait for a while until all the running games complete.
136 Now you can stop the process with no game interruptted by the 'kill'
139 Note that when a server gets started, a STOP file, if any, will be
140 deleted automatically.
142 FLOODGATE SCHEDULE CONFIGURATIONS
144 You need to set starting times of floodgate groups in
145 configuration files under the top directory. Each floodgate
146 group requires a corresponding configuration file named
147 "<game_name>.conf". The file will be re-read once just after a
150 For example, a floodgate-3600-30 game group requires
151 floodgate-3600-30.conf. However, for floodgate-900-0 and
152 floodgate-3600-0, which were default enabled in previous
153 versions, configuration files are optional if you are happy with
154 default time settings.
157 # This is a comment line
161 DoW := "Sun" | "Mon" | "Tue" | "Wed" | "Thu" | "Fri" | "Sat" |
162 "Sunday" | "Monday" | "Tuesday" | "Wednesday" | "Thursday" |
163 "Friday" | "Saturday"
173 In addition, this configuration file allows to set parameters
174 for the specific Floodaget group. A list of parameters is the
178 Specifies a factory function name generating a pairing
179 method which will be used in a specific Floodgate game.
180 ex. set pairing_factory floodgate_zyunisen
182 Specifies a sacrificed player.
183 ex. set sacrifice gps500+e293220e3f8a3e59f79f6b0efffaa931
186 GPL versoin 2 or later
191 #{ShogiServer::Revision}
217 # Parse command line options. Return a hash containing the option strings
218 # where a key is the option name without the first two slashes. For example,
219 # {"pid-file" => "foo.pid"}.
221 def parse_command_line
223 parser = GetoptLong.new(
224 ["--daemon", GetoptLong::REQUIRED_ARGUMENT],
225 ["--floodgate-games", GetoptLong::REQUIRED_ARGUMENT],
226 ["--least-time-per-move", GetoptLong::REQUIRED_ARGUMENT],
227 ["--max-identifier", GetoptLong::REQUIRED_ARGUMENT],
228 ["--max-moves", GetoptLong::REQUIRED_ARGUMENT],
229 ["--pid-file", GetoptLong::REQUIRED_ARGUMENT],
230 ["--player-log-dir", GetoptLong::REQUIRED_ARGUMENT])
233 parser.each_option do |name, arg|
235 options[name] = arg.dup
239 raise parser.error_message
244 # Check command line options.
245 # If any of them is invalid, exit the process.
247 def check_command_line
248 if (ARGV.length != 2)
253 if $options["daemon"]
254 $options["daemon"] = File.expand_path($options["daemon"], File.dirname(__FILE__))
255 unless is_writable_dir? $options["daemon"]
257 $stderr.puts "Can not create a file in the daemon directory: %s" % [$options["daemon"]]
262 $topdir = $options["daemon"] || File.expand_path(File.dirname(__FILE__))
264 if $options["player-log-dir"]
265 $options["player-log-dir"] = File.expand_path($options["player-log-dir"], $topdir)
266 unless is_writable_dir?($options["player-log-dir"])
268 $stderr.puts "Can not write a file in the player log dir: %s" % [$options["player-log-dir"]]
273 if $options["pid-file"]
274 $options["pid-file"] = File.expand_path($options["pid-file"], $topdir)
275 path = Pathname.new($options["pid-file"])
276 path.dirname().mkpath()
277 unless ShogiServer::is_writable_file? $options["pid-file"]
279 $stderr.puts "Can not create the pid file: %s" % [$options["pid-file"]]
284 if $options["floodgate-games"]
285 names = $options["floodgate-games"].split(",")
287 names.select do |name|
288 ShogiServer::League::Floodgate::game_name?(name)
290 if names.size != new_names.size
291 $stderr.puts "Found a wrong Floodgate game: %s" % [names.join(",")]
294 $options["floodgate-games"] = new_names
297 if $options["floodgate-history"]
298 $stderr.puts "WARNING: --floodgate-history has been deprecated."
299 $options["floodgate-history"] = nil
302 $options["max-moves"] ||= ShogiServer::Default_Max_Moves
303 $options["max-moves"] = $options["max-moves"].to_i
305 $options["max-identifier"] ||= ShogiServer::Default_Max_Identifier_Length
306 $options["max-identifier"] = $options["max-identifier"].to_i
308 $options["least-time-per-move"] ||= ShogiServer::Default_Least_Time_Per_Move
309 $options["least-time-per-move"] = $options["least-time-per-move"].to_i
312 # See if a file can be created in the directory.
313 # Return true if a file is writable in the directory, otherwise false.
315 def is_writable_dir?(dir)
316 unless File.directory? dir
323 temp_file = Tempfile.new("dummy-shogi-server", dir)
332 def write_pid_file(file)
333 open(file, "w") do |fh|
338 def mutex_watchdog(mutex, sec)
346 queue.push(Object.new)
349 log_error("mutex watchdog timeout: %d sec" % [sec])
357 def login_loop(client)
360 while r = select([client], nil, nil, ShogiServer::Login_Time) do
363 break unless str = r[0].first.gets
364 rescue Exception => ex
365 # It is posssible that the socket causes an error (ex. Errno::ECONNRESET)
366 log_error("login_loop: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
369 $mutex.lock # guards $league
373 if (ShogiServer::Login::good_login?(str))
374 player = ShogiServer::Player::new(str, client, eol)
375 login = ShogiServer::Login::factory(str, player)
376 if (current_player = $league.find(player.name))
377 # Even if a player is in the 'game' state, when the status of the
378 # player has not been updated for more than a day, it is very
379 # likely that the player is stalling. In such a case, a new player
380 # can override the current player.
381 if (current_player.password == player.password &&
382 (current_player.status != "game" ||
383 Time.now - current_player.last_command_at > ONE_DAY))
384 log_message("player %s login forcibly, nudging the former player" % [player.name])
385 log_message(" the former player was in %s and received the last command at %s" % [current_player.status, current_player.last_command_at])
388 login.incorrect_duplicated_player(str)
396 client.write("LOGIN:incorrect" + eol)
397 client.write("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
403 return [player, login]
406 def setup_logger(log_file)
407 logger = ShogiServer::Logger.new(log_file, 'daily')
408 logger.formatter = ShogiServer::Formatter.new
409 logger.level = $DEBUG ? Logger::DEBUG : Logger::INFO
410 logger.datetime_format = "%Y-%m-%d %H:%M:%S"
414 def setup_watchdog_for_giant_lock
418 mutex_watchdog($mutex, 10)
424 $options = parse_command_line
426 $config = ShogiServer::Config.new $options
428 $league = ShogiServer::League.new($topdir)
430 $league.event = ARGV.shift
433 log_file = $options["daemon"] ? File.join($options["daemon"], "shogi-server.log") : STDOUT
434 $logger = setup_logger(log_file)
436 $league.dir = $topdir
439 config[:BindAddress] = "0.0.0.0"
441 config[:ServerType] = WEBrick::Daemon if $options["daemon"]
442 config[:Logger] = $logger
444 setup_floodgate = nil
446 config[:StartCallback] = Proc.new do
448 if $options["pid-file"]
449 write_pid_file($options["pid-file"])
451 setup_watchdog_for_giant_lock
452 $league.setup_players_database
453 setup_floodgate = ShogiServer::SetupFloodgate.new($options["floodgate-games"])
454 setup_floodgate.start
457 config[:StopCallback] = Proc.new do
458 if $options["pid-file"]
459 FileUtils.rm($options["pid-file"], :force => true)
464 server = WEBrick::GenericServer.new(config)
465 ["INT", "TERM"].each do |signal|
471 unless (RUBY_PLATFORM.downcase =~ /mswin|mingw|cygwin|bccwin/)
476 $stderr.puts("server started as a deamon [Revision: #{ShogiServer::Revision}]") if $options["daemon"]
477 log_message("server started [Revision: #{ShogiServer::Revision}]")
479 if ShogiServer::STOP_FILE.exist?
480 log_message("Deleted the STOP file")
481 ShogiServer::STOP_FILE.delete
484 server.start do |client|
486 # client.sync = true # this is already set in WEBrick
487 client.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
488 # Keepalive time can be set by /proc/sys/net/ipv4/tcp_keepalive_time
489 player, login = login_loop(client) # loop
491 log_error("Detected a timed out login attempt")
495 log_message(sprintf("user %s login", player.name))
497 player.setup_logger($options["player-log-dir"]) if $options["player-log-dir"]
498 player.run(login.csa_1st_str) # loop
502 player.game.kill(player)
505 $league.delete(player)
506 log_message(sprintf("user %s logout", player.name))
510 player.wait_write_thread_finish(1000) # milliseconds
511 rescue Exception => ex
512 log_error("server.start: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
521 TCPSocket.do_not_reverse_lookup = true
522 Thread.abort_on_exception = $DEBUG ? true : false
526 rescue Exception => ex
528 log_error("main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
530 $stderr.puts "main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"