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))