# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-listen_port = 8333
+listen_port = process.env.PORT ? process.env.app_port ? 9988
sys = require 'sys'
fs = require 'fs'
http = require 'http'
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
+interval = (ms, func) -> setInterval 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) ->
html_handler = (args, out, request, url_parts) ->
fs.readFile 'index.html', 'utf8', (err, data) ->
if err
- return out.end 'Server failed to read index.html'
+ return out.end "Server failed to read index.html: #{err}"
out.end data
clean_pathname_regex = new RegExp('[^a-zA-Z/_.-]')
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}"
+# serve javascript files from within external/
+external_javascript_handler = (args, out, request, url_parts) ->
+ filename = clean_pathname "external/#{url_parts.pathname.substr 10}"
fs.readFile filename, 'utf8', (err, data) ->
if err
out.writeHead 404
return
game = games[args.game]
+ out.writeHead 200, 'Content-Type': 'text/javascript'
waiter = games["#{args.agent}_waiter"]
if waiter?
- waiter.writeHead 200, 'Content-Type': 'text/javascript'
waiter.end '[]'
game["#{args.agent}_waiter"] = out
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'
game = games[args.game]
+ game.last_seen = now_s()
+
game.process_messages messages
out.writeHead 200, "Content-Type": 'text/plain'
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
return game
+long_poll_keepalive = ->
+ for slug, g of games
+ if g.p1_waiter? and g.p1_waiter isnt false
+ g.p1_waiter.write '\n'
+ if g.p2_waiter? and g.p2_waiter isnt false
+ g.p2_waiter.write '\n'
+
+interval 12000, long_poll_keepalive
+
http_server = http.createServer (req, res) ->
url_parts = url.parse req.url, true
if url_parts.query is undefined
rel_path = url_parts.pathname.substr 1
- if rel_path.substr(0, 11) is 'javascript/'
- return javascript_handler url_parts.query, res, req, url_parts
+ if rel_path.substr(0, 9) is 'external/'
+ return external_javascript_handler url_parts.query, res, req, url_parts
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'
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'
+ else if rel_path is 'set'
data = ''
req.on 'data', (chunk) ->
data += chunk
query[key] = parg
return set_handler query, res, req, url_parts
return
- else if rel_path.substr(rel_path.length - 4) is '/get'
+ else if rel_path 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 html_handler url_parts.query, res, req, url_parts
-http_server.listen listen_port, "127.0.0.1"
+################## INIT ####################
+# make sure the current working directory is correct
+process.chdir __dirname
+
+setInterval expire_old_games, 2 * 60 * 1000 # check every 2 minutes for expired games
+
+http_server.listen listen_port
console.log "Server running at http://127.0.0.1:#{listen_port}/"