Merge branch 'pretzel_update' into 'pretzel'

Added update button and bug fixes

See merge request crafty-controller/crafty-commander!31
This commit is contained in:
Andrew McManus 2021-07-30 14:59:16 +00:00
commit 3d979ffed3
9 changed files with 255 additions and 13 deletions

View File

@ -12,6 +12,7 @@ import logging
import html import html
import zipfile import zipfile
import pathlib import pathlib
import shutil
from datetime import datetime from datetime import datetime
from socket import gethostname from socket import gethostname
@ -561,5 +562,31 @@ class Helpers:
zf.write(os.path.join(root, file), zf.write(os.path.join(root, file),
os.path.relpath(os.path.join(root, file), os.path.relpath(os.path.join(root, file),
os.path.join(path, '..'))) os.path.join(path, '..')))
@staticmethod
def copy_files(source, dest):
if os.path.isfile(source):
shutil.copyfile(source, dest)
logger.info("Copying jar %s to %s", source, dest)
else:
logger.info("Source jar does not exist.")
@staticmethod
def download_file(executable_url, jar_path):
try:
r = requests.get(executable_url, timeout=5)
except Exception as ex:
logger.error("Could not download executable: %s", ex)
return False
if r.status_code != 200:
logger.error("Unable to download file from %s", executable_url)
return False
try:
open(jar_path, "wb").write(r.content)
except Exception as e:
logger.error("Unable to finish executable download. Error: %s", e)
return False
return True
helper = Helpers() helper = Helpers()

View File

@ -122,6 +122,7 @@ class Servers(BaseModel):
auto_start_delay = IntegerField(default=10) auto_start_delay = IntegerField(default=10)
crash_detection = BooleanField(default=0) crash_detection = BooleanField(default=0)
stop_command = CharField(default="stop") stop_command = CharField(default="stop")
executable_update_url = CharField(default="")
server_ip = CharField(default="127.0.0.1") server_ip = CharField(default="127.0.0.1")
server_port = IntegerField(default=25565) server_port = IntegerField(default=25565)
logs_delete_after = IntegerField(default=0) logs_delete_after = IntegerField(default=0)
@ -156,6 +157,7 @@ class Server_Stats(BaseModel):
players = CharField(default="") players = CharField(default="")
desc = CharField(default="Unable to Connect") desc = CharField(default="Unable to Connect")
version = CharField(default="") version = CharField(default="")
updating = BooleanField(default=False)
class Meta: class Meta:
@ -842,6 +844,15 @@ class db_shortcuts:
} }
return conf return conf
@staticmethod
def set_update(server_id, value):
try:
row = Server_Stats.select().where(Server_Stats.server_id == server_id)
except Exception as ex:
logger.error("Database entry not found. ".format(ex))
with database.atomic():
Server_Stats.update(updating=value).where(Server_Stats.server_id == server_id).execute()
@staticmethod @staticmethod
def set_backup_config(server_id: int, backup_path: str = None, max_backups: int = None, auto_enabled: bool = True): def set_backup_config(server_id: int, backup_path: str = None, max_backups: int = None, auto_enabled: bool = True):
logger.debug("Updating server {} backup config with {}".format(server_id, locals())) logger.debug("Updating server {} backup config with {}".format(server_id, locals()))

View File

