OSDN Git Service

Experimantal implementation for specified games, codenamed Buoy.
[shogi-server/shogi-server.git] / shogi-server
1 #! /usr/bin/env ruby
2 # $Id$
3 #
4 # Author:: NABEYA Kenichi, Daigo Moriwaki
5 # Homepage:: http://sourceforge.jp/projects/shogi-server/
6 #
7 #--
8 # Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch)
9 # Copyright (C) 2007-2008 Daigo Moriwaki (daigo at debian dot org)
10 #
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.
15 #
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.
20 #
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
24 #++
25 #
26 #
27
28 $topdir = nil
29 $league = nil
30 $logger = nil
31 $config = nil
32 $:.unshift File.dirname(__FILE__)
33 require 'shogi_server'
34 require 'shogi_server/config'
35 require 'tempfile'
36
37 #################################################
38 # MAIN
39 #
40
41 ShogiServer.reload
42
43 def gets_safe(socket, timeout=nil)
44   if r = select([socket], nil, nil, timeout)
45     return r[0].first.gets
46   else
47     return :timeout
48   end
49 rescue Exception => ex
50   log_error("gets_safe: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
51   return :exception
52 end
53
54 def usage
55     print <<EOM
56 NAME
57         shogi-server - server for CSA server protocol
58
59 SYNOPSIS
60         shogi-server [OPTIONS] event_name port_number
61
62 DESCRIPTION
63         server for CSA server protocol
64
65 OPTIONS
66         --pid-file file
67                 specify filename for logging process ID
68         --daemon dir
69                 run as a daemon. Log files will be put in dir.
70         --player-log-dir dir
71                 log network messages for each player. Log files
72                 will be put in the dir.
73         --floodgate-history
74                 file name to record Floodgate game history
75                 default: './floodgate_history.yaml'
76
77 LICENSE
78         GPL versoin 2 or later
79
80 SEE ALSO
81
82 RELEASE
83         #{ShogiServer::Release}
84
85 REVISION
86         #{ShogiServer::Revision}
87
88 EOM
89 end
90
91
92 def log_debug(str)
93   $logger.debug(str)
94 end
95
96 def log_message(str)
97   $logger.info(str)
98 end
99 def log_info(str)
100   log_message(str)
101 end
102
103 def log_warning(str)
104   $logger.warn(str)
105 end
106
107 def log_error(str)
108   $logger.error(str)
109 end
110
111
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"}.
115 #
116 def parse_command_line
117   options = Hash::new
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])
123   parser.quiet = true
124   begin
125     parser.each_option do |name, arg|
126       name.sub!(/^--/, '')
127       options[name] = arg.dup
128     end
129   rescue
130     usage
131     raise parser.error_message
132   end
133   return options
134 end
135
136 # Check command line options.
137 # If any of them is invalid, exit the process.
138 #
139 def check_command_line
140   if (ARGV.length != 2)
141     usage
142     exit 2
143   end
144
145   if $options["daemon"]
146     $options["daemon"] = File.expand_path($options["daemon"], File.dirname(__FILE__))
147     unless is_writable_dir? $options["daemon"]
148       usage
149       $stderr.puts "Can not create a file in the daemon directory: %s" % [$options["daemon"]]
150       exit 5
151     end
152   end
153
154   $topdir = $options["daemon"] || File.expand_path(File.dirname(__FILE__))
155
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"])
159       usage
160       $stderr.puts "Can not write a file in the player log dir: %s" % [$options["player-log-dir"]]
161       exit 3
162     end 
163   end
164
165   if $options["pid-file"] 
166     $options["pid-file"] = File.expand_path($options["pid-file"], $topdir)
167     unless is_writable_file? $options["pid-file"]
168       usage
169       $stderr.puts "Can not create the pid file: %s" % [$options["pid-file"]]
170       exit 4
171     end
172   end
173
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"]
177     usage
178     $stderr.puts "Can not create the floodgate history file: %s" % [$options["floodgate-history"]]
179     exit 6
180   end
181 end
182
183 # See if the file is writable. The file will be created if it does not exist
184 # yet.
185 # Return true if the file is writable, otherwise false.
186 #
187 def is_writable_file?(file)
188   if File.exist?(file)
189     if FileTest.file?(file)
190       return FileTest.writable_real?(file)
191     else
192       return false
193     end
194   end
195   
196   begin
197     open(file, "w") {|fh| } 
198     FileUtils.rm file
199   rescue
200     return false
201   end
202
203   return true
204 end
205
206 # See if a file can be created in the directory.
207 # Return true if a file is writable in the directory, otherwise false.
208 #
209 def is_writable_dir?(dir)
210   unless File.directory? dir
211     return false
212   end
213
214   result = true
215
216   begin
217     temp_file = Tempfile.new("dummy-shogi-server", dir)
218     temp_file.close true
219   rescue
220     result = false
221   end
222
223   return result
224 end
225
226 def write_pid_file(file)
227   open(file, "w") do |fh|
228     fh.puts "#{$$}"
229   end
230 end
231
232 def mutex_watchdog(mutex, sec)
233   sec = 1 if sec < 1
234   queue = []
235   while true
236     if mutex.try_lock
237       queue.clear
238       mutex.unlock
239     else
240       queue.push(Object.new)
241       if queue.size > sec
242         # timeout
243         log_error("mutex watchdog timeout: %d sec" % [sec])
244         queue.clear
245       end
246     end
247     sleep(1)
248   end
249 end
250
251 def login_loop(client)
252   player = login = nil
253  
254   while r = select([client], nil, nil, ShogiServer::Login_Time) do
255     break unless str = r[0].first.gets
256     $mutex.lock # guards $league
257     begin
258       str =~ /([\r\n]*)$/
259       eol = $1
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))
267             current_player.kill
268           else
269             login.incorrect_duplicated_player(str)
270             player = nil
271             break
272           end
273         end
274         $league.add(player)
275         break
276       else
277         client.write("LOGIN:incorrect" + eol)
278         client.write("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
279       end
280     ensure
281       $mutex.unlock
282     end
283   end                       # login loop
284   return [player, login]
285 end
286
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"
292   return logger
293 end
294
295 def setup_watchdog_for_giant_lock
296   $mutex = Mutex::new
297   Thread::start do
298     Thread.pass
299     mutex_watchdog($mutex, 10)
300   end
301 end
302
303 def setup_floodgate
304   return Thread.start do 
305     Thread.pass
306     floodgate = ShogiServer::League::Floodgate.new($league)
307     log_message("Flooddgate reloaded. The next match will start at %s." % 
308                 [floodgate.next_time])
309
310     while (true)
311       begin
312         diff = floodgate.next_time - Time.now
313         if diff > 0
314           sleep(diff/2)
315           next
316         end
317         $league.reload
318         floodgate.match_game
319         floodgate.charge
320         next_time = floodgate.next_time
321         $mutex.synchronize do
322           log_message("Reloading source...")
323           ShogiServer.reload
324         end
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 
329         # ignore errors
330         log_error("[in Floodgate's thread] #{ex} #{ex.backtrace}")
331       end
332     end
333   end
334 end
335
336 def main
337   
338   $options = parse_command_line
339   check_command_line
340   $config = ShogiServer::Config.new $options
341
342   $league = ShogiServer::League.new($topdir)
343
344   $league.event = ARGV.shift
345   port = ARGV.shift
346
347   log_file = $options["daemon"] ? File.join($options["daemon"], "shogi-server.log") : STDOUT
348   $logger = setup_logger(log_file)
349
350   $league.dir = $topdir
351
352   config = {}
353   config[:Port]       = port
354   config[:ServerType] = WEBrick::Daemon if $options["daemon"]
355   config[:Logger]     = $logger
356
357   fg_thread = nil
358
359   config[:StartCallback] = Proc.new do
360     srand
361     if $options["pid-file"]
362       write_pid_file($options["pid-file"])
363     end
364     setup_watchdog_for_giant_lock
365     $league.setup_players_database
366     fg_thread = setup_floodgate
367   end
368
369   config[:StopCallback] = Proc.new do
370     if $options["pid-file"]
371       FileUtils.rm($options["pid-file"], :force => true)
372     end
373   end
374
375   srand
376   server = WEBrick::GenericServer.new(config)
377   ["INT", "TERM"].each do |signal| 
378     trap(signal) do
379       server.shutdown
380       fg_thread.kill if fg_thread
381     end
382   end
383   trap("HUP") do
384     Dependencies.clear
385   end
386   $stderr.puts("server started as a deamon [Revision: #{ShogiServer::Revision}]") if $options["daemon"] 
387   log_message("server started [Revision: #{ShogiServer::Revision}]")
388
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
394       next unless player
395
396       log_message(sprintf("user %s login", player.name))
397       login.process
398       player.setup_logger($options["player-log-dir"]) if $options["player-log-dir"]
399       player.run(login.csa_1st_str) # loop
400       $mutex.lock
401       begin
402         if (player.game)
403           player.game.kill(player)
404         end
405         player.finish # socket has been closed
406         $league.delete(player)
407         log_message(sprintf("user %s logout", player.name))
408       ensure
409         $mutex.unlock
410       end
411   end
412 end
413
414
415 if ($0 == __FILE__)
416   STDOUT.sync = true
417   STDERR.sync = true
418   TCPSocket.do_not_reverse_lookup = true
419   Thread.abort_on_exception = $DEBUG ? true : false
420
421   begin
422     main
423   rescue Exception => ex
424     if $logger
425       log_error("main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
426     else
427       $stderr.puts "main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"
428     end
429   end
430 end