331 lines
11 KiB
Python
331 lines
11 KiB
Python
import os, shutil, json, time, string, glob, platform
|
|
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.http import HttpResponse, HttpResponseRedirect, Http404
|
|
from django.template import loader
|
|
from django.contrib import messages
|
|
|
|
from random_word import RandomWords
|
|
|
|
from instances.Executor import Executor
|
|
from instances.InstanceForm import InstanceForm
|
|
|
|
try: r = RandomWords()
|
|
except: r = None
|
|
|
|
from accservermanager import settings
|
|
|
|
|
|
# the server process execution threads
|
|
executors = {}
|
|
|
|
|
|
# resources a running instance uses and can be viewed/downloaded
|
|
# format is (path,label,text), see instance.html
|
|
resources = [
|
|
('stdout', "Latest stdout", "Download full"),
|
|
('stderr', "Latest stderr", "Download full"),
|
|
('serverlog', "Server log", "Download full"),
|
|
('configuration', "configuration.json", "Download"),
|
|
('event', "event.json", "Download"),
|
|
('settings', "settings.json", "Download"),
|
|
('assistRules', "assistRules.json", "Download"),
|
|
('eventRules', "eventRules.json", "Download"),
|
|
]
|
|
|
|
@login_required
|
|
def instance(request, name):
|
|
if name not in executors: return HttpResponseRedirect('/instances')
|
|
template = loader.get_template('instances/instance.html')
|
|
|
|
path = request.path
|
|
if path[0] == '/': path = path[1:]
|
|
if path[-1] == '/':path = path[:-1]
|
|
path = path.split('/')
|
|
|
|
return HttpResponse(template.render(
|
|
{'path' : [(j, '/'+'/'.join(path[:i+1])) for i,j in enumerate(path)],
|
|
'resources': [dict(path=r[0], label=r[1], text=r[2], update=r[0] in ['stdout','stderr','serverlog']) for r in resources]},
|
|
request))
|
|
|
|
|
|
@login_required
|
|
def stdout(request, name):
|
|
if 'lines' not in request.POST:
|
|
return download(executors[name].stdout)
|
|
return log(executors[name].stdout, int(request.POST['lines']))
|
|
|
|
|
|
@login_required
|
|
def stderr(request, name):
|
|
if 'lines' not in request.POST:
|
|
return download(executors[name].stderr)
|
|
return log(executors[name].stderr, int(request.POST['lines']))
|
|
|
|
|
|
@login_required
|
|
def serverlog(request, name):
|
|
if 'lines' not in request.POST:
|
|
return download(executors[name].serverlog)
|
|
return log(executors[name].serverlog, int(request.POST['lines']))
|
|
|
|
|
|
@login_required
|
|
def download_configuration_file(request, name):
|
|
f = os.path.join(settings.INSTANCES, name, 'cfg', 'configuration.json')
|
|
return download(f, content_type='text/json')
|
|
|
|
|
|
@login_required
|
|
def download_event_file(request, name):
|
|
f = os.path.join(settings.INSTANCES, name, 'cfg', 'event.json')
|
|
return download(f, content_type='text/json')
|
|
|
|
|
|
@login_required
|
|
def download_settings_file(request, name):
|
|
f = os.path.join(settings.INSTANCES, name, 'cfg', 'settings.json')
|
|
return download(f, content_type='text/json')
|
|
|
|
|
|
@login_required
|
|
def download_assistRules_file(request, name):
|
|
f = os.path.join(settings.INSTANCES, name, 'cfg', 'assistRules.json')
|
|
return download(f, content_type='text/json')
|
|
|
|
|
|
@login_required
|
|
def download_eventRules_file(request, name):
|
|
f = os.path.join(settings.INSTANCES, name, 'cfg', 'eventRules.json')
|
|
return download(f, content_type='text/json')
|
|
|
|
|
|
|
|
# https://stackoverflow.com/questions/136168/get-last-n-lines-of-a-file-with-python-similar-to-tail
|
|
def tail(f, n=10):
|
|
assert n >= 0
|
|
pos, lines = n+1, []
|
|
while len(lines) <= n:
|
|
try:
|
|
f.seek(-pos, 2)
|
|
except IOError:
|
|
f.seek(0)
|
|
break
|
|
finally:
|
|
lines = list(f)
|
|
pos *= 2
|
|
return lines[-n:]
|
|
|
|
|
|
def log(_f, n):
|
|
if _f is not None and os.path.isfile(_f):
|
|
with open(_f, 'r', encoding='latin-1') as fh:
|
|
return HttpResponse(tail(fh, n))
|
|
raise Http404
|
|
|
|
|
|
def download(_f, content_type="text/plain"):
|
|
if _f is not None and os.path.isfile(_f):
|
|
with open(_f, 'r', encoding='utf-16' if content_type=='text/json' else None) as fh:
|
|
response = HttpResponse(fh.read(), content_type=content_type)
|
|
response['Content-Disposition'] = 'inline; filename=' + os.path.basename(_f)
|
|
return response
|
|
raise Http404
|
|
|
|
|
|
@login_required
|
|
def delete(request, name):
|
|
if name in executors:
|
|
if not executors[name].is_alive():
|
|
shutil.rmtree(executors[name].instanceDir)
|
|
executors.pop(name)
|
|
|
|
return HttpResponse(json.dumps({'success': True}),
|
|
content_type='application/json')
|
|
|
|
|
|
@login_required
|
|
def stop(request, name):
|
|
""" handle stop request from client """
|
|
global executors
|
|
if name in executors and executors[name].stop.value != 1:
|
|
executors[name].stop.value = 1
|
|
|
|
i = 0
|
|
# wait max 2 seconds for the instance to stop
|
|
while executors[name].is_alive() and i<10:
|
|
time.sleep(.2)
|
|
i+=1
|
|
|
|
# return HttpResponseRedirect('/instances')
|
|
return HttpResponse(json.dumps({'success': True, 'retval':executors[name].retval}),
|
|
content_type='application/json')
|
|
|
|
|
|
@login_required
|
|
def start(request, name):
|
|
""" handle (re)start request from client """
|
|
global executors
|
|
|
|
# create the Executor for the instance
|
|
# - if it does not exist yet
|
|
# - if the executor thread was alive and exited
|
|
if name not in executors or \
|
|
(not executors[name].is_alive() and executors[name].retval is not None):
|
|
inst_dir = os.path.join(settings.INSTANCES, name)
|
|
executors[name] = Executor(inst_dir)
|
|
|
|
# don't try to start running instances
|
|
if not executors[name].is_alive():
|
|
executors[name].start()
|
|
i = 0
|
|
# wait max 2 seconds for the instance to start
|
|
while (not executors[name].is_alive() or executors[name].p is None) and i<10:
|
|
time.sleep(.2)
|
|
i+=1
|
|
|
|
return HttpResponse(json.dumps({'success': True, 'pid':executors[name].p.pid}),
|
|
content_type='application/json')
|
|
|
|
|
|
def render_from(request, form):
|
|
template = loader.get_template('instances/instances.html')
|
|
context = {
|
|
'form': form,
|
|
'executors': executors,
|
|
}
|
|
return HttpResponse(template.render(context, request))
|
|
|
|
|
|
def write_config(name, inst_dir, form):
|
|
### use the values of the default *.json as basis
|
|
cfg = {}
|
|
if os.path.isfile(os.path.join(settings.ACCSERVER, 'cfg', name)):
|
|
cfg = json.load(open(os.path.join(settings.ACCSERVER, 'cfg', name), 'r', encoding='utf-16'))
|
|
|
|
for key in form.cleaned_data.keys():
|
|
if key == 'csrfmiddlewaretoken': continue
|
|
value = form.cleaned_data[key]
|
|
# eventRules needs to be true/false not 0/1
|
|
if name != 'eventRules.json':
|
|
if isinstance(value, bool): value = int(value)
|
|
|
|
if value is not None: cfg[key] = value
|
|
|
|
# write the file into the instances' directory
|
|
json.dump(cfg, open(os.path.join(inst_dir, 'cfg', name), 'w', encoding='utf-16'))
|
|
|
|
|
|
@login_required
|
|
def create(request):
|
|
""" handle create/start request from client """
|
|
|
|
# this function should only be entered via POST request
|
|
if request.method != 'POST':
|
|
return HttpResponseRedirect('/instances')
|
|
|
|
form = InstanceForm(request.POST)
|
|
name = request.POST['instanceName']
|
|
|
|
# form is invalid...
|
|
if not form.is_valid():
|
|
messages.error(request, "Form is not valid")
|
|
return render_from(request, form)
|
|
|
|
# instance with similar name already exists
|
|
if name in executors:
|
|
messages.error(request, "Instance with similar name already exists")
|
|
return render_from(request, form)
|
|
|
|
if not settings.ALLOW_SAME_PORTS and form.configuration['udpPort'].value() == form.configuration['tcpPort'].value():
|
|
messages.error(request, 'UDP and TCP port have to be different')
|
|
return render_from(request, form)
|
|
|
|
# check if a running instance already uses the same ports
|
|
if len(list(filter(lambda x: x.is_alive() and
|
|
(form.configuration['udpPort'].value() in [x.udpPort, x.tcpPort] or
|
|
form.configuration['tcpPort'].value() in [x.udpPort, x.tcpPort]),
|
|
executors.values()))) > 0:
|
|
messages.error(request, "The ports are already in use")
|
|
return render_from(request, form)
|
|
|
|
# create instance environment
|
|
inst_dir = os.path.join(settings.INSTANCES, name)
|
|
if os.path.isdir(inst_dir):
|
|
messages.error(request, "The instance directory exists already")
|
|
return render_from(request, form)
|
|
|
|
# create the directory for the instance
|
|
os.makedirs(os.path.join(inst_dir, 'cfg'))
|
|
os.makedirs(os.path.join(inst_dir, 'log'))
|
|
# link the server exe into the instance environment
|
|
os.symlink(os.path.join(settings.ACCSERVER, settings.SERVER_FILES[0]),
|
|
os.path.join(inst_dir, settings.SERVER_FILES[0]))
|
|
# the target configuration json
|
|
cfg = os.path.join(settings.CONFIGS, form['event'].value() + '.json')
|
|
# link the requested config into the instance environment
|
|
os.symlink(cfg, os.path.join(inst_dir, 'cfg', 'event.json'))
|
|
# link (possible) cars directory into the instance environment
|
|
if os.path.isdir(os.path.join(settings.ACCSERVER, 'cfg', 'cars')):
|
|
os.symlink(os.path.join(settings.ACCSERVER, 'cfg', 'cars'),
|
|
os.path.join(inst_dir, 'cfg', 'cars'))
|
|
|
|
# write the json files
|
|
write_config('configuration.json', inst_dir, form.configuration)
|
|
write_config('settings.json', inst_dir, form.settings)
|
|
write_config('assistRules.json', inst_dir, form.assistRules)
|
|
write_config('eventRules.json', inst_dir, form.eventRules)
|
|
|
|
# start the instance
|
|
start(request, name)
|
|
|
|
messages.info(request, "Successfully started the instance")
|
|
return HttpResponseRedirect('/instances')
|
|
|
|
|
|
def random_word():
|
|
s = 'somename'
|
|
try:
|
|
while s is None or any(c for c in s if c not in string.ascii_letters):
|
|
s = r.get_random_word(hasDictionaryDef="true",
|
|
minLength=5,
|
|
maxLength=10)
|
|
except: pass
|
|
return s
|
|
|
|
|
|
def index(request):
|
|
# read defaults from files
|
|
cfg = json.load(open(os.path.join(
|
|
settings.ACCSERVER, 'cfg', 'configuration.json'), 'r', encoding='utf-16'))
|
|
cfg.update(json.load(open(os.path.join(
|
|
settings.ACCSERVER, 'cfg', 'settings.json'), 'r', encoding='utf-16')))
|
|
cfg.update(json.load(open(os.path.join(
|
|
settings.ACCSERVER, 'cfg', 'assistRules.json'), 'r', encoding='utf-16')))
|
|
if os.path.isfile(os.path.join(settings.ACCSERVER, 'cfg', 'eventRules.json')):
|
|
cfg.update(json.load(open(os.path.join(
|
|
settings.ACCSERVER, 'cfg', 'eventRules.json'), 'r', encoding='utf-16')))
|
|
else:
|
|
cfg.update(settings.EVENT_RULES_TEMPLATE)
|
|
|
|
# some static defaults
|
|
cfg['instanceName'] = random_word()
|
|
cfg['serverName'] = 'ACC server'
|
|
cfg['dumpLeaderboards'] = 1
|
|
cfg['registerToLobby'] = 1
|
|
cfg['dumpLeaderboards'] = 1
|
|
# this setting seems to work only in windows
|
|
cfg['ignorePrematureDisconnects'] = platform.system() == "Windows"
|
|
|
|
# overwrite nonsense trackMedalsRequirement default value
|
|
if cfg['trackMedalsRequirement'] == -1:
|
|
cfg['trackMedalsRequirement'] = 0
|
|
|
|
for inst_dir in glob.glob(os.path.join(settings.INSTANCES, '*')):
|
|
inst_name = os.path.split(inst_dir)[-1]
|
|
if inst_name not in executors:
|
|
executors[inst_name] = Executor(inst_dir)
|
|
|
|
return render_from(request, InstanceForm(cfg))
|
|
|