@ -10,11 +10,13 @@ import threading
import schedule import schedule
import logging.config import logging.config
import zipfile import zipfile
from threading import Thread
from app.classes.shared.helpers import helper from app.classes.shared.helpers import helper
from app.classes.shared.console import console from app.classes.shared.console import console
from app.classes.shared.models import db_helper, Servers from app.classes.shared.models import db_helper, Servers
from app.classes.web.websocket_helper import websocket_helper
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -42,11 +44,14 @@ class Server:
self.settings = None self.settings = None
self.updating = False self.updating = False
self.server_id = None self.server_id = None
self.jar_update_url = None
self.name = None self.name = None
self.is_crashed = False self.is_crashed = False
self.restart_count = 0 self.restart_count = 0
self.crash_watcher_schedule = None self.crash_watcher_schedule = None
self.stats = stats self.stats = stats
self.backup_thread = threading.Thread(target=self.a_backup_server, daemon=True, name="backup")
self.is_backingup = False
def reload_server_settings(self): def reload_server_settings(self):
server_data = db_helper.get_server_data_by_id(self.server_id) server_data = db_helper.get_server_data_by_id(self.server_id)
@ -111,11 +116,16 @@ class Server:
def start_server(self): def start_server(self):
logger.info("Start command detected. Reloading settings from DB for server {}".format(self.name))
self.setup_server_run_command()
# fail safe in case we try to start something already running # fail safe in case we try to start something already running
if self.check_running(): if self.check_running():
logger.error("Server is already running - Cancelling Startup") logger.error("Server is already running - Cancelling Startup")
console.error("Server is already running - Cancelling Startup") console.error("Server is already running - Cancelling Startup")
return False return False
if self.check_update():
logger.error("Server is updating. Terminating startup.")
return False
logger.info("Launching Server {} with command {}".format(self.name, self.server_command)) logger.info("Launching Server {} with command {}".format(self.name, self.server_command))
console.info("Launching Server {} with command {}".format(self.name, self.server_command)) console.info("Launching Server {} with command {}".format(self.name, self.server_command))
@ -127,7 +137,11 @@ class Server:
logger.info("Linux Detected") logger.info("Linux Detected")
logger.info("Starting server in {p} with command: {c}".format(p=self.server_path, c=self.server_command)) logger.info("Starting server in {p} with command: {c}".format(p=self.server_path, c=self.server_command))
try:
self.process = pexpect.spawn(self.server_command, cwd=self.server_path, timeout=None, encoding=None) self.process = pexpect.spawn(self.server_command, cwd=self.server_path, timeout=None, encoding=None)
except Exception as ex:
logger.error("Server {} failed to start with error code: {}".format(self.name, ex))
return False
self.is_crashed = False self.is_crashed = False
self.start_time = str(datetime.datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')) self.start_time = str(datetime.datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S'))
@ -318,20 +332,50 @@ class Server:
console.info("Removing old crash detection watcher thread") console.info("Removing old crash detection watcher thread")
schedule.clear(self.name) schedule.clear(self.name)
def is_backup_running(self):
if self.is_backingup:
return True
else:
return False
def backup_server(self): def backup_server(self):
backup_thread = threading.Thread(target=self.a_backup_server, daemon=True, name="backup")
logger.info("Starting Backup Thread for server {}.".format(self.settings['server_name']))
#checks if the backup thread is currently alive for this server
if not self.is_backingup:
try:
backup_thread.start()
except Exception as ex:
logger.error("Failed to start backup: {}".format(ex))
return False
else:
logger.error("Backup is already being processed for server {}. Canceling backup request".format(self.settings['server_name']))
return False
logger.info("Backup Thread started for server {}.".format(self.settings['server_name']))
def a_backup_server(self):
logger.info("Starting server {} (ID {}) backup".format(self.name, self.server_id)) logger.info("Starting server {} (ID {}) backup".format(self.name, self.server_id))
self.is_backingup = True
conf = db_helper.get_backup_config(self.server_id) conf = db_helper.get_backup_config(self.server_id)
try: try:
backup_filename = "{}/{}.zip".format(conf['backup_path'], datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')) backup_filename = "{}/{}.zip".format(self.settings['backup_path'], datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S'))
logger.info("Creating backup of server '{}' (ID#{}) at '{}'".format(self.settings['server_name'], self.server_id, backup_filename)) logger.info("Creating backup of server '{}' (ID#{}) at '{}'".format(self.settings['server_name'], self.server_id, backup_filename))
helper.zip_directory(backup_filename, self.server_path) helper.zip_directory(backup_filename, self.server_path)
while len(self.list_backups()) > conf["max_backups"]:
backup_list = self.list_backups() backup_list = self.list_backups()
if len(self.list_backups()) > conf["max_backups"]:
oldfile = backup_list[0] oldfile = backup_list[0]
backup_path = self.settings['backup_path']
old_file_name = oldfile['path']
back_old_file = os.path.join(backup_path, old_file_name)
logger.info("Removing old backup '{}'".format(oldfile)) logger.info("Removing old backup '{}'".format(oldfile))
os.remove(oldfile) os.remove(back_old_file)
self.is_backingup = False
logger.info("Backup of server: {} completed".format(self.name))
return
except: except:
logger.exception("Failed to create backup of server {} (ID {})".format(self.name, self.server_id)) logger.exception("Failed to create backup of server {} (ID {})".format(self.name, self.server_id))
self.is_backingup = False
return
def list_backups(self): def list_backups(self):
conf = db_helper.get_backup_config(self.server_id) conf = db_helper.get_backup_config(self.server_id)
@ -340,3 +384,88 @@ class Server:
return [{"path": os.path.relpath(f['path'], start=conf['backup_path']), "size": f["size"]} for f in files] return [{"path": os.path.relpath(f['path'], start=conf['backup_path']), "size": f["size"]} for f in files]
else: else:
return [] return []
def jar_update(self):
db_helper.set_update(self.server_id, True)
update_thread = threading.Thread(target=self.a_jar_update, daemon=True, name="exe_update")
update_thread.start()
def check_update(self):
server_stats = db_helper.get_server_stats_by_id(self.server_id)
if server_stats['updating']:
return True
else:
return False
def a_jar_update(self):
wasStarted = "-1"
self.backup_server()
#checks if server is running. Calls shutdown if it is running.
if self.check_running():
wasStarted = True
logger.info("Server with PID {} is running. Sending shutdown command".format(self.PID))
self.stop_threaded_server()
else:
wasStarted = False
if len(websocket_helper.clients) > 0:
# There are clients
self.check_update()
message = '<a data-id="'+str(self.server_id)+'" class=""> UPDATING...</i></a>'
websocket_helper.broadcast('update_button_status', {
'isUpdating': self.check_update(),
'server_id': self.server_id,
'wasRunning': wasStarted,
'string': message
})
backup_dir = os.path.join(self.settings['path'], 'crafty_executable_backups')
#checks if backup directory already exists
if os.path.isdir(backup_dir):
backup_executable = os.path.join(backup_dir, 'old_server.jar')
else:
logger.info("Executable backup directory not found for Server: {}. Creating one.".format(self.name))
os.mkdir(backup_dir)
backup_executable = os.path.join(backup_dir, 'old_server.jar')
if os.path.isfile(backup_executable):
#removes old backup
logger.info("Old backup found for server: {}. Removing...".format(self.name))
os.remove(backup_executable)
logger.info("Old backup removed for server: {}.".format(self.name))
else:
logger.info("No old backups found for server: {}".format(self.name))
current_executable = os.path.join(self.settings['path'], self.settings['executable'])
#copies to backup dir
helper.copy_files(current_executable, backup_executable)
#boolean returns true for false for success
downloaded = helper.download_file(self.settings['executable_update_url'], current_executable)
if downloaded:
while self.is_backingup:
db_helper.set_update(self.server_id, True)
pass
logger.info("Executable updated successfully. Starting Server")
db_helper.set_update(self.server_id, False)
if len(websocket_helper.clients) > 0:
# There are clients
self.check_update()
websocket_helper.broadcast('notification', "Executable update finished for " + self.name)
time.sleep(3)
websocket_helper.broadcast('update_button_status', {
'isUpdating': self.check_update(),
'server_id': self.server_id,
'wasRunning': wasStarted
})
websocket_helper.broadcast('notification', "Executable update finished for "+self.name)
db_helper.add_to_audit_log_raw('Alert', '-1', self.server_id, "Executable update finished for "+self.name, self.settings['server_ip'])
if wasStarted:
self.start_server()
else:
time.sleep(5)
db_helper.set_update(self.server_id, False)
websocket_helper.broadcast('notification', "Executable update failed for " + self.name+". Check log file for details.")
logger.error("Executable download failed.")

