OSDN Git Service

07e42a63adbac4653eff2ffc8dd48c614984ff08
[shogi-server/shogi-server.git] / shogi-server
1 #! /usr/bin/env ruby
2 ## $Id$
3
4 ## Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch)
5 ## Copyright (C) 2007-2008 Daigo Moriwaki (daigo at debian dot org)
6 ##
7 ## This program is free software; you can redistribute it and/or modify
8 ## it under the terms of the GNU General Public License as published by
9 ## the Free Software Foundation; either version 2 of the License, or
10 ## (at your option) any later version.
11 ##
12 ## This program is distributed in the hope that it will be useful,
13 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
14 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 ## GNU General Public License for more details.
16 ##
17 ## You should have received a copy of the GNU General Public License
18 ## along with this program; if not, write to the Free Software
19 ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20
21 TOP_DIR = File.expand_path(File.dirname(__FILE__))
22 $:.unshift File.dirname(__FILE__)
23 require 'shogi_server'
24
25 #################################################
26 # MAIN
27 #
28
29 ShogiServer.reload
30
31 def gets_safe(socket, timeout=nil)
32   if r = select([socket], nil, nil, timeout)
33     return r[0].first.gets
34   else
35     return :timeout
36   end
37 rescue Exception => ex
38   log_error("gets_safe: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
39   return :exception
40 end
41
42 def usage
43     print <<EOM
44 NAME
45         shogi-server - server for CSA server protocol
46
47 SYNOPSIS
48         shogi-server [OPTIONS] event_name port_number
49
50 DESCRIPTION
51         server for CSA server protocol
52
53 OPTIONS
54         --pid-file file
55                 specify filename for logging process ID
56         --daemon dir
57                 run as a daemon. Log files will be put in dir.
58         --player-log-dir dir
59                 log network messages for each player. Log files
60                 will be put in the dir.
61         --floodgate_history
62                 file name to record Floodgate game history
63                 default: './floodgate_history.yaml'
64
65 LICENSE
66         GPL versoin 2 or later
67
68 SEE ALSO
69
70 RELEASE
71         #{ShogiServer::Release}
72
73 REVISION
74         #{ShogiServer::Revision}
75 EOM
76 end
77
78
79 def log_debug(str)
80   $logger.debug(str)
81 end
82
83 def log_message(str)
84   $logger.info(str)
85 end
86
87 def log_warning(str)
88   $logger.warn(str)
89 end
90
91 def log_error(str)
92   $logger.error(str)
93 end
94
95
96 def parse_command_line
97   options = Hash::new
98   parser = GetoptLong.new(
99     ["--daemon",   GetoptLong::REQUIRED_ARGUMENT],
100     ["--pid-file", GetoptLong::REQUIRED_ARGUMENT],
101     ["--player-log-dir", GetoptLong::REQUIRED_ARGUMENT])
102   parser.quiet = true
103   begin
104     parser.each_option do |name, arg|
105       name.sub!(/^--/, '')
106       options[name] = arg.dup
107     end
108   rescue
109     usage
110     raise parser.error_message
111   end
112   return options
113 end
114
115 def write_pid_file(file)
116   open(file, "w") do |fh|
117     fh.puts "#{$$}"
118   end
119 end
120
121 def mutex_watchdog(mutex, sec)
122   sec = 1 if sec < 1
123   queue = []
124   while true
125     if mutex.try_lock
126       queue.clear
127       mutex.unlock
128     else
129       queue.push(Object.new)
130       if queue.size > sec
131         # timeout
132         log_error("mutex watchdog timeout: %d sec" % [sec])
133         queue.clear
134       end
135     end
136     sleep(1)
137   end
138 end
139
140 def login_loop(client)
141   player = login = nil
142  
143   while r = select([client], nil, nil, ShogiServer::Login_Time) do
144     break unless str = r[0].first.gets
145     $mutex.lock # guards LEAGUE
146     begin
147       str =~ /([\r\n]*)$/
148       eol = $1
149       if (ShogiServer::Login::good_login?(str))
150         player = ShogiServer::Player::new(str, client, eol)
151         login  = ShogiServer::Login::factory(str, player)
152         if (current_player = LEAGUE.find(player.name))
153           if (current_player.password == player.password &&
154               current_player.status != "game")
155             log_message(sprintf("user %s login forcely", player.name))
156             current_player.kill
157           else
158             login.incorrect_duplicated_player(str)
159             player = nil
160             break
161           end
162         end
163         LEAGUE.add(player)
164         break
165       else
166         client.write("LOGIN:incorrect" + eol)
167         client.write("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
168       end
169     ensure
170       $mutex.unlock
171     end
172   end                       # login loop
173   return [player, login]
174 end
175
176 def setup_logger(log_file)
177   logger = Logger.new(log_file, 'daily')
178   logger.formatter = ShogiServer::Formatter.new
179   logger.level = $DEBUG ? Logger::DEBUG : Logger::INFO  
180   logger.datetime_format = "%Y-%m-%d %H:%M:%S"
181   return logger
182 end
183
184 def setup_watchdog_for_giant_lock
185   $mutex = Mutex::new
186   Thread::start do
187     Thread.pass
188     mutex_watchdog($mutex, 10)
189   end
190 end
191
192 def setup_floodgate
193   return Thread.start do 
194     Thread.pass
195     floodgate = ShogiServer::League::Floodgate.new(LEAGUE)
196     log_message("Flooddgate reloaded. The next match will start at %s." % 
197                 [floodgate.next_time])
198
199     while (true)
200       begin
201         diff = floodgate.next_time - Time.now
202         if diff > 0
203           sleep(diff/2)
204           next
205         end
206         LEAGUE.reload
207         floodgate.match_game
208         floodgate.charge
209         next_time = floodgate.next_time
210         $mutex.synchronize do
211           log_message("Reloading source...")
212           ShogiServer.reload
213         end
214         floodgate = ShogiServer::League::Floodgate.new(LEAGUE, next_time)
215         log_message("Floodgate will start the next match at %s." % 
216                     [floodgate.next_time])
217       rescue Exception => ex 
218         # ignore errors
219         log_error("[in Floodgate's thread] #{ex} #{ex.backtrace}")
220       end
221     end
222   end
223 end
224
225 def main
226   
227   $options = parse_command_line
228   if (ARGV.length != 2)
229     usage
230     exit 2
231   end
232   if $options["player-log-dir"]
233     $options["player-log-dir"] = File.expand_path($options["player-log-dir"])
234   end
235   if $options["player-log-dir"] && 
236      !File.directory?($options["player-log-dir"])
237     usage
238     exit 3
239   end
240   if $options["pid-file"] 
241     $options["pid-file"] = File.expand_path($options["pid-file"])
242   end
243   $options["floodgate-history"] ||= File.join(File.dirname(__FILE__), "floodgate_history.yaml")
244   $options["floodgate-history"] = File.expand_path($options["floodgate-history"])
245
246   LEAGUE.event = ARGV.shift
247   port = ARGV.shift
248
249   dir = $options["daemon"]
250   dir = File.expand_path(dir) if dir
251   if dir && ! File.exist?(dir)
252     FileUtils.mkdir(dir)
253   end
254
255   log_file = dir ? File.join(dir, "shogi-server.log") : STDOUT
256   $logger = setup_logger(log_file)
257
258   LEAGUE.dir = dir || TOP_DIR
259
260   config = {}
261   config[:Port]       = port
262   config[:ServerType] = WEBrick::Daemon if $options["daemon"]
263   config[:Logger]     = $logger
264
265   fg_thread = nil
266
267   config[:StartCallback] = Proc.new do
268     srand
269     if $options["pid-file"]
270       write_pid_file($options["pid-file"])
271     end
272     setup_watchdog_for_giant_lock
273     LEAGUE.setup_players_database
274     fg_thread = setup_floodgate
275   end
276
277   config[:StopCallback] = Proc.new do
278     if $options["pid-file"]
279       FileUtils.rm($options["pid-file"], :force => true)
280     end
281   end
282
283   server = WEBrick::GenericServer.new(config)
284   ["INT", "TERM"].each do |signal| 
285     trap(signal) do
286       server.shutdown
287       fg_thread.kill if fg_thread
288     end
289   end
290   trap("HUP") do
291     Dependencies.clear
292   end
293   $stderr.puts("server started as a deamon [Revision: #{ShogiServer::Revision}]") if $options["daemon"] 
294   log_message("server started [Revision: #{ShogiServer::Revision}]")
295
296   server.start do |client|
297       # client.sync = true # this is already set in WEBrick 
298       client.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
299         # Keepalive time can be set by /proc/sys/net/ipv4/tcp_keepalive_time
300       player, login = login_loop(client) # loop
301       next unless player
302
303       log_message(sprintf("user %s login", player.name))
304       login.process
305       player.setup_logger($options["player-log-dir"]) if $options["player-log-dir"]
306       player.run(login.csa_1st_str) # loop
307       $mutex.lock
308       begin
309         if (player.game)
310           player.game.kill(player)
311         end
312         player.finish # socket has been closed
313         LEAGUE.delete(player)
314         log_message(sprintf("user %s logout", player.name))
315       ensure
316         $mutex.unlock
317       end
318   end
319 end
320
321
322 if ($0 == __FILE__)
323   STDOUT.sync = true
324   STDERR.sync = true
325   TCPSocket.do_not_reverse_lookup = true
326   Thread.abort_on_exception = $DEBUG ? true : false
327
328   begin
329     LEAGUE = ShogiServer::League.new(TOP_DIR)
330     main
331   rescue Exception => ex
332     log_error("main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
333   end
334 end