Permalink
Browse files

Merge branch 'scheduled_task'

  • Loading branch information...
Raekye committed Nov 26, 2015
2 parents 39a6319 + d5b82d6 commit 365142f74b77d08fc732e43eb0bad214226ba804
@@ -20,12 +20,19 @@ def demo
@server.remote_size_slug = '1gb'
@server.setup_stage = 5
@server.remote_id = 1
@server.timezone_delta = 0
@server.scheduled_tasks = ScheduledTask.parse([
'Wednesday 8:00 pm start',
'Friday 3:30 pm start',
'Sunday 11:00 am start',
].join("\n"), @server)

minecraft = Mock::Minecraft.new
minecraft.server = @server
minecraft.autoshutdown_enabled = true
minecraft.autoshutdown_last_check = Time.now - 32.seconds
minecraft.autoshutdown_last_successful = Time.now - 32.seconds
minecraft.autoshutdown_minutes = 8
@server.minecraft = minecraft

user = User.new
@@ -43,12 +43,26 @@ def create

def update
@server = find_server_only_owner(params[:id])
# can be ftp ssh tab, schedule tab, or advanced tab
ssh_keys = server_ssh_keys_params[:ssh_keys]
if ssh_keys.nil?
if @server.update_attributes(server_advanced_params)
return redirect_to server_path(@server), flash: { success: 'Server advanced configuration updated' }
p = server_schedule_params
@server_tab = :schedule
schedule_text = nil
if p[:timezone_delta].nil?
p = server_advanced_params
@server_tab = :advanced
else
if !p[:minecraft_attributes].nil?
p[:minecraft_attributes][:id] = @server.minecraft.id
end
schedule_text = p.delete(:schedule_text)
end
if @server.update_attributes(p)
if @server_tab != :schedule || @server.parse_and_save_schedule(schedule_text)
return redirect_to server_path(@server), flash: { success: (@server_tab == :schedule ? 'Server schedule updated' : 'Server advanced configuration updated') }
end
end
@server_advanced_tab = true
else
@server.ssh_keys = ssh_keys
if @server.save
@@ -58,7 +72,7 @@ def update
end
return redirect_to server_path(@server), flash: { success: message }
end
@server_ftp_ssh_tab = true
@server_tab = :ftp_ssh
end
flash[:error] = 'Something went wrong. Please try again'
return render :show
@@ -369,6 +383,14 @@ def server_ssh_keys_params
return params.require(:server).permit(:ssh_keys)
end

def server_schedule_params
return params.require(:server).permit(
:timezone_delta,
:schedule_text,
minecraft_attributes: [:autoshutdown_minutes],
)
end

def minecraft_command_params
return params.require(:command).permit(:data)
end
@@ -8,14 +8,17 @@
# server_id :uuid not null
# flavour :string not null
# mcsw_password :string not null
# autoshutdown_enabled :boolean default("false"), not null
# autoshutdown_enabled :boolean default(FALSE), not null
# autoshutdown_last_check :datetime not null
# autoshutdown_last_successful :datetime not null
# autoshutdown_minutes :integer default(8), not null
#

class Minecraft < ActiveRecord::Base
belongs_to :server

validates :autoshutdown_minutes, numericality: { only_integer: true, greater_than_or_equal_to: 5, less_than: 24 * 60 }

after_initialize :after_initialize_callback
before_validation :before_validate_callback

@@ -0,0 +1,141 @@
# == Schema Information
#
# Table name: scheduled_tasks
#
# id :integer not null, primary key
# server_id :uuid not null
# partition :integer not null
# action :string not null
#

class ScheduledTask < ActiveRecord::Base
belongs_to :server

# in minutes
PARTITION_SIZE = 30
# in minutes, the margin of error
PARTITION_DELTA = 5

DAYS_OF_WEEK = {
'mon' => 0,
'monday' => 0,
'tue' => 1,
'tuesday' => 1,
'wed' => 2,
'wednesday' => 2,
'thu' => 3,
'thursday' => 3,
'fri' => 4,
'friday' => 4,
'sat' => 5,
'saturday' => 5,
'sun' => 6,
'sunday' => 6,
}
DAYS_OF_WEEK_INVERSE = Hash[DAYS_OF_WEEK.map { |k, v| [v, k.capitalize] }]

