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-2008 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__)
33 require 'shogi_server'
34 require 'shogi_server/config'
37 #################################################
43 def gets_safe(socket, timeout=nil)
44 if r = select([socket], nil, nil, timeout)
45 return r[0].first.gets
49 rescue Exception => ex
50 log_error("gets_safe: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
57 shogi-server - server for CSA server protocol
60 shogi-server [OPTIONS] event_name port_number
63 server for CSA server protocol
67 specify filename for logging process ID
69 run as a daemon. Log files will be put in dir.
71 log network messages for each player. Log files
72 will be put in the dir.
74 file name to record Floodgate game history
75 default: './floodgate_history.yaml'
78 GPL versoin 2 or later
83 #{ShogiServer::Release}
86 #{ShogiServer::Revision}
112 # Parse command line options. Return a hash containing the option strings
113 # where a key is the option name without the first two slashes. For example,
114 # {"pid-file" => "foo.pid"}.
116 def parse_command_line
118 parser = GetoptLong.new(
119 ["--daemon", GetoptLong::REQUIRED_ARGUMENT],
120 ["--pid-file", GetoptLong::REQUIRED_ARGUMENT],
121 ["--player-log-dir", GetoptLong::REQUIRED_ARGUMENT],
122 ["--floodgate-history", GetoptLong::REQUIRED_ARGUMENT])
125 parser.each_option do |name, arg|
127 options[name] = arg.dup
131 raise parser.error_message
136 # Check command line options.
137 # If any of them is invalid, exit the process.
139 def check_command_line
140 if (ARGV.length != 2)
145 if $options["daemon"]
146 $options["daemon"] = File.expand_path($options["daemon"], File.dirname(__FILE__))
147 unless is_writable_dir? $options["daemon"]
149 $stderr.puts "Can not create a file in the daemon directory: %s" % [$options["daemon"]]
154 $topdir = $options["daemon"] || File.expand_path(File.dirname(__FILE__))
156 if $options["player-log-dir"]
157 $options["player-log-dir"] = File.expand_path($options["player-log-dir"], $topdir)
158 unless is_writable_dir?($options["player-log-dir"])
160 $stderr.puts "Can not write a file in the player log dir: %s" % [$options["player-log-dir"]]
165 if $options["pid-file"]
166 $options["pid-file"] = File.expand_path($options["pid-file"], $topdir)
167 unless is_writable_file? $options["pid-file"]
169 $stderr.puts "Can not create the pid file: %s" % [$options["pid-file"]]
174 $options["floodgate-history"] ||= File.join($topdir, "floodgate_history.yaml")
175 $options["floodgate-history"] = File.expand_path($options["floodgate-history"], $topdir)
176 unless is_writable_file? $options["floodgate-history"]
178 $stderr.puts "Can not create the floodgate history file: %s" % [$options["floodgate-history"]]
183 # See if the file is writable. The file will be created if it does not exist
185 # Return true if the file is writable, otherwise false.
187 def is_writable_file?(file)
189 if FileTest.file?(file)
190 return FileTest.writable_real?(file)
197 open(file, "w") {|fh| }
206 # See if a file can be created in the directory.
207 # Return true if a file is writable in the directory, otherwise false.
209 def is_writable_dir?(dir)
210 unless File.directory? dir
217 temp_file = Tempfile.new("dummy-shogi-server", dir)
226 def write_pid_file(file)
227 open(file, "w") do |fh|
232 def mutex_watchdog(mutex, sec)
240 queue.push(Object.new)
243 log_error("mutex watchdog timeout: %d sec" % [sec])
251 def login_loop(client)
254 while r = select([client], nil, nil, ShogiServer::Login_Time) do
255 break unless str = r[0].first.gets
256 $mutex.lock # guards $league
260 if (ShogiServer::Login::good_login?(str))
261 player = ShogiServer::Player::new(str, client, eol)
262 login = ShogiServer::Login::factory(str, player)
263 if (current_player = $league.find(player.name))
264 if (current_player.password == player.password &&
265 current_player.status != "game")
266 log_message(sprintf("user %s login forcely", player.name))
269 login.incorrect_duplicated_player(str)
277 client.write("LOGIN:incorrect" + eol)
278 client.write("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
284 return [player, login]
287 def setup_logger(log_file)
288 logger = ShogiServer::Logger.new(log_file, 'daily')
289 logger.formatter = ShogiServer::Formatter.new
290 logger.level = $DEBUG ? Logger::DEBUG : Logger::INFO
291 logger.datetime_format = "%Y-%m-%d %H:%M:%S"
295 def setup_watchdog_for_giant_lock
299 mutex_watchdog($mutex, 10)
304 return Thread.start do
306 floodgate = ShogiServer::League::Floodgate.new($league)
307 log_message("Flooddgate reloaded. The next match will start at %s." %
308 [floodgate.next_time])
312 diff = floodgate.next_time - Time.now
320 next_time = floodgate.next_time
321 $mutex.synchronize do
322 log_message("Reloading source...")
325 floodgate = ShogiServer::League::Floodgate.new($league, next_time)
326 log_message("Floodgate: The next match will start at %s." %
327 [floodgate.next_time])
328 rescue Exception => ex
330 log_error("[in Floodgate's thread] #{ex} #{ex.backtrace}")
338 $options = parse_command_line
340 $config = ShogiServer::Config.new $options
342 $league = ShogiServer::League.new($topdir)
344 $league.event = ARGV.shift
347 log_file = $options["daemon"] ? File.join($options["daemon"], "shogi-server.log") : STDOUT
348 $logger = setup_logger(log_file)
350 $league.dir = $topdir
354 config[:ServerType] = WEBrick::Daemon if $options["daemon"]
355 config[:Logger] = $logger
359 config[:StartCallback] = Proc.new do
361 if $options["pid-file"]
362 write_pid_file($options["pid-file"])
364 setup_watchdog_for_giant_lock
365 $league.setup_players_database
366 fg_thread = setup_floodgate
369 config[:StopCallback] = Proc.new do
370 if $options["pid-file"]
371 FileUtils.rm($options["pid-file"], :force => true)
376 server = WEBrick::GenericServer.new(config)
377 ["INT", "TERM"].each do |signal|
380 fg_thread.kill if fg_thread
386 $stderr.puts("server started as a deamon [Revision: #{ShogiServer::Revision}]") if $options["daemon"]
387 log_message("server started [Revision: #{ShogiServer::Revision}]")
389 server.start do |client|
390 # client.sync = true # this is already set in WEBrick
391 client.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
392 # Keepalive time can be set by /proc/sys/net/ipv4/tcp_keepalive_time
393 player, login = login_loop(client) # loop
396 log_message(sprintf("user %s login", player.name))
398 player.setup_logger($options["player-log-dir"]) if $options["player-log-dir"]
399 player.run(login.csa_1st_str) # loop
403 player.game.kill(player)
405 player.finish # socket has been closed
406 $league.delete(player)
407 log_message(sprintf("user %s logout", player.name))
418 TCPSocket.do_not_reverse_lookup = true
419 Thread.abort_on_exception = $DEBUG ? true : false
423 rescue Exception => ex
425 log_error("main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
427 $stderr.puts "main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"