OSDN Git Service

Merge remote-tracking branch 'origin/master'
[shogi-server/shogi-server.git] / shogi-server
1 #! /usr/bin/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-2012 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.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'
37 require 'pathname'
38 require 'set'
39 require 'tempfile'
40
41 #################################################
42 # MAIN
43 #
44
45 ONE_DAY = 3600 * 24   # in seconds
46
47 ShogiServer.reload
48
49 # Return
50 #   - a received string
51 #   - :timeout
52 #   - :exception
53 #   - nil when a socket is closed
54 #
55 def gets_safe(socket, timeout=nil)
56   if r = select([socket], nil, nil, timeout)
57     return r[0].first.gets
58   else
59     return :timeout
60   end
61 rescue Exception => ex
62   log_error("gets_safe: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
63   return :exception
64 end
65
66 def usage
67     print <<EOM
68 NAME
69         shogi-server - server for CSA server protocol
70
71 SYNOPSIS
72         shogi-server [OPTIONS] event_name port_number
73
74 DESCRIPTION
75         server for CSA server protocol
76
77 OPTIONS
78         event_name
79                 a prefix of record files.
80         port_number
81                 a port number for the server to listen. 
82                 4081 is often used.
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.
87         --max-identifier n
88                 maximum length of an identifier
89         --max-moves n
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.
92         --pid-file file
93                 a file path in which a process ID will be written.
94                 Use with --daemon option.
95         --daemon dir
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)
99         --player-log-dir dir
100                 enable to log network messages for players. Log files
101                 will be put in the dir.
102
103 EXAMPLES
104
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.
108
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.
111
112         3. % ./shogi-server --daemon . --pid-file ./shogi-server.pid \
113                             --player-log-dir ./player-logs \
114                             test 4081
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.
118
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 \
122                             test 4081
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 
126            for format details.
127
128 GRACEFUL SHUTDOWN
129
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
133
134           $ touch STOP
135
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'
138         command.
139
140         Note that when a server gets started, a STOP file, if any, will be
141         deleted automatically.
142
143 FLOODGATE SCHEDULE CONFIGURATIONS
144
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
149             game starts. 
150             
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.
156             File format is:
157               Line format: 
158                 # This is a comment line
159                 DoW Time
160                 ...
161               where
162                 DoW := "Sun" | "Mon" | "Tue" | "Wed" | "Thu" | "Fri" | "Sat" |
163                        "Sunday" | "Monday" | "Tuesday" | "Wednesday" | "Thursday" |
164                        "Friday" | "Saturday" 
165                 Time := HH:MM
166              
167               For example,
168                 Sat 13:00
169                 Sat 22:00
170                 Sun 13:00
171
172             PAREMETER SETTING
173
174             In addition, this configuration file allows to set parameters
175             for the specific Floodaget group. A list of parameters is the
176             following:
177
178             * pairing_factory:
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
182             * sacrifice:
183               Specifies a sacrificed player.
184               ex. set sacrifice gps500+e293220e3f8a3e59f79f6b0efffaa931
185
186 LICENSE
187         GPL versoin 2 or later
188
189 SEE ALSO
190
191 REVISION
192         #{ShogiServer::Revision}
193
194 EOM
195 end
196
197
198 def log_debug(str)
199   $logger.debug(str)
200 end
201
202 def log_message(str)
203   $logger.info(str)
204 end
205 def log_info(str)
206   log_message(str)
207 end
208
209 def log_warning(str)
210   $logger.warn(str)
211 end
212
213 def log_error(str)
214   $logger.error(str)
215 end
216
217
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"}.
221 #
222 def parse_command_line
223   options = Hash::new
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])
232   parser.quiet = true
233   begin
234     parser.each_option do |name, arg|
235       name.sub!(/^--/, '')
236       options[name] = arg.dup
237     end
238   rescue
239     usage
240     raise parser.error_message
241   end
242   return options
243 end
244
245 # Check command line options.
246 # If any of them is invalid, exit the process.
247 #
248 def check_command_line
249   if (ARGV.length != 2)
250     usage
251     exit 2
252   end
253
254   if $options["daemon"]
255     $options["daemon"] = File.expand_path($options["daemon"], File.dirname(__FILE__))
256     unless is_writable_dir? $options["daemon"]
257       usage
258       $stderr.puts "Can not create a file in the daemon directory: %s" % [$options["daemon"]]
259       exit 5
260     end
261   end
262
263   $topdir = $options["daemon"] || File.expand_path(File.dirname(__FILE__))
264
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"])
268       usage
269       $stderr.puts "Can not write a file in the player log dir: %s" % [$options["player-log-dir"]]
270       exit 3
271     end 
272   end
273
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"]
279       usage
280       $stderr.puts "Can not create the pid file: %s" % [$options["pid-file"]]
281       exit 4
282     end
283   end
284
285   if $options["floodgate-games"]
286     names = $options["floodgate-games"].split(",")
287     new_names = 
288       names.select do |name|
289         ShogiServer::League::Floodgate::game_name?(name)
290       end
291     if names.size != new_names.size
292       $stderr.puts "Found a wrong Floodgate game: %s" % [names.join(",")]
293       exit 6
294     end
295     $options["floodgate-games"] = new_names
296   end
297
298   if $options["floodgate-history"]
299     $stderr.puts "WARNING: --floodgate-history has been deprecated."
300     $options["floodgate-history"] = nil
301   end
302
303   $options["max-moves"] ||= ShogiServer::Default_Max_Moves
304   $options["max-moves"] = $options["max-moves"].to_i
305
306   $options["max-identifier"] ||= ShogiServer::Default_Max_Identifier_Length
307   $options["max-identifier"] = $options["max-identifier"].to_i
308
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
311 end
312
313 # See if a file can be created in the directory.
314 # Return true if a file is writable in the directory, otherwise false.
315 #
316 def is_writable_dir?(dir)
317   unless File.directory? dir
318     return false
319   end
320
321   result = true
322
323   begin
324     temp_file = Tempfile.new("dummy-shogi-server", dir)
325     temp_file.close true
326   rescue
327     result = false
328   end
329
330   return result
331 end
332
333 def write_pid_file(file)
334   open(file, "w") do |fh|
335     fh.puts "#{$$}"
336   end
337 end
338
339 def mutex_watchdog(mutex, sec)
340   sec = 1 if sec < 1
341   queue = []
342   while true
343     if mutex.try_lock
344       queue.clear
345       mutex.unlock
346     else
347       queue.push(Object.new)
348       if queue.size > sec
349         # timeout
350         log_error("mutex watchdog timeout: %d sec" % [sec])
351         queue.clear
352       end
353     end
354     sleep(1)
355   end
356 end
357
358 def login_loop(client)
359   player = login = nil
360  
361   while r = select([client], nil, nil, ShogiServer::Login_Time) do
362     str = nil
363     begin
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]}")
368       break
369     end
370     $mutex.lock # guards $league
371     begin
372       str =~ /([\r\n]*)$/
373       eol = $1
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])
387             current_player.kill
388           else
389             login.incorrect_duplicated_player(str)
390             player = nil
391             break
392           end
393         end
394         $league.add(player)
395         break
396       else
397         client.write("LOGIN:incorrect" + eol)
398         client.write("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
399       end
400     ensure
401       $mutex.unlock
402     end
403   end                       # login loop
404   return [player, login]
405 end
406
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"
412   return logger
413 end
414
415 def setup_watchdog_for_giant_lock
416   $mutex = Mutex::new
417   Thread::start do
418     Thread.pass
419     mutex_watchdog($mutex, 10)
420   end
421 end
422
423 def main
424   
425   $options = parse_command_line
426   check_command_line
427   $config = ShogiServer::Config.new $options
428
429   $league = ShogiServer::League.new($topdir)
430
431   $league.event = ARGV.shift
432   port = ARGV.shift
433
434   log_file = $options["daemon"] ? File.join($options["daemon"], "shogi-server.log") : STDOUT
435   $logger = setup_logger(log_file)
436
437   $league.dir = $topdir
438
439   # Set of connected players
440   $players = Set.new
441
442   config = {}
443   config[:BindAddress] = "0.0.0.0"
444   config[:Port]       = port
445   config[:ServerType] = WEBrick::Daemon if $options["daemon"]
446   config[:Logger]     = $logger
447
448   setup_floodgate = nil
449
450   config[:StartCallback] = Proc.new do
451     srand
452     if $options["pid-file"]
453       write_pid_file($options["pid-file"])
454     end
455     setup_watchdog_for_giant_lock
456     $league.setup_players_database
457     setup_floodgate = ShogiServer::SetupFloodgate.new($options["floodgate-games"])
458     setup_floodgate.start
459   end
460
461   config[:StopCallback] = Proc.new do
462     if $options["pid-file"]
463       FileUtils.rm($options["pid-file"], :force => true)
464     end
465   end
466
467   srand
468   server = WEBrick::GenericServer.new(config)
469   ["INT", "TERM"].each do |signal|
470     trap(signal) do
471       $players.each {|p| p.kill}
472       server.shutdown
473       setup_floodgate.kill
474     end
475   end
476   unless (RUBY_PLATFORM.downcase =~ /mswin|mingw|cygwin|bccwin/)
477     trap("HUP") do
478       Dependencies.clear
479     end
480   end
481   $stderr.puts("server started as a deamon [Revision: #{ShogiServer::Revision}]") if $options["daemon"] 
482   log_message("server started [Revision: #{ShogiServer::Revision}]")
483
484   if ShogiServer::STOP_FILE.exist?
485     log_message("Deleted the STOP file")
486     ShogiServer::STOP_FILE.delete
487   end
488
489   server.start do |client|
490     begin
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
495       unless player
496         log_error("Detected a timed out login attempt")
497         next
498       end
499
500       log_message(sprintf("user %s login", player.name))
501       login.process
502       player.setup_logger($options["player-log-dir"]) if $options["player-log-dir"]
503
504       $mutex.lock
505       begin
506         $players.add(player)
507       ensure
508         $mutex.unlock
509       end
510
511       player.run(login.csa_1st_str) # loop
512       $mutex.lock
513       begin
514         if (player.game)
515           player.game.kill(player)
516         end
517         player.finish
518         $league.delete(player)
519         log_message(sprintf("user %s logout", player.name))
520         $players.delete(player)
521       ensure
522         $mutex.unlock
523       end
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]}")
527     end
528   end
529 end
530
531
532 if ($0 == __FILE__)
533   STDOUT.sync = true
534   STDERR.sync = true
535   TCPSocket.do_not_reverse_lookup = true
536   Thread.abort_on_exception = $DEBUG ? true : false
537
538   begin
539     main
540   rescue Exception => ex
541     if $logger
542       log_error("main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
543     else
544       $stderr.puts "main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"
545     end
546   end
547 end