AMPM = {
'am' => 0,
'pm' => 1,
}

ACTIONS = [
'start',
'stop',
]

def to_user_string
m = self.partition % 100
hours = (self.partition / 100) + self.server.timezone_delta
h_24 = hours % 24
h_12 = h_24 % 12
ampm = h_24 < 12 ? 'am' : 'pm'
d = hours / 24 % 7
return "#{DAYS_OF_WEEK_INVERSE[d]} #{h_12}:#{m.to_s.rjust(2, '0')} #{ampm} #{action}"
end

def self.parse(str, server)
xs = []
str.each_line do |l|
x = self.parse_line(l.clean, server)
if x.error?
return x
end
if !x.nil?
xs.push(x)
end
end
return xs
end

def self.parse_line(line, server)
if line.nil?
return nil
end
if line =~ /([a-z]+)\s+(\d+):(\d\d)\s*([a-z]+)\s+([a-z]+)/
day = DAYS_OF_WEEK[$1]
hour = $2.to_i
minute = $3.to_i
ampm = AMPM[$4]
action = $5
if day.nil?
return "Bad day of week \"#{$1}\"".error!(nil)
end
if hour < 0 || hour >= 12
return "Bad hour \"#{$2}\"".error!(nil)
end
if minute < 0 || minute >= 60 || minute % PARTITION_SIZE != 0
return "Bad minute \"#{$3}\"".error!(nil)
end
if ampm.nil?
return "Bad am/pm \"#{$4}\"".error!(nil)
end
if !ACTIONS.include?(action)
return "Bad action \"#{$5}\"".error!(nil)
end
return ScheduledTask.new({
server: server,
partition: Partition.calculate(day, hour, minute, ampm, server.timezone_delta),
action: action,
})
end
return "Bad schedule item format \"#{line}\"".error!(nil)
end

def self.server_time_string
return DateTime.now.in_time_zone(Gamocosm::TIMEZONE).strftime('%-I:%M %P (%H:%M) %Z')
end

class Partition
attr_reader :value
attr_reader :snap

def initialize(value)
@value = value
x = @value % 100 % PARTITION_SIZE
y = @value - x
@snap = x * 2 < PARTITION_SIZE ? y : y + PARTITION_SIZE
end

def self.calculate(day, hour, minute, ampm, delta)
x = ((day * 24) + (hour) + (ampm * 12) - (delta)) % (7 * 24)
return x * 100 + minute
end

def valid?
return (@value - @snap).abs <= PARTITION_DELTA
end

def next
if self.valid? || @snap < @value
return @value + PARTITION_SIZE
end
return @snap
end

def self.server_current
x = DateTime.current
return Partition.new(Partition.calculate((x.wday - 1) % 7, x.hour, x.minute, 0, 0))
end
end
end
@@ -9,20 +9,22 @@
# updated_at :datetime
# domain :string not null
# pending_operation :string
# ssh_port :integer default("4022"), not null
# ssh_port :integer default(4022), not null
# ssh_keys :string
# setup_stage :integer default("0"), not null
# setup_stage :integer default(0), not null
# remote_id :integer
# remote_region_slug :string not null
# remote_size_slug :string not null
# remote_snapshot_id :integer
# timezone_delta :integer default(0), not null
#

class Server < ActiveRecord::Base
belongs_to :user
has_one :minecraft, dependent: :destroy
has_and_belongs_to_many :friends, foreign_key: 'server_id', class_name: 'User', dependent: :destroy
has_many :logs, foreign_key: 'server_id', class_name: 'ServerLog', dependent: :destroy
has_many :scheduled_tasks, dependent: :destroy

validates :name, length: { in: 3...64 }
validates :name, format: { with: /\A[a-z][a-z0-9-]*[a-z0-9]\z/, message: 'Name must start with a letter, and end with a letter or number. May include letters, numbers, and dashes in between' }
@@ -32,7 +34,8 @@ class Server < ActiveRecord::Base
validates :remote_snapshot_id, numericality: { only_integer: true }, allow_nil: true
validates :remote_region_slug, presence: true
validates :remote_size_slug, presence: true

