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'
41 #################################################
45 ONE_DAY = 3600 * 24 # in seconds
53 # - nil when a socket is closed
55 def gets_safe(socket, timeout=nil)
56 if r = select([socket], nil, nil, timeout)
57 return r[0].first.gets
61 rescue Exception => ex
62 log_error("gets_safe: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
69 shogi-server - server for CSA server protocol
72 shogi-server [OPTIONS] event_name port_number
75 server for CSA server protocol
79 a prefix of record files.
81 a port number for the server to listen.
83 --least-time-per-move n
84 Least time in second per move: 0, 1 (default 1).
85 - 0: The new rule that CSA introduced in November 2014.
86 - 1: The old rule before it.
88 maximum length of an identifier
90 when a game with the n-th move played does not end, make the game a draw.
91 Default 256. 0 disables this feature.
93 a file path in which a process ID will be written.
94 Use with --daemon option.
96 run as a daemon. Log files will be put in dir.
97 --floodgate-games game_A[,...]
98 enable Floodgate with various game names (separated by a comma)
100 enable to log network messages for players. Log files
101 will be put in the dir.
105 1. % ./shogi-server test 4081
106 Run the shogi-server. Then clients can connect to port#4081.
107 The server output logs to the stdout.
109 2. % ./shogi-server --max-moves 0 --least-time-per-move 1 test 4081
110 Run the shogi-server in compliance with CSA Protocol V1.1.2 or before.
112 3. % ./shogi-server --daemon . --pid-file ./shogi-server.pid \
113 --player-log-dir ./player-logs \
115 Run the shogi-server as a daemon. The server outputs regular logs
116 to shogi-server.log located in the current directory and network
117 messages in ./player-logs directory.
119 4. % ./shogi-server --daemon . --pid-file ./shogi-server.pid \
120 --player-log-dir ./player-logs \
121 --floodgate-games floodgate-900-0,floodgate-3600-0 \
123 Run the shogi-server with two groups of Floodgate games.
124 Configuration files allow you to schedule starting times. Consult
125 floodgate-0-240.conf.sample or shogi_server/league/floodgate.rb
130 A file named "STOP" in the base directory prevents the server from
131 starting new games including Floodgate matches.
132 When you want to stop the server gracefully, first, create a STOP file
136 then wait for a while until all the running games complete.
137 Now you can stop the process with no game interruptted by the 'kill'
140 Note that when a server gets started, a STOP file, if any, will be
141 deleted automatically.
143 FLOODGATE SCHEDULE CONFIGURATIONS
145 You need to set starting times of floodgate groups in
146 configuration files under the top directory. Each floodgate
147 group requires a corresponding configuration file named
148 "<game_name>.conf". The file will be re-read once just after a
151 For example, a floodgate-3600-30 game group requires
152 floodgate-3600-30.conf. However, for floodgate-900-0 and
153 floodgate-3600-0, which were default enabled in previous
154 versions, configuration files are optional if you are happy with
155 default time settings.
158 # This is a comment line
162 DoW := "Sun" | "Mon" | "Tue" | "Wed" | "Thu" | "Fri" | "Sat" |
163 "Sunday" | "Monday" | "Tuesday" | "Wednesday" | "Thursday" |
164 "Friday" | "Saturday"
174 In addition, this configuration file allows to set parameters
175 for the specific Floodaget group. A list of parameters is the
179 Specifies a factory function name generating a pairing
180 method which will be used in a specific Floodgate game.
181 ex. set pairing_factory floodgate_zyunisen
183 Specifies a sacrificed player.
184 ex. set sacrifice gps500+e293220e3f8a3e59f79f6b0efffaa931
187 GPL versoin 2 or later
192 #{ShogiServer::Revision}
218 # Parse command line options. Return a hash containing the option strings
219 # where a key is the option name without the first two slashes. For example,
220 # {"pid-file" => "foo.pid"}.
222 def parse_command_line
224 parser = GetoptLong.new(
225 ["--daemon", GetoptLong::REQUIRED_ARGUMENT],
226 ["--floodgate-games", GetoptLong::REQUIRED_ARGUMENT],
227 ["--least-time-per-move", GetoptLong::REQUIRED_ARGUMENT],
228 ["--max-identifier", GetoptLong::REQUIRED_ARGUMENT],
229 ["--max-moves", GetoptLong::REQUIRED_ARGUMENT],
230 ["--pid-file", GetoptLong::REQUIRED_ARGUMENT],
231 ["--player-log-dir", GetoptLong::REQUIRED_ARGUMENT])
234 parser.each_option do |name, arg|
236 options[name] = arg.dup
240 raise parser.error_message
245 # Check command line options.
246 # If any of them is invalid, exit the process.
248 def check_command_line
249 if (ARGV.length != 2)
254 if $options["daemon"]
255 $options["daemon"] = File.expand_path($options["daemon"], File.dirname(__FILE__))
256 unless is_writable_dir? $options["daemon"]
258 $stderr.puts "Can not create a file in the daemon directory: %s" % [$options["daemon"]]
263 $topdir = $options["daemon"] || File.expand_path(File.dirname(__FILE__))
265 if $options["player-log-dir"]
266 $options["player-log-dir"] = File.expand_path($options["player-log-dir"], $topdir)
267 unless is_writable_dir?($options["player-log-dir"])
269 $stderr.puts "Can not write a file in the player log dir: %s" % [$options["player-log-dir"]]
274 if $options["pid-file"]
275 $options["pid-file"] = File.expand_path($options["pid-file"], $topdir)
276 path = Pathname.new($options["pid-file"])
277 path.dirname().mkpath()
278 unless ShogiServer::is_writable_file? $options["pid-file"]
280 $stderr.puts "Can not create the pid file: %s" % [$options["pid-file"]]
285 if $options["floodgate-games"]
286 names = $options["floodgate-games"].split(",")
288 names.select do |name|
289 ShogiServer::League::Floodgate::game_name?(name)
291 if names.size != new_names.size
292 $stderr.puts "Found a wrong Floodgate game: %s" % [names.join(",")]
295 $options["floodgate-games"] = new_names
298 if $options["floodgate-history"]
299 $stderr.puts "WARNING: --floodgate-history has been deprecated."
300 $options["floodgate-history"] = nil
303 $options["max-moves"] ||= ShogiServer::Default_Max_Moves
304 $options["max-moves"] = $options["max-moves"].to_i
306 $options["max-identifier"] ||= ShogiServer::Default_Max_Identifier_Length
307 $options["max-identifier"] = $options["max-identifier"].to_i
309 $options["least-time-per-move"] ||= ShogiServer::Default_Least_Time_Per_Move
310 $options["least-time-per-move"] = $options["least-time-per-move"].to_i
313 # See if a file can be created in the directory.
314 # Return true if a file is writable in the directory, otherwise false.
316 def is_writable_dir?(dir)
317 unless File.directory? dir
324 temp_file = Tempfile.new("dummy-shogi-server", dir)
333 def write_pid_file(file)
334 open(file, "w") do |fh|
339 def mutex_watchdog(mutex, sec)
347 queue.push(Object.new)
350 log_error("mutex watchdog timeout: %d sec" % [sec])
358 def login_loop(client)
361 while r = select([client], nil, nil, ShogiServer::Login_Time) do
364 break unless str = r[0].first.gets
365 rescue Exception => ex
366 # It is posssible that the socket causes an error (ex. Errno::ECONNRESET)
367 log_error("login_loop: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
370 $mutex.lock # guards $league
374 if (ShogiServer::Login::good_login?(str))
375 player = ShogiServer::Player::new(str, client, eol)
376 login = ShogiServer::Login::factory(str, player)
377 if (current_player = $league.find(player.name))
378 # Even if a player is in the 'game' state, when the status of the
379 # player has not been updated for more than a day, it is very
380 # likely that the player is stalling. In such a case, a new player
381 # can override the current player.
382 if (current_player.password == player.password &&
383 (current_player.status != "game" ||
384 Time.now - current_player.last_command_at > ONE_DAY))
385 log_message("player %s login forcibly, nudging the former player" % [player.name])
386 log_message(" the former player was in %s and received the last command at %s" % [current_player.status, current_player.last_command_at])
389 login.incorrect_duplicated_player(str)
397 client.write("LOGIN:incorrect" + eol)
398 client.write("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
404 return [player, login]
407 def setup_logger(log_file)
408 logger = ShogiServer::Logger.new(log_file, 'daily')
409 logger.formatter = ShogiServer::Formatter.new
410 logger.level = $DEBUG ? Logger::DEBUG : Logger::INFO
411 logger.datetime_format = "%Y-%m-%d %H:%M:%S"
415 def setup_watchdog_for_giant_lock
419 mutex_watchdog($mutex, 10)
425 $options = parse_command_line
427 $config = ShogiServer::Config.new $options
429 $league = ShogiServer::League.new($topdir)
431 $league.event = ARGV.shift
434 log_file = $options["daemon"] ? File.join($options["daemon"], "shogi-server.log") : STDOUT
435 $logger = setup_logger(log_file)
437 $league.dir = $topdir
439 # Set of connected players
443 config[:BindAddress] = nil # both IPv4 and IPv6
445 config[:ServerType] = WEBrick::Daemon if $options["daemon"]
446 config[:Logger] = $logger
448 setup_floodgate = nil
450 config[:StartCallback] = Proc.new do
452 if $options["pid-file"]
453 write_pid_file($options["pid-file"])
455 setup_watchdog_for_giant_lock
456 $league.setup_players_database
457 setup_floodgate = ShogiServer::SetupFloodgate.new($options["floodgate-games"])
458 setup_floodgate.start
461 config[:StopCallback] = Proc.new do
462 if $options["pid-file"]
463 FileUtils.rm($options["pid-file"], :force => true)
468 server = WEBrick::GenericServer.new(config)
469 ["INT", "TERM"].each do |signal|
471 $players.each {|p| p.kill}
476 unless (RUBY_PLATFORM.downcase =~ /mswin|mingw|cygwin|bccwin/)
481 $stderr.puts("server started as a deamon [Revision: #{ShogiServer::Revision}]") if $options["daemon"]
482 log_message("server started [Revision: #{ShogiServer::Revision}]")
484 if ShogiServer::STOP_FILE.exist?
485 log_message("Deleted the STOP file")
486 ShogiServer::STOP_FILE.delete
489 server.start do |client|
491 # client.sync = true # this is already set in WEBrick
492 client.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
493 # Keepalive time can be set by /proc/sys/net/ipv4/tcp_keepalive_time
494 player, login = login_loop(client) # loop
496 log_error("Detected a timed out login attempt")
500 log_message(sprintf("user %s login", player.name))
502 player.setup_logger($options["player-log-dir"]) if $options["player-log-dir"]
511 player.run(login.csa_1st_str) # loop
515 player.game.kill(player)
518 $league.delete(player)
519 log_message(sprintf("user %s logout", player.name))
520 $players.delete(player)
524 player.wait_write_thread_finish(1000) # milliseconds
525 rescue Exception => ex
526 log_error("server.start: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
535 TCPSocket.do_not_reverse_lookup = true
536 Thread.abort_on_exception = $DEBUG ? true : false
540 rescue Exception => ex
542 log_error("main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
544 $stderr.puts "main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"