X-Git-Url: https://jasonwoof.com/gitweb/?p=peach-cgt.git;a=blobdiff_plain;f=server.coffee;h=1ce56f235f68ed6455029ad638b1b6835913c7fa;hp=48e28570437e0e2e90cf1ae85488a014cc10337f;hb=387a29e812d34804ee5f424d00d1b9f68eb2ea91;hpb=4197e7b5a4746c9d04a96190707d7021e443aa42 diff --git a/server.coffee b/server.coffee index 48e2857..1ce56f2 100644 --- a/server.coffee +++ b/server.coffee @@ -1,10 +1,66 @@ -listen_port = 8333 +# Peach CGT -- Card Game Table simulator +# Copyright (C) 2011 Jason Woofenden +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +listen_port = process.env.app_port ? 9988 sys = require 'sys' fs = require 'fs' http = require 'http' +querystring = require 'querystring' url = require 'url' less = require 'less' coffee = require 'coffee-script' +model = require './common.coffee' + +games = {} +max_concurrent_games = 50 +max_game_idle = 3 * 60 * 60 * 1000 # three hours (in miliseconds) + +# timeout function with args in convenient order +timeout = (ms, func) -> setTimeout func, ms + +now_s = -> + d = new Date() + return d.getTime() + +expire_old_games = -> + count = 0 + for slug, g of games + count += 1 + oldest_slug = slug + oldest_seen = g.last_seen + + return unless count > 0 + + # check all the games + # track oldest + # delete old ones + too_old = now_s() - max_game_idle + kills = [] + for slug, g of games + if g.last_seen < oldest_seen + oldest_seen = g.last_seen + oldest_slug = slug + if g.last_seen < too_old + kills.push slug + if count > max_concurrent_games and kills.length is 0 + console.log "hit max_concurrent_games, destroying oldest" + kills.push oldest_slug + for slug in kills + console.log "killing game #{slug}" + delete games[slug] css_handler = (args, out, request, url_parts) -> fs.readFile 'style.less', 'utf8', (err, data) -> @@ -16,10 +72,34 @@ css_handler = (args, out, request, url_parts) -> out.end css js_handler = (args, out, request, url_parts) -> - fs.readFile 'client.coffee', 'utf8', (err, data) -> + convert = false + basename = url_parts.pathname.substr 1, (url_parts.pathname.length - 4) + if basename is 'client' + filename = 'client.coffee' + convert = true + else if basename is 'common' + filename = 'common.coffee' + convert = true + else if basename is 'cs_cards' + filename = 'cs_cards.js' + convert = false + else + error = "Unknown js basename: #{basename}" + console.log error + out.end(error) + return + + fs.readFile filename, 'utf8', (err, data) -> if err - return out.end 'Server failed to read client.coffee' - out.end coffee.compile data + return out.end "Server failed to read #{filename}" + if convert + try + converted = coffee.compile data + catch e + out.end "alert(\"server faild to compile #{filename}\");" + out.end converted + else + out.end data html_handler = (args, out, request, url_parts) -> fs.readFile 'index.html', 'utf8', (err, data) -> @@ -34,7 +114,7 @@ clean_pathname = (str) -> str = str.replace clean_pathname_regex, '_' str = str.replace clean_pathname_regex2, '/_' return str.replace clean_pathname_regex3, '_' - + # serve javascript files from within /usr/share/javascript javascript_handler = (args, out, request, url_parts) -> filename = clean_pathname "/usr/share/#{url_parts.pathname}" @@ -44,7 +124,148 @@ javascript_handler = (args, out, request, url_parts) -> return out.end "Server failed to read #{filename}" out.writeHead 200, 'Content-Type': 'text/javascript' out.end data - + + +get_handler = (args, out, request, url_parts) -> + unless args.game?.length + out.writeHead 404, "Content-Type": 'text/plain' + out.end 'Missing (or empty) "game" argument' + return + + unless args.agent is 'p1' or args.agent is 'p2' + out.writeHead 404, "Content-Type": 'text/plain' + out.end '"agent" argument must be set to p1 or p2' + return + + unless games[args.game]? + out.writeHead 404, "Content-Type": 'text/plain' + out.end 'Game not found' + return + + game = games[args.game] + + waiter = games["#{args.agent}_waiter"] + if waiter? + waiter.writeHead 200, 'Content-Type': 'text/javascript' + waiter.end '[]' + + game["#{args.agent}_waiter"] = out + + answer_soon game # in case there's something queued already + +set_handler = (args, out, request, url_parts) -> + unless args.game?.length + out.writeHead 404, "Content-Type": 'text/plain' + out.end '{"status":1,"text_status":"Missing (or empty) game argument"}' + return + + unless args.agent is 'p1' or args.agent is 'p2' + out.writeHead 404, "Content-Type": 'text/plain' + out.end '{"status":2,"text_status":"agent argument must be set to p1 or p2"}' + return + + unless args.messages? + out.writeHead 404, "Content-Type": 'text/plain' + out.end '{"status":3,"text_status":"messages argument must be set"}' + return + + try + messages = JSON.parse args.messages + catch e + out.writeHead 400, "Content-Type": 'text/plain' + out.end '{"status":4,"text_status":"Invalid JSON"}' + return + + # special handling of 'new_game' api, because for this one we don't have a + # game object to pass the message to + if messages?[0]?[0] is 'new_game' + message = messages.shift() + slug = message[1] + if games[slug]? + out.writeHead 403, "Content-Type": 'text/plain' + out.end '{"status":6,"text_status":"Game already exists"}' + return + game = games[slug] = new_game slug, 'server' + game.last_seen = now_s() + console.log "new game: #{slug}" + expire_old_games() + + unless games[args.game]? + out.writeHead 404, "Content-Type": 'text/plain' + out.end '{"status":5,"text_status":"Game not found"}' + return + + game = games[args.game] + + game.last_seen = now_s() + + game.process_messages messages + + out.writeHead 200, "Content-Type": 'text/plain' + out.end '{"status":0,"text_status":"Success"}' + +# don't call this directly, call answer_soon instead +answer_now = (game) -> + if game.p1_waiter and game.p1_queue.length + waiter = game.p1_waiter + queue = game.p1_queue + game.p1_waiter = false + game.p1_queue = [] + waiter.writeHead 200, 'Content-Type': 'text/javascript' + waiter.end JSON.stringify queue + if game.p2_waiter and game.p2_queue.length + waiter = game.p2_waiter + queue = game.p2_queue + game.p2_waiter = false + game.p2_queue = [] + waiter.writeHead 200, 'Content-Type': 'text/javascript' + waiter.end JSON.stringify queue + +# this marks a game as "dirty" and makes sure there's exactly one timeout +# that'll respond to any clients that are waiting, and now have messages. +answer_soon = (game) -> + unless game.replier_id + game.replier_id = timeout 1, -> + delete game.replier_id + answer_now game + +forward_events = (message...) -> + unless message[1] is 'p1' + @p1_queue.push message + answer_soon this + unless message[1] is 'p2' + @p2_queue.push message + answer_soon this + +new_game = (slug) -> + game = games[slug] = model.new slug, 'server' + game.p1_waiter = false + game.p2_waiter = false + game.p1_queue = [] + game.p2_queue = [] + + game.on 'move', (agent, card, x, y, z, pile) -> + forward_events.call this, 'move', agent, card, x, y, z, pile + game.on 'mark', (agent, card, state) -> + forward_events.call this, 'mark', agent, card, state + game.on 'flip', (agent, card, state) -> + forward_events.call this, 'flip', agent, card, state + game.on 'new_cards', (agent, cards) -> + # server assigns card numbers, and tells both clients + # (unlike all other api calls, sending agent expects to get this one back) + forward_events.call this, 'new_cards', 'server', cards + game.on 'set_cards', (agent, cards) -> + forward_events.call this, 'set_cards', agent, cards + game.on 'send_state', (agent) -> + timeout 10, => + if agent is 'p1' + @p1_queue.push ['set_cards', 'server', @cards] + answer_soon this + if agent is 'p2' + @p2_queue.push ['set_cards', 'server', @cards] + answer_soon this + + return game http_server = http.createServer (req, res) -> url_parts = url.parse req.url, true @@ -58,11 +279,29 @@ http_server = http.createServer (req, res) -> else if rel_path.substr(rel_path.length - 4) is '.css' res.writeHead 200, 'Content-Type': 'text/css' return css_handler url_parts.query, res, req, url_parts - else if rel_path.substr rel_path.length - 3 is '.js' + else if rel_path.substr(rel_path.length - 3) is '.js' res.writeHead 200, 'Content-Type': 'text/javascript' return js_handler url_parts.query, res, req, url_parts - + else if rel_path.substr(rel_path.length - 4) is '/set' + data = '' + req.on 'data', (chunk) -> + data += chunk + req.on 'end', -> + query = url_parts.query + post_args = querystring.parse data + for key, parg of post_args + query[key] = parg + return set_handler query, res, req, url_parts + return + else if rel_path.substr(rel_path.length - 4) is '/get' + return get_handler url_parts.query, res, req, url_parts + else if rel_path.substr(rel_path.length - 4) is '.ico' + res.writeHead 404 + return res.end() + return html_handler url_parts.query, res, req, url_parts +setInterval expire_old_games, 2 * 60 * 1000 # check every 2 minutes for expired games + http_server.listen listen_port, "127.0.0.1" console.log "Server running at http://127.0.0.1:#{listen_port}/"