View File

@ -110,6 +110,9 @@ class TasksManager:
elif command == "backup_server": elif command == "backup_server":
svr.backup_server() svr.backup_server()
elif command == "update_executable":
svr.jar_update()
db_helper.mark_command_complete(c.get('command_id', None)) db_helper.mark_command_complete(c.get('command_id', None))
time.sleep(1) time.sleep(1)

View File

@ -426,6 +426,7 @@ class PanelHandler(BaseHandler):
auto_start_delay = self.get_argument('auto_start_delay', '10') auto_start_delay = self.get_argument('auto_start_delay', '10')
server_ip = self.get_argument('server_ip', None) server_ip = self.get_argument('server_ip', None)
server_port = self.get_argument('server_port', None) server_port = self.get_argument('server_port', None)
executable_update_url = self.get_argument('executable_update_url', None)
auto_start = int(float(self.get_argument('auto_start', '0'))) auto_start = int(float(self.get_argument('auto_start', '0')))
crash_detection = int(float(self.get_argument('crash_detection', '0'))) crash_detection = int(float(self.get_argument('crash_detection', '0')))
logs_delete_after = int(float(self.get_argument('logs_delete_after', '0'))) logs_delete_after = int(float(self.get_argument('logs_delete_after', '0')))
@ -454,6 +455,7 @@ class PanelHandler(BaseHandler):
Servers.server_ip: server_ip, Servers.server_ip: server_ip,
Servers.server_port: server_port, Servers.server_port: server_port,
Servers.auto_start: auto_start, Servers.auto_start: auto_start,
Servers.executable_update_url: executable_update_url,
Servers.crash_detection: crash_detection, Servers.crash_detection: crash_detection,
Servers.logs_delete_after: logs_delete_after, Servers.logs_delete_after: logs_delete_after,
}).where(Servers.server_id == server_id).execute() }).where(Servers.server_id == server_id).execute()