validates :ssh_port, numericality: { only_integer: true }
validates :timezone_delta, numericality: { only_integer: true, greater_than: -24, less_than: 24 }

after_initialize :after_initialize_callback
before_validation :before_validate_callback
@@ -54,6 +57,26 @@ def before_validate_callback
self.ssh_keys = self.ssh_keys.try(:gsub, /\s/, '').clean
end

def schedule_text
return self.scheduled_tasks.map { |x| x.to_user_string }.join("\n")
end

def parse_and_save_schedule(text)
tasks = ScheduledTask.parse(text, self)
if tasks.error?
self.errors.add(:schedule_text, tasks.msg)
return false
end
self.scheduled_tasks.delete_all
tasks.each do |t|
if !t.save
self.errors.add(:schedule_text, 'Unknown error saving schedule item')
return false
end
end
return true
end

def host_name
return "#{name}.minecraft.gamocosm"
end
@@ -14,6 +14,6 @@ class ServerLog < ActiveRecord::Base
belongs_to :server

def when
return created_at.in_time_zone(ActiveSupport::TimeZone[-8]).strftime('%Y %b %e (%H:%M:%S %Z)')
return created_at.in_time_zone(Gamocosm::TIMEZONE).strftime('%Y %b %-d (%H:%M:%S %Z)')
end
end
@@ -8,7 +8,7 @@
# reset_password_token :string(255)
# reset_password_sent_at :datetime
# remember_created_at :datetime
# sign_in_count :integer default("0"), not null
# sign_in_count :integer default(0), not null
# current_sign_in_at :datetime
# last_sign_in_at :datetime
# current_sign_in_ip :string(255)
@@ -62,6 +62,11 @@
<% end %>
<br />
<br />
<p>
Autoshutdown after <b><%= @server.minecraft.autoshutdown_minutes %> minutes</b>.
<br />
Change this under the "Schedule" tab.
</p>
<i>
Gamocosm is not responsible if something goes wrong, but it will try to notify you via email.
It is your responsibility to periodically check on your servers.
@@ -0,0 +1,40 @@
<br />
<%= panel_with_heading 'Schedule' do %>
<%= simple_form_for @server, (@demo.nil? ? { url: server_path(@server), method: :put } : { html: { onsubmit: 'return false;' } }) do |f| %>
<p>
You can setup a schedule to start/stop a server at different days of the week.
Autoshutdown also works with the schedule.
This feature is in beta!
Things may break, and if they do it'd be greatly appreciated if you <%= link_to 'submitted an issue', issues_path %> with as many details as possible.
Remember to backup your server locally!
</p>
<br />
<%= f.simple_fields_for :minecraft do |f_m| %>
<%= f_m.input :autoshutdown_minutes %>
<p>
Time of inactivity before shutting off your server (autoshutdown is <b><%= @server.minecraft.autoshutdown_enabled ? 'enabled' : 'disabled' %></b>, change this under the "Profile" tab).
</p>
<% end %>
<br />
<%= f.input :timezone_delta %>
<p>
The server's schedule is based on Pacific Time (currently <b><%= ScheduledTask.server_time_string %></b>).
By setting the timezone delta, you can enter times below based on your own clock.
Suppose you are 3 hours ahead (Eastern Time); your "timezone delta" is 3.
If you are behind, you can use negative numbers.
</p>
<br />
<%= f.input :schedule_text, as: :text, label: 'Schedule', input_html: { rows: 4 } %>
<p>
Enter a rule on each line, in the format <code>[day of week] [hour]:[minute] [am or pm] [start or stop]</code>:
<ul>
<li>A day of the week is [<%= ScheduledTask::DAYS_OF_WEEK_INVERSE.map { |k, v| v }.join(', ') %>], or just the first 3 letters</li>
<li>The hour is in 12-hour format (1-12)</li>
<li>The minute is either "00" or "30" (only half-hour intervals supported)</li>
<li>Everything is case-insensitive ("am" is the same as "AM")</li>
</ul>
</p>
<br />
<%= f.button :submit, 'Save', class: 'btn btn-success' %>
<%end %>
<% end %>
Oops, something went wrong.

0 comments on commit 365142f

Please sign in to comment.