# == Schema Information # # Table name: servers # # id :integer not null, primary key # name :string(255) # description :string(255) # dns :string(255) # ip :string(255) # port :string(255) # rcon :string(255) # password :string(255) # irc :string(255) # user_id :integer # official :boolean # created_at :datetime # updated_at :datetime # map :string(255) # players :integer # max_players :integer # ping :string(255) # version :integer # domain :integer default(0), not null # reservation :string(255) # recording :string(255) # idle :datetime # default_id :integer # active :boolean default(TRUE), not null # recordable_type :string(255) # recordable_id :integer # category_id :integer # require "rcon" require "yaml" class Server < ActiveRecord::Base include Extra DOMAIN_HLDS = 0 DOMAIN_HLTV = 1 DOMAIN_NS2 = 2 HLTV_IDLE = 1200 DEMOS = "/var/www/virtual/ensl.org/hlds_l/ns/demos" QSTAT = "/usr/bin/quakestat" TMPFILE = "tmp/server.txt" attr_accessor :rcon_handle, :pwd attr_protected :id, :user_id, :updated_at, :created_at, :map, :players, :maxplayers, :ping, :version validates_length_of [:name, :dns,], :in => 1..30 validates_length_of [:rcon, :password, :irc], :maximum => 30, :allow_blank => true validates_length_of :description, :maximum => 255, :allow_blank => true validates_format_of :ip, :with => /\A[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\z/ validates_format_of :port, :with => /\A[0-9]{1,5}\z/ validates_format_of :reservation, :with => /\A[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}:[0-9]{1,5}\z/, :allow_nil => true validates_format_of :pwd, :with => /\A[A-Za-z0-9_\-]*\z/, :allow_nil => true scope :ordered, :order => "name" scope :hlds, :conditions => ["domain = ?", DOMAIN_HLDS] scope :ns2, :conditions => ["domain = ?", DOMAIN_NS2] scope :hltvs, :conditions => ["domain = ?", DOMAIN_HLTV] scope :active, :conditions => "active = 1" scope :with_players, :conditions => "players > 0" scope :reserved, :conditions => "reservation IS NOT NULL" scope :unreserved_now, :conditions => "reservation IS NULL" scope :unreserved_hltv_around, lambda { |time| { :select => "servers.*", :joins => "LEFT JOIN matches ON servers.id = matches.hltv_id AND match_time > '#{(time.ago(Match::MATCH_LENGTH).utc).strftime("%Y-%m-%d %H:%M:%S")}' AND match_time < '#{(time.ago(-Match::MATCH_LENGTH).utc).strftime("%Y-%m-%d %H:%M:%S")}'", :conditions => "matches.hltv_id IS NULL"} } scope :of_addr, lambda { |addr| { :conditions => { :ip => addr.match(/([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})/)[0], :port => addr.match(/:([0-9]{1,5})/)[1] } } } scope :of_category, lambda { |category| { :conditions => {:category_id => category.id} }} has_many :logs has_many :matches has_many :challenges belongs_to :user belongs_to :recordable, :polymorphic => true before_create :set_category acts_as_versioned non_versioned_columns << 'name' non_versioned_columns << 'description' non_versioned_columns << 'dns' non_versioned_columns << 'ip' non_versioned_columns << 'port' non_versioned_columns << 'rcon' non_versioned_columns << 'password' non_versioned_columns << 'irc' non_versioned_columns << 'user_id' non_versioned_columns << 'official' non_versioned_columns << 'domain' non_versioned_columns << 'reservation' non_versioned_columns << 'recording' non_versioned_columns << 'idle' non_versioned_columns << 'default_id' non_versioned_columns << 'active' non_versioned_columns << 'recordable_type' non_versioned_columns << 'recordable_id' def domains {DOMAIN_HLTV => "HLTV", DOMAIN_HLDS => "NS Server", DOMAIN_NS2 => "NS2 Server"} end def to_s name end def addr ip + ":" + port.to_s end def players_s if players.nil? or max_players.nil? "N/A" else players.to_s + " / " + max_players.to_s end end def recording_s return nil if self.domain != DOMAIN_HLTV # recording.to_i > 0 ? Match.find(recording).to_s : recording recording end def reservation_s return nil if domain != DOMAIN_HLTV reservation end def graphfile File.join("public", "images", "servers", id.to_s + ".png") end def set_category self.category_id = (domain == DOMAIN_NS2 ? 45 : 44 ) end def after_validation_on_update if reservation_changed? rcon_connect if reservation == nil rcon_exec "stop" self.recording = nil self.recordable = nil self.idle = nil save_demos if self.recording hltv_stop else if changes['reservation'][0].nil? hltv_start rcon_exec "stop" self.recording = recordable.demo_name if recordable and recordable_type == "Match" or recordable_type == "Gather" rcon_exec "record demos/" + self.recording if self.recording end rcon_exec "serverpassword " + pwd rcon_exec "connect " + reservation self.idle = DateTime.now end rcon_disconnect end end def execute command rcon_connect response = rcon_exec command rcon_disconnect response end def rcon_connect self.rcon_handle = RCon::Query::Original.new(ip, port, rcon) end def rcon_exec command response = rcon_handle.command(command) Log.transaction do Log.add(self, Log::DOMAIN_RCON_COMMAND, command) if response.to_s.length > 0 Log.add(self, Log::DOMAIN_RCON_RESPONSE, response) end end response end def rcon_disconnect rcon_handle.disconnect end def hltv_start if nr = hltv_nr `screen -d -m -S "Hltv-#{nr[1]}" -c $HOME/.screenrc-hltv $HOME/hlds_l/hltv -ip 78.46.36.107 -port 28#{nr[1]}00 +exec ns/hltv#{nr[1]}.cfg` sleep 1 end end def hltv_nr self.name.match(/Tv \#([0-9])/) end def hltv_stop if nr = hltv_nr sleep 5 rcon_exec "exit" #`screen -S "Hltv-#{nr[1]}" -X 'quit'` end end def save_demos dir = case recordable_type when "Match" then recordable.contest.demos when "Gather" then Directory.find(Directory::DEMOS_GATHERS) end dir ||= Directory.find(Directory::DEMOS_DEFAULT) zip_path = File.join(dir.path, recording + ".zip") Zip::ZipOutputStream::open(zip_path) do |zos| if recordable_type == "Match" zos.put_next_entry "readme.txt" zos.write "Team1: " + recordable.contester1.to_s + "\r\n" zos.write "Team2: " + recordable.contester2.to_s + "\r\n" zos.write "Date: " + recordable.match_time.to_s + "\r\n" zos.write "Contest: " + recordable.contest.to_s + "\r\n" zos.write "Server: " + recordable.server.addr + "\r\n" if recordable.server zos.write "HLTV: " + addr + "\r\n" zos.write YAML::dump(recordable.attributes).to_s end Dir.glob("#{DEMOS}/*").each do |file| if File.file?(file) and file.match(/#{recording}.*\.dem/) zos.put_next_entry File.basename(file) zos.write(IO.read(file)) end end end DataFile.transaction do unless dbfile = DataFile.find_by_path(zip_path) dbfile = DataFile.new dbfile.path = zip_path dbfile.directory = dir dbfile.save! DataFile.update_all({:name => File.basename(zip_path)}, {:id => dbfile.id}) end if recordable_type == "Match" recordable.demo = dbfile recordable.save end end end def make_stats graph = Gruff::Line.new graph.title = name pings = [] players = [] labels = {} n = 0 for version in versions.all(:order => "updated_at DESC", :limit => 30).reverse pings << version.ping.to_i players << version.players.to_i labels[n] = version.updated_at.strftime("%H:%M") if n % 3 == 0 n = n + 1 end graph.theme_37signals graph.data("Ping", pings, '#057fc0') graph.data("Players", players, '#ff0000') graph.labels = labels graph.write(graphfile) end def default_record # if self.default # rcon_exec "record demos/auto-" + Verification.uncrap(default.name) # rcon_exec "serverpassword " + default.password # rcon_exec "connect " + default.addr # end end def is_free time challenges.around(time).pending.count == 0 and matches.around(time).count == 0 end def can_create? cuser cuser end def can_update? cuser cuser and cuser.admin? or user == cuser end def can_destroy? cuser cuser and cuser.admin? end def self.refresh servers = "" Server.hlds.active.all.each do |server| servers << " -a2s " + server.ip + ":" + server.port.to_s end file = File.join(Rails.root, TMPFILE) system "#{QSTAT} -xml #{servers} | grep -v '' > #{file}" doc = REXML::Document.new(File.new(file).read) doc.elements.each('qstat/server') do |server| hostname = server.elements['hostname'].text.split(':', 2) if s = Server.active.first(:conditions => {:ip => hostname[0], :port => hostname[1]}) if server.elements.include? 'map' s.map = server.elements['map'].text s.players = server.elements['numplayers'].text.to_i s.max_players = server.elements['maxplayers'].text.to_i s.ping = server.elements['ping'].text s.map = server.elements['map'].text s.save s.make_stats end end end servers = "" Server.hltvs.reserved.each do |server| servers << " -a2s #{server.reservation}" end doc = REXML::Document.new(`#{QSTAT} -xml #{servers} | grep -v ''`) doc.elements.each('qstat/server') do |server| hostname = server.elements['hostname'].text.split(':', 2) if s = Server.hltvs.reserved.first(:conditions => {:ip => hostname[0], :port => hostname[1]}) if server.elements['numplayers'].text.to_i > 0 s.update_attribute :idle, DateTime.now elsif (s.idle + HLTV_IDLE).past? s.reservation = nil s.save end end end end def self.move addr, newaddr, newpwd self.hltvs.all(:conditions => {:reservation => addr}).each do |hltv| hltv.reservation = newaddr hltv.pwd = newpwd hltv.save! end end def self.stop addr self.hltvs.all(:conditions => {:reservation => addr}).each do |hltv| hltv.reservation = nil hltv.save! end end end