View File

@ -123,11 +123,12 @@
</a> </a>
</td> </td>
<td class="actions_serverlist"> <td id="controls{{server['server_data']['server_id']}}" class="actions_serverlist">
{% if server['stats']['running'] %} {% if server['stats']['running'] %}
<a class="stop_button" data-id="{{server['server_data']['server_id']}}"> <i class="fas fa-stop"></i></a> &nbsp; <a class="stop_button" data-id="{{server['server_data']['server_id']}}"> <i class="fas fa-stop"></i></a> &nbsp;
<a class="restart_button" data-id="{{server['server_data']['server_id']}}"> <i class="fas fa-sync"></i></a> &nbsp; <a class="restart_button" data-id="{{server['server_data']['server_id']}}"> <i class="fas fa-sync"></i></a> &nbsp;
{% elif server['stats']['updating']%}
<a data-id="{{server['server_data']['server_id']}}" class=""> UPDATING...</i></a>
{% else %} {% else %}
<a data-id="{{server['server_data']['server_id']}}" class="play_button"><i class="fas fa-play"></i></a> &nbsp; <a data-id="{{server['server_data']['server_id']}}" class="play_button"><i class="fas fa-play"></i></a> &nbsp;
<a data-id="{{server['server_data']['server_id']}}" class="clone_button"> <i class="fas fa-clone"></i></a>&nbsp; <a data-id="{{server['server_data']['server_id']}}" class="clone_button"> <i class="fas fa-clone"></i></a>&nbsp;
@ -281,6 +282,21 @@ $( document ).ready(function() {
mem_percent.textContent = hostStats.mem_percent + '%'; mem_percent.textContent = hostStats.mem_percent + '%';
}); });
} }
if (webSocket) {
webSocket.on('update_button_status', function (updateButton) {
var id = 'controls';
var dataId = updateButton.server_id;
var string = updateButton.string
var id = id.concat(updateButton.server_id);
if (updateButton.isUpdating){
console.log(updateButton.isUpdating)
document.getElementById(id).innerHTML = string;
}
else{
window.location.reload()
}
});
}
$( ".clone_button" ).click(function() { $( ".clone_button" ).click(function() {
server_id = $(this).attr("data-id"); server_id = $(this).attr("data-id");

View File

@ -106,6 +106,11 @@
<input type="number" class="form-control" name="auto_start_delay" id="auto_start_delay" value="{{ data['server_stats']['server_id']['auto_start_delay'] }}" step="1" max="999" min="10" > <input type="number" class="form-control" name="auto_start_delay" id="auto_start_delay" value="{{ data['server_stats']['server_id']['auto_start_delay'] }}" step="1" max="999" min="10" >
</div> </div>
<div class="form-group">
<label for="executable_update_url">{{ translate('serverConfig', 'exeUpdateURL') }} <small class="text-muted ml-1"> - {{ translate('serverConfig', 'exeUpdateURLDesc') }}</small> </label>
<input type="text" class="form-control" name="executable_update_url" id="executable_update_url" value="{{ data['server_stats']['server_id']['executable_update_url'] }}" placeholder="{{ translate('serverConfig', 'exeUpdateURL') }}" >
</div>
<div class="form-group"> <div class="form-group">
<label for="server_ip">{{ translate('serverConfig', 'serverPort') }} <small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverPortDesc') }}</small> </label> <label for="server_ip">{{ translate('serverConfig', 'serverPort') }} <small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverPortDesc') }}</small> </label>
<input type="text" class="form-control" name="server_ip" id="server_ip" value="{{ data['server_stats']['server_id']['server_ip'] }}"> <input type="text" class="form-control" name="server_ip" id="server_ip" value="{{ data['server_stats']['server_id']['server_ip'] }}">
@ -159,9 +164,11 @@
</div> </div>
<div class="text-center"> <div class="text-center">
{% if data['server_stats']['running'] %} {% if data['server_stats']['running'] %}
<button onclick="send_command(server_id, 'update_executable');" id="update_executable" style="max-width: 7rem;" class="btn btn-warning m-1 flex-grow-1 disabled">{{ translate('serverConfig', 'update') }}</button>
<a class="btn btn-sm btn-danger disabled">{{ translate('serverConfig', 'deleteServer') }}</a><br /> <a class="btn btn-sm btn-danger disabled">{{ translate('serverConfig', 'deleteServer') }}</a><br />
<small>{{ translate('serverConfig', 'stopBeforeDeleting') }}</small> <small>{{ translate('serverConfig', 'stopBeforeDeleting') }}</small>
{% else %} {% else %}
<button onclick="send_command(server_id, 'update_executable');" id="update_executable" style="max-width: 7rem;" class="btn btn-warning m-1 flex-grow-1">{{ translate('serverConfig', 'update') }}</button>
<a href="/panel/remove_server?id={{ data['server_stats']['server_id']['server_id'] }}" class="btn btn-sm btn-danger">{{ translate('serverConfig', 'deleteServer') }}</a> <a href="/panel/remove_server?id={{ data['server_stats']['server_id']['server_id'] }}" class="btn btn-sm btn-danger">{{ translate('serverConfig', 'deleteServer') }}</a>
{% end %} {% end %}
@ -196,6 +203,28 @@
}); });
let server_id = '{{ data['server_stats']['server_id']['server_id'] }}';
function send_command (server_id, command){
<!-- this getCookie function is in base.html-->
var token = getCookie("_xsrf");
$.ajax({
type: "POST",
headers: {'X-XSRFToken': token},
url: '/server/command?command=' + command + '&id=' + server_id,
success: function(data){
console.log("got response:");
console.log(data);
setTimeout(function(){ location.reload(); }, 10000);
}
});bootbox.alert({
backdrop: true,
title: '{% raw translate("serverConfig", "sendingRequest") %}',
message: '<div align="center"><i class="fas fa-spin fa-spinner"></i> &nbsp; {% raw translate("serverConfig", "bePatientUpdate") %} </div>'
});
}
</script> </script>

