OSDN Git Service

Help the write_thread to terminate
[shogi-server/shogi-server.git] / shogi_server / player.rb
1 ## $Id$
2
3 ## Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch)
4 ## Copyright (C) 2007-2008 Daigo Moriwaki (daigo at debian dot org)
5 ##
6 ## This program is free software; you can redistribute it and/or modify
7 ## it under the terms of the GNU General Public License as published by
8 ## the Free Software Foundation; either version 2 of the License, or
9 ## (at your option) any later version.
10 ##
11 ## This program is distributed in the hope that it will be useful,
12 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
13 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 ## GNU General Public License for more details.
15 ##
16 ## You should have received a copy of the GNU General Public License
17 ## along with this program; if not, write to the Free Software
18 ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19
20 require 'shogi_server/command'
21
22 module ShogiServer # for a namespace
23
24 class BasicPlayer
25   def initialize
26     @player_id = nil
27     @name = nil
28     @password = nil
29     @rate = 0
30     @win  = 0
31     @loss = 0
32     @last_game_win = false
33     @sente = nil
34     @game_name = ""
35   end
36
37   # Idetifier of the player in the rating system
38   attr_accessor :player_id
39
40   # Name of the player
41   attr_accessor :name
42   
43   # Password of the player, which does not include a trip
44   attr_accessor :password
45
46   # Score in the rating sysem
47   attr_accessor :rate
48
49   # Number of games for win and loss in the rating system
50   attr_accessor :win, :loss
51   
52   # Group in the rating system
53   attr_accessor :rating_group
54
55   # Last timestamp when the rate was modified
56   attr_accessor :modified_at
57
58   # Whether win the previous game or not
59   attr_accessor :last_game_win
60
61   # true for Sente; false for Gote
62   attr_accessor :sente
63
64   # game name
65   attr_accessor :game_name
66
67   def is_human?
68     return [%r!_human$!, %r!_human@!].any? do |re|
69       re.match(@name)
70     end
71   end
72
73   def is_computer?
74     return !is_human?
75   end
76
77   def modified_at
78     @modified_at || Time.now
79   end
80
81   def rate=(new_rate)
82     if @rate != new_rate
83       @rate = new_rate
84       @modified_at = Time.now
85     end
86   end
87
88   def rated?
89     @player_id != nil
90   end
91
92   def last_game_win?
93     return @last_game_win
94   end
95
96   def simple_player_id
97     if @trip
98       simple_name = @name.gsub(/@.*?$/, '')
99       "%s+%s" % [simple_name, @trip[0..8]]
100     else
101       @name
102     end
103   end
104
105   ##
106   # Parses str in the LOGIN command, sets up @player_id and @trip
107   #
108   def set_password(str)
109     if str && !str.empty?
110       @password = str.strip
111       @player_id   = "%s+%s" % [@name, Digest::MD5.hexdigest(@password)]
112     else
113       @player_id = @password = nil
114     end
115   end
116 end
117
118
119 class Player < BasicPlayer
120   WRITE_THREAD_WATCH_INTERVAL = 20 # sec
121   def initialize(str, socket, eol=nil)
122     super()
123     @socket = socket
124     @status = "connected"       # game_waiting -> agree_waiting -> start_waiting -> game -> finished
125
126     @protocol = nil             # CSA or x1
127     @eol = eol || "\m"          # favorite eol code
128     @game = nil
129     @mytime = 0                 # set in start method also
130     @socket_buffer = []
131     @main_thread = Thread::current
132     @write_queue = ShogiServer::TimeoutQueue.new(WRITE_THREAD_WATCH_INTERVAL)
133     @player_logger = nil
134     start_write_thread
135   end
136
137   attr_accessor :socket, :status
138   attr_accessor :protocol, :eol, :game, :mytime
139   attr_accessor :main_thread
140   attr_reader :socket_buffer
141   
142   def setup_logger(dir)
143     log_file = File.join(dir, "%s.log" % [simple_player_id])
144     @player_logger = Logger.new(log_file, 'daily')
145     @player_logger.formatter = ShogiServer::Formatter.new
146     @player_logger.level = $DEBUG ? Logger::DEBUG : Logger::INFO  
147     @player_logger.datetime_format = "%Y-%m-%d %H:%M:%S"
148   end
149
150   def log(level, direction, message)
151     return unless @player_logger
152     str = message.chomp
153     case direction
154       when :in
155         str = "IN: %s" % [str]
156       when :out
157         str = "OUT: %s" % [str]
158       else
159         str = "UNKNOWN DIRECTION: %s %s" % [direction, str]
160     end
161     case level
162       when :debug
163         @player_logger.debug(str)
164       when :info
165         @player_logger.info(str)
166       when :warn
167         @player_logger.warn(str)
168       when :error
169         @player_logger.error(str)
170       else
171         @player_logger.debug("UNKNOWN LEVEL: %s %s" % [level, str])
172     end
173   rescue Exception => ex
174     log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
175   end
176
177   def kill
178     log_message(sprintf("user %s killed", @name))
179     if (@game)
180       @game.kill(self)
181     end
182     finish
183     Thread::kill(@main_thread)  if @main_thread
184     Thread::kill(@write_thread) if @write_thread
185   end
186
187   def finish
188     if (@status != "finished")
189       @status = "finished"
190       log_message(sprintf("user %s finish", @name))    
191       begin
192         log_debug("Terminating %s's write thread..." % [@name])
193         if @write_thread && @write_thread.alive?
194           write_safe(nil)
195           Thread.pass # help the write_thread to terminate
196         end
197         @player_logger.close if @player_logger
198         log_debug("done.")
199       rescue
200         log_message(sprintf("user %s finish failed", @name))    
201       end
202     end
203   end
204
205   def start_write_thread
206     @write_thread = Thread.start do
207       Thread.pass
208       while !@socket.closed?
209         begin
210           str = @write_queue.deq
211           if (str == nil)
212             log_debug("%s's write thread terminated" % [@name])
213             break
214           end
215           if (str == :timeout)
216             log_debug("%s's write queue timed out. Try again..." % [@name])
217             next
218           end
219
220           if r = select(nil, [@socket], nil, 20)
221             r[1].first.write(str)
222             log(:info, :out, str)
223           else
224             log_error("Gave a try to send a message to #{@name}, but it timed out.")
225             break
226           end
227         rescue Exception => ex
228           log_error("Failed to send a message to #{@name}. #{ex.class}: #{ex.message}\t#{ex.backtrace[0]}")
229           break
230         end
231       end # while loop
232       log_error("%s's socket closed." % [@name]) if @socket.closed?
233       log_message("At least %d messages are not sent to the client." % 
234                   [@write_queue.get_messages.size])
235     end # thread
236   end
237
238   #
239   # Note that sending a message is included in the giant lock.
240   #
241   def write_safe(str)
242     @write_queue.enq(str)
243   end
244
245   def to_s
246     if ["game_waiting", "start_waiting", "agree_waiting", "game"].include?(status)
247       if (@sente)
248         return sprintf("%s %s %s %s +", rated? ? @player_id : @name, @protocol, @status, @game_name)
249       elsif (@sente == false)
250         return sprintf("%s %s %s %s -", rated? ? @player_id : @name, @protocol, @status, @game_name)
251       elsif (@sente == nil)
252         return sprintf("%s %s %s %s *", rated? ? @player_id : @name, @protocol, @status, @game_name)
253       end
254     else
255       return sprintf("%s %s %s", rated? ? @player_id : @name, @protocol, @status)
256     end
257   end
258
259   def run(csa_1st_str=nil)
260     while ( csa_1st_str || 
261             str = gets_safe(@socket, (@socket_buffer.empty? ? Default_Timeout : 1)) )
262       time = Time.now
263       log(:info, :in, str) if str && str.instance_of?(String) 
264       $mutex.lock
265       begin
266         if !@write_thread.alive?
267           log_error("%s's write thread is dead. Aborting..." % [@name])
268           return
269         end
270         if (@game && @game.turn?(self))
271           @socket_buffer << str
272           str = @socket_buffer.shift
273         end
274         log_debug("%s (%s)" % [str, @socket_buffer.map {|a| String === a ? a.strip : a }.join(",")])
275
276         if (csa_1st_str)
277           str = csa_1st_str
278           csa_1st_str = nil
279         end
280
281         if (@status == "finished")
282           return
283         end
284         str.chomp! if (str.class == String) # may be strip! ?
285
286         delay = Time.now - time
287         if delay > 5
288           log_warning("Detected a long delay: %.2f sec" % [delay])
289         end
290         cmd = ShogiServer::Command.factory(str, self, time)
291         case cmd.call
292         when :return
293           return
294         when :continue
295           # do nothing
296         else
297           # TODO never reach
298         end
299
300       ensure
301         $mutex.unlock
302       end
303     end # enf of while
304     log_warning("%s's socket was suddenly closed" % [@name])
305   end # def run
306 end # class
307
308 end # ShogiServer