OSDN Git Service

84594b5b3fc95bee5d0664fad8e633d9895a678a
[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 $:.unshift File.dirname(__FILE__)
22 require 'shogi_server'
23
24 #################################################
25 # MAIN
26 #
27
28 def gets_safe(socket, timeout=nil)
29   if r = select([socket], nil, nil, timeout)
30     return r[0].first.gets
31   else
32     return :timeout
33   end
34 rescue Exception => ex
35   log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
36   return :exception
37 end
38
39 def usage
40     print <<EOM
41 NAME
42         shogi-server - server for CSA server protocol
43
44 SYNOPSIS
45         shogi-server [OPTIONS] event_name port_number
46
47 DESCRIPTION
48         server for CSA server protocol
49
50 OPTIONS
51         --pid-file file
52                 specify filename for logging process ID
53         --daemon dir
54                 run as a daemon. Log files will be put in dir.
55         --player-log-dir dir
56                 log network messages for each player. Log files
57                 will be put in the dir.
58
59 LICENSE
60         GPL versoin 2 or later
61
62 SEE ALSO
63
64 RELEASE
65         #{ShogiServer::Release}
66
67 REVISION
68         #{ShogiServer::Revision}
69 EOM
70 end
71
72
73 def log_debug(str)
74   $logger.debug(str)
75 end
76
77 def log_message(str)
78   $logger.info(str)
79 end
80
81 def log_warning(str)
82   $logger.warn(str)
83 end
84
85 def log_error(str)
86   $logger.error(str)
87 end
88
89
90 def parse_command_line
91   options = Hash::new
92   parser = GetoptLong.new(
93     ["--daemon",   GetoptLong::REQUIRED_ARGUMENT],
94     ["--pid-file", GetoptLong::REQUIRED_ARGUMENT],
95     ["--player-log-dir", GetoptLong::REQUIRED_ARGUMENT])
96   parser.quiet = true
97   begin
98     parser.each_option do |name, arg|
99       name.sub!(/^--/, '')
100       options[name] = arg.dup
101     end
102   rescue
103     usage
104     raise parser.error_message
105   end
106   return options
107 end
108
109 def write_pid_file(file)
110   open(file, "w") do |fh|
111     fh.puts "#{$$}"
112   end
113 end
114
115 def mutex_watchdog(mutex, sec)
116   while true
117     begin
118       timeout(sec) do
119         begin
120           mutex.lock
121         ensure
122           mutex.unlock
123         end
124       end
125       sleep(sec)
126     rescue TimeoutError
127       log_error("mutex watchdog timeout")
128       exit(1)
129     end
130   end
131 end
132
133 def login_loop(client)
134   player = login = nil
135  
136   while r = select([client], nil, nil, ShogiServer::Login_Time) do
137     break unless str = r[0].first.gets
138     $mutex.lock # guards LEAGUE
139     begin
140       str =~ /([\r\n]*)$/
141       eol = $1
142       if (ShogiServer::Login::good_login?(str))
143         player = ShogiServer::Player::new(str, client, eol)
144         login  = ShogiServer::Login::factory(str, player)
145         if (current_player = LEAGUE.find(player.name))
146           if (current_player.password == player.password &&
147               current_player.status != "game")
148             log_message(sprintf("user %s login forcely", player.name))
149             current_player.kill
150           else
151             login.incorrect_duplicated_player(str)
152             player = nil
153             break
154           end
155         end
156         LEAGUE.add(player)
157         break
158       else
159         client.write("LOGIN:incorrect" + eol)
160         client.write("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
161       end
162     ensure
163       $mutex.unlock
164     end
165   end                       # login loop
166   return [player, login]
167 end
168
169 def setup_logger(log_file)
170   logger = Logger.new(log_file, 'daily')
171   logger.formatter = ShogiServer::Formatter.new
172   logger.level = $DEBUG ? Logger::DEBUG : Logger::INFO  
173   logger.datetime_format = "%Y-%m-%d %H:%M:%S"
174   return logger
175 end
176
177 def setup_watchdog_for_giant_lock
178   $mutex = Mutex::new
179   Thread::start do
180     Thread.pass
181     mutex_watchdog($mutex, 10)
182   end
183 end
184
185 def main
186
187   setup_watchdog_for_giant_lock
188
189   $options = parse_command_line
190   if (ARGV.length != 2)
191     usage
192     exit 2
193   end
194   if $options["player-log-dir"] && 
195      !File.exists?($options["player-log-dir"])
196     usage
197     exit 3
198   end
199   if $options["player-log-dir"]
200     $options["player-log-dir"] = File.expand_path($options["player-log-dir"])
201   end
202
203   LEAGUE.event = ARGV.shift
204   port = ARGV.shift
205
206   dir = $options["daemon"]
207   dir = File.expand_path(dir) if dir
208   if dir && ! File.exist?(dir)
209     FileUtils.mkdir(dir)
210   end
211
212   log_file = dir ? File.join(dir, "shogi-server.log") : STDOUT
213   $logger = setup_logger(log_file)
214
215   LEAGUE.dir = dir || File.dirname(__FILE__)
216   LEAGUE.setup_players_database
217
218   config = {}
219   config[:Port]       = port
220   config[:ServerType] = WEBrick::Daemon if $options["daemon"]
221   config[:Logger]     = $logger
222   if $options["pid-file"]
223     pid_file = File.expand_path($options["pid-file"])
224     config[:StartCallback] = Proc.new do
225       write_pid_file(pid_file)
226     end
227     config[:StopCallback] = Proc.new do
228       FileUtils.rm(pid_file, :force => true)
229     end
230   end
231
232   server = WEBrick::GenericServer.new(config)
233   ["INT", "TERM"].each do |signal| 
234     trap(signal) do
235       LEAGUE.shutdown
236       server.shutdown
237     end
238   end
239   trap("HUP") do
240     LEAGUE.shutdown
241     Dependencies.clear
242     LEAGUE.restart
243   end
244   $stderr.puts("server started as a deamon [Revision: #{ShogiServer::Revision}]") if $options["daemon"] 
245   log_message("server started [Revision: #{ShogiServer::Revision}]")
246
247   server.start do |client|
248       # client.sync = true # this is already set in WEBrick 
249       client.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
250         # Keepalive time can be set by /proc/sys/net/ipv4/tcp_keepalive_time
251       player, login = login_loop(client) # loop
252       next unless player
253
254       log_message(sprintf("user %s login", player.name))
255       login.process
256       player.setup_logger($options["player-log-dir"]) if $options["player-log-dir"]
257       player.run(login.csa_1st_str) # loop
258       $mutex.lock
259       begin
260         if (player.game)
261           player.game.kill(player)
262         end
263         player.finish # socket has been closed
264         LEAGUE.delete(player)
265         log_message(sprintf("user %s logout", player.name))
266       ensure
267         $mutex.unlock
268       end
269   end
270   $logger.close
271 end
272
273
274 if ($0 == __FILE__)
275   STDOUT.sync = true
276   STDERR.sync = true
277   TCPSocket.do_not_reverse_lookup = true
278   Thread.abort_on_exception = $DEBUG ? true : false
279
280   LEAGUE = ShogiServer::League::new
281   main
282 end