View File

@ -83,12 +83,19 @@
<button id="submit" class="btn btn-sm btn-info" type="button">{{ translate('serverTerm', 'sendCommand') }}</button> <button id="submit" class="btn btn-sm btn-info" type="button">{{ translate('serverTerm', 'sendCommand') }}</button>
</span> </span>
</div> </div>
{% if data['server_stats']['updating']%}
<div class="mt-4 flex-wrap d-flex justify-content-between justify-content-md-center align-items-center px-5 px-md-0"> <div id="update_control_buttons" class="mt-4 flex-wrap d-flex justify-content-between justify-content-md-center align-items-center px-5 px-md-0" style="visibility: visible">
<button onclick="" id="start-btn" style="max-width: 7rem;" class="btn btn-warning m-1 flex-grow-1 disabled">{{ translate('serverTerm', 'updating') }}</button>
<button onclick="" id="restart-btn" style="max-width: 7rem;" class="btn btn-outline-primary m-1 flex-grow-1 disabled">{% raw translate('serverTerm', 'restart') %}</button>
<button onclick="" id="stop-btn" style="max-width: 7rem;" class="btn btn-danger m-1 flex-grow-1 disabled">{{ translate('serverTerm', 'stop') }}</button>
</div>
{% else %}
<div id="control_buttons" class="mt-4 flex-wrap d-flex justify-content-between justify-content-md-center align-items-center px-5 px-md-0" style="visibility: visible">
<button onclick="send_command(server_id, 'start_server');" id="start-btn" style="max-width: 7rem;" class="btn btn-primary m-1 flex-grow-1">{{ translate('serverTerm', 'start') }}</button> <button onclick="send_command(server_id, 'start_server');" id="start-btn" style="max-width: 7rem;" class="btn btn-primary m-1 flex-grow-1">{{ translate('serverTerm', 'start') }}</button>
<button onclick="send_command(server_id, 'restart_server');" id="restart-btn" style="max-width: 7rem;" class="btn btn-outline-primary m-1 flex-grow-1">{% raw translate('serverTerm', 'restart') %}</button> <button onclick="send_command(server_id, 'restart_server');" id="restart-btn" style="max-width: 7rem;" class="btn btn-outline-primary m-1 flex-grow-1">{% raw translate('serverTerm', 'restart') %}</button>
<button onclick="send_command(server_id, 'stop_server');" id="stop-btn" style="max-width: 7rem;" class="btn btn-danger m-1 flex-grow-1">{{ translate('serverTerm', 'stop') }}</button> <button onclick="send_command(server_id, 'stop_server');" id="stop-btn" style="max-width: 7rem;" class="btn btn-danger m-1 flex-grow-1">{{ translate('serverTerm', 'stop') }}</button>
</div> </div>
{% end %}
</div> </div>
@ -125,6 +132,18 @@
} }
}); });
} }
if (webSocket) {
webSocket.on('update_button_status', function (updateButton) {
if (updateButton.isUpdating){
console.log(updateButton.isUpdating)
document.getElementById('control_buttons').innerHTML = '<button onclick="" id="start-btn" style="max-width: 7rem;" class="btn btn-primary m-1 flex-grow-1">{{ translate("serverTerm", "updating") }}</button><button onclick="" id="restart-btn" style="max-width: 7rem;" class="btn btn-outline-primary m-1 flex-grow-1">{% raw translate("serverTerm", "restart") %}</button><button onclick="" id="stop-btn" style="max-width: 7rem;" class="btn btn-danger m-1 flex-grow-1 disabled">{{ translate("serverTerm", "stop") }}</button>';
}
else{
window.location.reload()
document.getElementById('update_control_buttons').innerHTML = '<button onclick="send_command(server_id, "start_server");" id="start-btn" style="max-width: 7rem;" class="btn btn-primary m-1 flex-grow-1">{{ translate("serverTerm", "start") }}</button><button onclick="send_command(server_id, "restart_server");" id="restart-btn" style="max-width: 7rem;" class="btn btn-outline-primary m-1 flex-grow-1">{% raw translate("serverTerm", "restart") %}</button><button onclick="" id="stop-btn" style="max-width: 7rem;" class="btn btn-danger m-1 flex-grow-1 disabled">{{ translate("serverTerm", "stop") }}</button>';
}
});
}
// Convert running to lower case (example: 'True' converts to 'true') and // Convert running to lower case (example: 'True' converts to 'true') and
// then to boolean via JSON.parse() // then to boolean via JSON.parse()

View File

@ -125,7 +125,8 @@
"sendCommand": "Send command", "sendCommand": "Send command",
"start": "Start", "start": "Start",
"restart": "Restart", "restart": "Restart",
"stop": "Stop" "stop": "Stop",
"updating": "Updating..."
}, },
"serverPlayerManagement": { "serverPlayerManagement": {
"players": "Players", "players": "Players",
@ -199,7 +200,12 @@
"save": "Save", "save": "Save",
"cancel": "Cancel", "cancel": "Cancel",
"deleteServer": "Delete Server", "deleteServer": "Delete Server",
"stopBeforeDeleting": "Please stop the server before deleting it" "stopBeforeDeleting": "Please stop the server before deleting it",
"exeUpdateURLDesc": "Direct Download URL for updates.",
"exeUpdateURL": "Server Executable Update URL",
"update": "Update Executable",
"bePatientUpdate": "Please be patient while we update the server. Download times can vary depending upon your internet speeds.<br /> This screen will refresh in a moment",
"sendingRequest": "Sending your request..."
}, },
"serverConfigHelp": { "serverConfigHelp": {
"title": "Server Config Area", "title": "Server Config Area",