Skip to content

Instantly share code, notes, and snippets.

@leafstorm
Created June 5, 2011 15:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save leafstorm/1009040 to your computer and use it in GitHub Desktop.
Save leafstorm/1009040 to your computer and use it in GitHub Desktop.
Minecraft server management script
#!/usr/bin/env ruby
# craftr
# This manages a Craftbukkit server. It helps you start it, stop it,
# back up your world, and download specific Craftbukkit builds.
# You must "gem install thor" first.
# Run it in your server directory.
require 'thor'
require 'yaml'
THIS_FILE = File.expand_path(__FILE__)
LAUNCHER_SCRIPT = <<EOS
#!/bin/sh
MEMORY=$1
JAR=$2
STATUSFILE=$3
if test -f $STATUSFILE; then
echo Error: Will not start while status file $3 exists
exit 1
fi
echo running > $STATUSFILE
java -Xms${MEMORY}M -Xmx${MEMORY}M -jar $JAR nogui
rm $STATUSFILE
EOS
CRAFTR_KEYS = %w[jvm-memory server-build status-file console-screen-name
backups-dir runtime-level-dir before-start after-stop
server-type]
DEFAULT_SERVER_CONFIG = {
"jvm-memory" => 1024,
"server-build" => "recommended",
"status-file" => ".server.status",
"console-screen-name" => "minecraft",
"server-ip" => "",
"server-port" => 25565,
"level-name" => "world",
"online-mode" => true,
"max-players" => 20,
"spawn-monsters" => true,
"pvp" => true,
"hellworld" => false,
"backups-dir" => "world-backups",
"runtime-level-dir" => nil,
"before-start" => nil,
"after-stop" => nil
}
CONFIGURATION_HELP = <<EOH
All keys not described here are copied to the server.properties file. That
way, Craftr has access to the keys it needs, and you only have to maintain
one file. (The server.properties file is regenerated whenever you start the
server.)
jvm-memory: The amount of memory to allocate for the server's heap. 512 is
good for smaller machines, 1024 for ones with more memory to spare. Anything
less than 512 is not recommended.
server-build: The build of Craftbukkit to run the server with. Use an
integer to use that numbered build, "latest" to just use the latest
snapshot, or "recommended" to use the latest recommended build (redownload it
with "craftr update"). If you don't want to use Craftbukkit, but rather
Notch's Minecraft server, set this to "vanilla".
status-file: The name of the file to store the server's current state in.
".server.status" should be fine, but if you don't want it hidden you could
change it to "server.status".
console-screen-name: The name of the screen session to start the server in.
You should only need to change this if you run multiple servers. Each server
running on the same machine *must* have the same name.
backups-dir: The directory to make world backups into. All backups are .tar.gz
files stamped with the current date and time, and they contain the complete
level. You can restore them with "craftr restore". (However, you must take
care of deleting old backups yourself.)
runtime-level-dir: If you set this, then the world will be copied into a
subdirectory of this directory whenever the server starts, then copied back
after it stops. You can use this with a RAM disk or something. (Note that this
messes with level-name in the server.properties file to do its job.) (Also
note that this is not actually implemented.)
before-start: A command that should be run whenever the server starts.
after-stop: A command that should be run after the server stops.
EOH
LATEST_URL = "http://ci.bukkit.org/job/dev-CraftBukkit/lastSuccessfulBuild/artifact/target/craftbukkit-0.0.1-SNAPSHOT.jar"
RECOMMENDED_URL = "http://ci.bukkit.org/job/dev-CraftBukkit/promotion/latest/Recommended/artifact/target/craftbukkit-0.0.1-SNAPSHOT.jar"
VANILLA_URL = "https://s3.amazonaws.com/MinecraftDownload/launcher/minecraft_server.jar"
BUILD_TEMPLATE = "http://ci.bukkit.org/job/dev-CraftBukkit/%d/artifact/target/craftbukkit-0.0.1-SNAPSHOT.jar"
class Craftr < Thor
include Thor::Actions
desc "makeconfig", "generates a default config file"
def makeconfig
if file_exists(server_config_path)
say "Sorry, but your server configuration file already exists."
say "If you need it regenerated from default, delete or move it."
else
File.open(server_config_path, 'w') do |io|
io.write "# This is the craftr server configuration file.\n"
io.write "# In addition to configuring craftr's settings,\n"
io.write "# it is also used to autogenerate your server.properties.\n"
io.write "# Any keys not used by craftr will be automatically copied\n"
io.write "# to the server.properties file before the server is started.\n"
io.write "# Run 'craftr confighelp' for more information\n"
YAML::dump(DEFAULT_SERVER_CONFIG, io)
end
end
end
desc "confighelp", "prints help for the server-config.yml file"
def confighelp
puts CONFIGURATION_HELP
end
desc "update", "downloads the Craftbukkit JAR"
def update
build = server_config["server-build"]
if build == "vanilla"
say "Downloading Notch's Minecraft server..."
get VANILLA_URL, jar_filename
elsif build == "latest"
say "Downloading latest Craftbukkit build..."
get LATEST_URL, jar_filename
elsif build == "recommended"
say "Downloading latest Craftbukkit recommended build..."
get RECOMMENDED_URL, jar_filename
elsif build.is_a?(Integer)
target = jar_filename
if file_exists(target)
say "Build #{build} is already downloaded."
say "Delete the #{target} file if you need it redownloaded."
else
say "Downloading Craftbukkit build #{build}..."
get (BUILD_TEMPLATE % build), "craftbukkit-#{build}.jar"
end
else
halt "invalid build (must be \"latest\", \"recommended\", \"vanilla\" or an integer)"
end
end
desc "start", "starts the server"
method_options :console => :false
def start
ensure_server_shutdown
ensure_jar_exists
ensure_launcher_exists
update_properties
detach = options[:console] ? "" : "-d "
run(server_config["before-start"]) if !server_config["before-start"].nil?
memory = server_config["jvm-memory"]
statusfile = server_config["status-file"]
run(
"screen -m #{detach}-S #{screen_name} sh .craftr-launcher.sh " +
"#{memory} #{jar_filename} #{statusfile}",
:verbose => false
)
command_server "say Server online." unless options[:console]
end
desc "stop", "stops the server"
def stop
ensure_server_running
command_server "stop"
say "Stopping server..."
counter = 0
while counter < 31
sleep 1
if server_state == :stopped
say "Server shut down successfully."
run(server_config["after-stop"]) if !server_config["after-stop"].nil?
return
else
counter = counter + 1
end
end
say "The server is taking too long to shut down. Something's wrong."
end
desc "status", "checks the server status"
def status
case server_state
when :running
say "The server is currently running."
when :crashed
say "The server crashed without shutting down safely!"
say "Check the logs to diagnose the problem, then delete the status file."
when :stopped
say "The server is not running right now."
else
say "The server's status could not be determined!"
end
end
desc "running", "fails if server is not running (for scripts)"
def running
case server_state
when :running
exit 0
else
exit 1
end
end
desc "console", "opens the console (press ctrl-a then d to exit console)"
def console
ensure_server_running
run "screen -r #{screen_name}", :verbose => false
end
desc "command COMMAND...", "sends a console command to the server"
def command(*cmds)
ensure_server_running
command_server(cmds.join(" "))
end
desc "backup", "creates a new backup of the world"
def backup
save_if_running
save_locked do
worldpath = path(server_config["level-name"])
filename = safe_level_name + Time.now.strftime("-%Y-%m-%d-%H-%M-%S") + ".tar.gz"
say "Backing up level to #{filename}..."
backupdir = path(server_config["backups-dir"])
Dir.mkdir(backupdir) if !Dir.exist?(backupdir)
target = path(backupdir, filename)
run "tar -cjf '#{target}' -C '#{worldpath}' ."
end
end
desc "restore ARCHIVE", "restore the world from a backup"
def restore(archive)
ensure_server_shutdown
worldpath = path(server_config["level-name"])
archive = File.expand_path(archive)
run "tar -xf '#{target}' -C '#{worldpath}'"
end
private
def halt(message)
say "Error: #{message}"
exit
end
def path(*components)
File.expand_path(File.join(*components), destination_root)
end
def file_exists(*components)
File.exist?(path(*components))
end
def server_config
@config ||= begin
if file_exists(server_config_path)
File.open(server_config_path) { |io| return YAML.load(io) }
else
DEFAULT_SERVER_CONFIG
end
end
end
def server_config_path
path(
ENV["CRAFTR_SERVER_CONFIG"] || options[:server] || "server-config.yml"
)
end
def safe_level_name(level=nil)
level = level || server_config["level-name"]
level.gsub(/[^a-zA-Z0-9_-]/, "-")
end
def jar_filename(build=nil)
build = build || server_config["server-build"]
build == "vanilla" ? path("minecraft_server.jar") : path("craftbukkit-#{build}.jar")
end
def ensure_jar_exists
if (not file_exists(jar_filename))
invoke :update
end
end
def ensure_launcher_exists
if (not file_exists(".craftr-launcher.sh"))
create_file ".craftr-launcher.sh", LAUNCHER_SCRIPT, :verbose => false
end
end
def server_properties
props = server_config.clone
if props["runtime-level-dir"]
props["level-name"] = File.join(props["runtime-level-dir"], safe_level_name)
end
CRAFTR_KEYS.each { |k| props.delete(k) }
props
end
def update_properties
remove_file("server.properties", :verbose => false) if file_exists("server.properties")
create_file "server.properties", :verbose => false do
lines = []
lines << "# Automatically generated by craftr"
lines << "# Do not edit this file, it will just get overwritten"
lines << "# Make your changes in the server-config.yml file instead"
server_properties.each do |k, v|
lines << "#{k}=#{v.to_s}"
end
lines.join "\n"
end
end
def server_state
status_file = path(server_config["status-file"])
if file_exists(status_file)
File.open(status_file) { |io| io.read.strip }.to_sym
else
:stopped
end
end
def screen_name
server_config["console-screen-name"]
end
def command_server(cmd)
run "screen -p 0 -S #{screen_name} -X stuff '#{cmd}\n'", :verbose => false
end
def save_if_running
return if !server_running
say "Server is running, forcing save in 5 seconds..."
command_server "say Warning - saving level in 5 seconds."
sleep 5
say "Issuing save-all command. Wait 5 seconds..."
command_server "say Warning - saving level now."
command_server "save-all"
sleep 5
say "World saved."
end
def save_locked
if server_running
say "Disabling saving..."
command_server "save-off"
sleep 1
yield
say "Re-enabling saving..."
command_server "save-on"
sleep 1
else
yield
end
end
def server_running
return server_state == :running
end
def ensure_server_running
case server_state
when :running
nil
when :stopped
halt "The server is not running right now."
when :crashed
halt "The server crashed without shutting down safely!"
else
halt "The server's status could not be determined!"
end
end
def ensure_server_shutdown
case server_state
when :running
halt "The server is currently running."
when :stopped
nil
when :crashed
halt "The server crashed without shutting down safely!"
else
halt "The server's status could not be determined!"
end
end
def ensure_server_offline
case server_state
when :running
halt "The server is currently running."
when :stopped
nil
when :crashed
say "Warning: The server crashed without shutting down safely!"
else
halt "The server's status could not be determined!"
end
end
end
Craftr.start
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment