ファイル情報

Rev. 2d5cda5e9dde331e37d58133b688a8b2c6295345
サイズ 15,076 バイト
日時 2017-09-03 14:40:39
作者 Daigo Moriwaki
ログメッセージ

Merge remote-tracking branch 'origin/master' into wdoor-stable

内容

  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. $topdir = nil
  28. $league = nil
  29. $logger = nil
  30. $config = nil
  31. $:.unshift(File.dirname(File.expand_path(__FILE__)))
  32. require 'shogi_server'
  33. require 'shogi_server/config'
  34. require 'shogi_server/util'
  35. require 'shogi_server/league/floodgate_thread.rb'
  36. require 'tempfile'
  37. #################################################
  38. # MAIN
  39. #
  40. ONE_DAY = 3600 * 24 # in seconds
  41. ShogiServer.reload
  42. # Return
  43. # - a received string
  44. # - :timeout
  45. # - :exception
  46. # - nil when a socket is closed
  47. #
  48. def gets_safe(socket, timeout=nil)
  49. if r = select([socket], nil, nil, timeout)
  50. return r[0].first.gets
  51. else
  52. return :timeout
  53. end
  54. rescue Exception => ex
  55. log_error("gets_safe: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
  56. return :exception
  57. end
  58. def usage
  59. print <<EOM
  60. NAME
  61. shogi-server - server for CSA server protocol
  62. SYNOPSIS
  63. shogi-server [OPTIONS] event_name port_number
  64. DESCRIPTION
  65. server for CSA server protocol
  66. OPTIONS
  67. event_name
  68. a prefix of record files.
  69. port_number
  70. a port number for the server to listen.
  71. 4081 is often used.
  72. --least-time-per-move n
  73. Least time in second per move: 0, 1 (default 1).
  74. - 0: The new rule that CSA introduced in November 2014.
  75. - 1: The old rule before it.
  76. --max-identifier n
  77. maximum length of an identifier
  78. --max-moves n
  79. when a game with the n-th move played does not end, make the game a draw.
  80. Default 256. 0 disables this feature.
  81. --pid-file file
  82. a file path in which a process ID will be written.
  83. Use with --daemon option.
  84. --daemon dir
  85. run as a daemon. Log files will be put in dir.
  86. --floodgate-games game_A[,...]
  87. enable Floodgate with various game names (separated by a comma)
  88. --player-log-dir dir
  89. enable to log network messages for players. Log files
  90. will be put in the dir.
  91. EXAMPLES
  92. 1. % ./shogi-server test 4081
  93. Run the shogi-server. Then clients can connect to port#4081.
  94. The server output logs to the stdout.
  95. 2. % ./shogi-server --max-moves 0 --least-time-per-move 1 test 4081
  96. Run the shogi-server in compliance with CSA Protocol V1.1.2 or before.
  97. 3. % ./shogi-server --daemon . --pid-file ./shogi-server.pid \
  98. --player-log-dir ./player-logs \
  99. test 4081
  100. Run the shogi-server as a daemon. The server outputs regular logs
  101. to shogi-server.log located in the current directory and network
  102. messages in ./player-logs directory.
  103. 4. % ./shogi-server --daemon . --pid-file ./shogi-server.pid \
  104. --player-log-dir ./player-logs \
  105. --floodgate-games floodgate-900-0,floodgate-3600-0 \
  106. test 4081
  107. Run the shogi-server with two groups of Floodgate games.
  108. Configuration files allow you to schedule starting times. Consult
  109. floodgate-0-240.conf.sample or shogi_server/league/floodgate.rb
  110. for format details.
  111. FLOODGATE SCHEDULE CONFIGURATIONS
  112. You need to set starting times of floodgate groups in
  113. configuration files under the top directory. Each floodgate
  114. group requires a corresponding configuration file named
  115. "<game_name>.conf". The file will be re-read once just after a
  116. game starts.
  117. For example, a floodgate-3600-30 game group requires
  118. floodgate-3600-30.conf. However, for floodgate-900-0 and
  119. floodgate-3600-0, which were default enabled in previous
  120. versions, configuration files are optional if you are happy with
  121. default time settings.
  122. File format is:
  123. Line format:
  124. # This is a comment line
  125. DoW Time
  126. ...
  127. where
  128. DoW := "Sun" | "Mon" | "Tue" | "Wed" | "Thu" | "Fri" | "Sat" |
  129. "Sunday" | "Monday" | "Tuesday" | "Wednesday" | "Thursday" |
  130. "Friday" | "Saturday"
  131. Time := HH:MM
  132. For example,
  133. Sat 13:00
  134. Sat 22:00
  135. Sun 13:00
  136. PAREMETER SETTING
  137. In addition, this configuration file allows to set parameters
  138. for the specific Floodaget group. A list of parameters is the
  139. following:
  140. * pairing_factory:
  141. Specifies a factory function name generating a pairing
  142. method which will be used in a specific Floodgate game.
  143. ex. set pairing_factory floodgate_zyunisen
  144. * sacrifice:
  145. Specifies a sacrificed player.
  146. ex. set sacrifice gps500+e293220e3f8a3e59f79f6b0efffaa931
  147. LICENSE
  148. GPL versoin 2 or later
  149. SEE ALSO
  150. REVISION
  151. #{ShogiServer::Revision}
  152. EOM
  153. end
  154. def log_debug(str)
  155. $logger.debug(str)
  156. end
  157. def log_message(str)
  158. $logger.info(str)
  159. end
  160. def log_info(str)
  161. log_message(str)
  162. end
  163. def log_warning(str)
  164. $logger.warn(str)
  165. end
  166. def log_error(str)
  167. $logger.error(str)
  168. end
  169. # Parse command line options. Return a hash containing the option strings
  170. # where a key is the option name without the first two slashes. For example,
  171. # {"pid-file" => "foo.pid"}.
  172. #
  173. def parse_command_line
  174. options = Hash::new
  175. parser = GetoptLong.new(
  176. ["--daemon", GetoptLong::REQUIRED_ARGUMENT],
  177. ["--floodgate-games", GetoptLong::REQUIRED_ARGUMENT],
  178. ["--least-time-per-move", GetoptLong::REQUIRED_ARGUMENT],
  179. ["--max-identifier", GetoptLong::REQUIRED_ARGUMENT],
  180. ["--max-moves", GetoptLong::REQUIRED_ARGUMENT],
  181. ["--pid-file", GetoptLong::REQUIRED_ARGUMENT],
  182. ["--player-log-dir", GetoptLong::REQUIRED_ARGUMENT])
  183. parser.quiet = true
  184. begin
  185. parser.each_option do |name, arg|
  186. name.sub!(/^--/, '')
  187. options[name] = arg.dup
  188. end
  189. rescue
  190. usage
  191. raise parser.error_message
  192. end
  193. return options
  194. end
  195. # Check command line options.
  196. # If any of them is invalid, exit the process.
  197. #
  198. def check_command_line
  199. if (ARGV.length != 2)
  200. usage
  201. exit 2
  202. end
  203. if $options["daemon"]
  204. $options["daemon"] = File.expand_path($options["daemon"], File.dirname(__FILE__))
  205. unless is_writable_dir? $options["daemon"]
  206. usage
  207. $stderr.puts "Can not create a file in the daemon directory: %s" % [$options["daemon"]]
  208. exit 5
  209. end
  210. end
  211. $topdir = $options["daemon"] || File.expand_path(File.dirname(__FILE__))
  212. if $options["player-log-dir"]
  213. $options["player-log-dir"] = File.expand_path($options["player-log-dir"], $topdir)
  214. unless is_writable_dir?($options["player-log-dir"])
  215. usage
  216. $stderr.puts "Can not write a file in the player log dir: %s" % [$options["player-log-dir"]]
  217. exit 3
  218. end
  219. end
  220. if $options["pid-file"]
  221. $options["pid-file"] = File.expand_path($options["pid-file"], $topdir)
  222. unless ShogiServer::is_writable_file? $options["pid-file"]
  223. usage
  224. $stderr.puts "Can not create the pid file: %s" % [$options["pid-file"]]
  225. exit 4
  226. end
  227. end
  228. if $options["floodgate-games"]
  229. names = $options["floodgate-games"].split(",")
  230. new_names =
  231. names.select do |name|
  232. ShogiServer::League::Floodgate::game_name?(name)
  233. end
  234. if names.size != new_names.size
  235. $stderr.puts "Found a wrong Floodgate game: %s" % [names.join(",")]
  236. exit 6
  237. end
  238. $options["floodgate-games"] = new_names
  239. end
  240. if $options["floodgate-history"]
  241. $stderr.puts "WARNING: --floodgate-history has been deprecated."
  242. $options["floodgate-history"] = nil
  243. end
  244. $options["max-moves"] ||= ShogiServer::Default_Max_Moves
  245. $options["max-moves"] = $options["max-moves"].to_i
  246. $options["max-identifier"] ||= ShogiServer::Default_Max_Identifier_Length
  247. $options["max-identifier"] = $options["max-identifier"].to_i
  248. $options["least-time-per-move"] ||= ShogiServer::Default_Least_Time_Per_Move
  249. $options["least-time-per-move"] = $options["least-time-per-move"].to_i
  250. end
  251. # See if a file can be created in the directory.
  252. # Return true if a file is writable in the directory, otherwise false.
  253. #
  254. def is_writable_dir?(dir)
  255. unless File.directory? dir
  256. return false
  257. end
  258. result = true
  259. begin
  260. temp_file = Tempfile.new("dummy-shogi-server", dir)
  261. temp_file.close true
  262. rescue
  263. result = false
  264. end
  265. return result
  266. end
  267. def write_pid_file(file)
  268. open(file, "w") do |fh|
  269. fh.puts "#{$$}"
  270. end
  271. end
  272. def mutex_watchdog(mutex, sec)
  273. sec = 1 if sec < 1
  274. queue = []
  275. while true
  276. if mutex.try_lock
  277. queue.clear
  278. mutex.unlock
  279. else
  280. queue.push(Object.new)
  281. if queue.size > sec
  282. # timeout
  283. log_error("mutex watchdog timeout: %d sec" % [sec])
  284. queue.clear
  285. end
  286. end
  287. sleep(1)
  288. end
  289. end
  290. def login_loop(client)
  291. player = login = nil
  292. while r = select([client], nil, nil, ShogiServer::Login_Time) do
  293. str = nil
  294. begin
  295. break unless str = r[0].first.gets
  296. rescue Exception => ex
  297. # It is posssible that the socket causes an error (ex. Errno::ECONNRESET)
  298. log_error("login_loop: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
  299. break
  300. end
  301. $mutex.lock # guards $league
  302. begin
  303. str =~ /([\r\n]*)$/
  304. eol = $1
  305. if (ShogiServer::Login::good_login?(str))
  306. player = ShogiServer::Player::new(str, client, eol)
  307. login = ShogiServer::Login::factory(str, player)
  308. if (current_player = $league.find(player.name))
  309. # Even if a player is in the 'game' state, when the status of the
  310. # player has not been updated for more than a day, it is very
  311. # likely that the player is stalling. In such a case, a new player
  312. # can override the current player.
  313. if (current_player.password == player.password &&
  314. (current_player.status != "game" ||
  315. Time.now - current_player.last_command_at > ONE_DAY))
  316. log_message("player %s login forcibly, nudging the former player" % [player.name])
  317. log_message(" the former player was in %s and received the last command at %s" % [current_player.status, current_player.last_command_at])
  318. current_player.kill
  319. else
  320. login.incorrect_duplicated_player(str)
  321. player = nil
  322. break
  323. end
  324. end
  325. $league.add(player)
  326. break
  327. else
  328. client.write("LOGIN:incorrect" + eol)
  329. client.write("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
  330. end
  331. ensure
  332. $mutex.unlock
  333. end
  334. end # login loop
  335. return [player, login]
  336. end
  337. def setup_logger(log_file)
  338. logger = ShogiServer::Logger.new(log_file, 'daily')
  339. logger.formatter = ShogiServer::Formatter.new
  340. logger.level = $DEBUG ? Logger::DEBUG : Logger::INFO
  341. logger.datetime_format = "%Y-%m-%d %H:%M:%S"
  342. return logger
  343. end
  344. def setup_watchdog_for_giant_lock
  345. $mutex = Mutex::new
  346. Thread::start do
  347. Thread.pass
  348. mutex_watchdog($mutex, 10)
  349. end
  350. end
  351. def main
  352. $options = parse_command_line
  353. check_command_line
  354. $config = ShogiServer::Config.new $options
  355. $league = ShogiServer::League.new($topdir)
  356. $league.event = ARGV.shift
  357. port = ARGV.shift
  358. log_file = $options["daemon"] ? File.join($options["daemon"], "shogi-server.log") : STDOUT
  359. $logger = setup_logger(log_file)
  360. $league.dir = $topdir
  361. config = {}
  362. config[:BindAddress] = "0.0.0.0"
  363. config[:Port] = port
  364. config[:ServerType] = WEBrick::Daemon if $options["daemon"]
  365. config[:Logger] = $logger
  366. setup_floodgate = nil
  367. config[:StartCallback] = Proc.new do
  368. srand
  369. if $options["pid-file"]
  370. write_pid_file($options["pid-file"])
  371. end
  372. setup_watchdog_for_giant_lock
  373. $league.setup_players_database
  374. setup_floodgate = ShogiServer::SetupFloodgate.new($options["floodgate-games"])
  375. setup_floodgate.start
  376. end
  377. config[:StopCallback] = Proc.new do
  378. if $options["pid-file"]
  379. FileUtils.rm($options["pid-file"], :force => true)
  380. end
  381. end
  382. srand
  383. server = WEBrick::GenericServer.new(config)
  384. ["INT", "TERM"].each do |signal|
  385. trap(signal) do
  386. server.shutdown
  387. setup_floodgate.kill
  388. end
  389. end
  390. unless (RUBY_PLATFORM.downcase =~ /mswin|mingw|cygwin|bccwin/)
  391. trap("HUP") do
  392. Dependencies.clear
  393. end
  394. end
  395. $stderr.puts("server started as a deamon [Revision: #{ShogiServer::Revision}]") if $options["daemon"]
  396. log_message("server started [Revision: #{ShogiServer::Revision}]")
  397. server.start do |client|
  398. begin
  399. # client.sync = true # this is already set in WEBrick
  400. client.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
  401. # Keepalive time can be set by /proc/sys/net/ipv4/tcp_keepalive_time
  402. player, login = login_loop(client) # loop
  403. unless player
  404. log_error("Detected a timed out login attempt")
  405. next
  406. end
  407. log_message(sprintf("user %s login", player.name))
  408. login.process
  409. player.setup_logger($options["player-log-dir"]) if $options["player-log-dir"]
  410. player.run(login.csa_1st_str) # loop
  411. $mutex.lock
  412. begin
  413. if (player.game)
  414. player.game.kill(player)
  415. end
  416. player.finish
  417. $league.delete(player)
  418. log_message(sprintf("user %s logout", player.name))
  419. ensure
  420. $mutex.unlock
  421. end
  422. player.wait_write_thread_finish(1000) # milliseconds
  423. rescue Exception => ex
  424. log_error("server.start: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
  425. end
  426. end
  427. end
  428. if ($0 == __FILE__)
  429. STDOUT.sync = true
  430. STDERR.sync = true
  431. TCPSocket.do_not_reverse_lookup = true
  432. Thread.abort_on_exception = $DEBUG ? true : false
  433. begin
  434. main
  435. rescue Exception => ex
  436. if $logger
  437. log_error("main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
  438. else
  439. $stderr.puts "main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"
  440. end
  441. end
  442. end
旧リポジトリブラウザで表示