First push
This commit is contained in:
69
ACC/instances/Executor.py
Normal file
69
ACC/instances/Executor.py
Normal file
@@ -0,0 +1,69 @@
|
||||
import os, subprocess, time, datetime, json
|
||||
from multiprocessing import Value
|
||||
from threading import Thread
|
||||
from pathlib import Path
|
||||
|
||||
from accservermanager import settings
|
||||
|
||||
try: import resource
|
||||
except: resource = None
|
||||
|
||||
|
||||
class Executor(Thread):
|
||||
"""
|
||||
Thread for running the server process
|
||||
"""
|
||||
def __init__(self, instanceDir):
|
||||
super().__init__()
|
||||
|
||||
# add all configuration values to the object
|
||||
for key, val in json.load(open(os.path.join(instanceDir, 'cfg', 'configuration.json'), 'r', encoding='utf-16')).items():
|
||||
setattr(self, key, val)
|
||||
for key, val in json.load(open(os.path.join(instanceDir, 'cfg', 'settings.json'), 'r', encoding='utf-16')).items():
|
||||
setattr(self, key, val)
|
||||
|
||||
# find the name of the config file, just needed to display it in the instances list
|
||||
self.config = Path(os.path.join(instanceDir, 'cfg', 'event.json')).resolve().name
|
||||
|
||||
self.p = None
|
||||
self.stdout = None
|
||||
self.stderr = None
|
||||
self.serverlog = None
|
||||
self.retval = None
|
||||
self.instanceDir = instanceDir
|
||||
|
||||
self.stop = Value('i', 0)
|
||||
|
||||
def run(self):
|
||||
preexec_fn = None
|
||||
exec = settings.ACCEXEC
|
||||
if resource:
|
||||
# in linux, limit ram to 1GB soft, 2GB hard
|
||||
preexec_fn = lambda: resource.setrlimit(resource.RLIMIT_DATA, (2**30, 2**31))
|
||||
else:
|
||||
# if 'resource' is not available, assume windows which needs to full path to the exec
|
||||
exec = os.path.join(self.instanceDir, *settings.ACCEXEC)
|
||||
|
||||
# fire up the server, store stderr to the log/ dir
|
||||
_tm = datetime.datetime.now().strftime('%Y-%m-%d-%H-%M-%S')
|
||||
self.stdout = os.path.join(self.instanceDir, 'log', 'stdout-%s.log'%(_tm))
|
||||
self.stderr = os.path.join(self.instanceDir, 'log', 'stderr-%s.log'%(_tm))
|
||||
self.serverlog = os.path.join(self.instanceDir, 'log', 'server.log')
|
||||
self.p = subprocess.Popen(exec,
|
||||
# set working dir
|
||||
cwd=self.instanceDir,
|
||||
# limit ram to 1GB soft, 2GB hard
|
||||
preexec_fn=preexec_fn,
|
||||
# shell=True,
|
||||
universal_newlines=True,
|
||||
stdout=open(self.stdout,'w'),
|
||||
stderr=open(self.stderr,'w'))
|
||||
|
||||
# wait for the stop signal or for the server to die on its own
|
||||
self.retval = None
|
||||
while self.retval is None:
|
||||
if self.stop.value == 1: self.p.kill()
|
||||
time.sleep(1)
|
||||
self.retval = self.p.poll()
|
||||
|
||||
print("Retval:",self.retval)
|
119
ACC/instances/InstanceForm.py
Normal file
119
ACC/instances/InstanceForm.py
Normal file
@@ -0,0 +1,119 @@
|
||||
from django import forms
|
||||
|
||||
from accservermanager.settings import CAR_GROUPS
|
||||
from cfgs.confEdit import createLabel
|
||||
from cfgs.confSelect import getCfgsField
|
||||
|
||||
from accservermanager import settings
|
||||
|
||||
|
||||
class BaseForm(forms.Form):
|
||||
def __init__(self, data):
|
||||
super().__init__(data)
|
||||
|
||||
for key in self.fields:
|
||||
if key == 'cfg': continue
|
||||
# generate better label
|
||||
self.fields[key].label = createLabel(key)
|
||||
|
||||
# add help text if available
|
||||
if key in settings.MESSAGES:
|
||||
self.fields[key].help_text = settings.MESSAGES[key]
|
||||
|
||||
# use default values from the 'data' object
|
||||
if key in data:
|
||||
self.fields[key].initial = data[key]
|
||||
|
||||
|
||||
class SettingsForm(BaseForm):
|
||||
"""
|
||||
Form around settings.json
|
||||
"""
|
||||
serverName = forms.CharField(max_length=100)
|
||||
|
||||
password = forms.CharField(max_length=100, required=False)
|
||||
spectatorPassword = forms.CharField(max_length=100, required=False)
|
||||
adminPassword = forms.CharField(max_length=100, required=False)
|
||||
|
||||
carGroup = forms.ChoiceField(widget=forms.Select, choices=CAR_GROUPS)
|
||||
trackMedalsRequirement = forms.IntegerField(max_value=3, min_value=0, required=False)
|
||||
safetyRatingRequirement = forms.IntegerField(max_value=99, min_value=-1, required=False)
|
||||
racecraftRatingRequirement = forms.IntegerField(max_value=99, min_value=-1, required=False)
|
||||
|
||||
maxCarSlots = forms.IntegerField(max_value=1000, min_value=0, required=False)
|
||||
|
||||
isRaceLocked = forms.BooleanField(required=False)
|
||||
allowAutoDQ = forms.BooleanField(required=False)
|
||||
shortFormationLap = forms.BooleanField(required=False)
|
||||
dumpEntryList = forms.BooleanField(required=False)
|
||||
dumpLeaderboards = forms.BooleanField(required=False)
|
||||
randomizeTrackWhenEmpty = forms.BooleanField(required=False)
|
||||
ignorePrematureDisconnects = forms.BooleanField(required=False)
|
||||
|
||||
|
||||
class ConfigurationForm(BaseForm):
|
||||
"""
|
||||
Form around configuration.json
|
||||
"""
|
||||
udpPort = forms.IntegerField(max_value=None, min_value=1000)
|
||||
tcpPort = forms.IntegerField(max_value=None, min_value=1000)
|
||||
maxConnections = forms.IntegerField(max_value=1000, min_value=0, required=False)
|
||||
registerToLobby = forms.BooleanField(required=False)
|
||||
lanDiscovery = forms.BooleanField(required=False)
|
||||
|
||||
|
||||
class AssistRulesForm(BaseForm):
|
||||
"""
|
||||
Form around assistRules.json
|
||||
"""
|
||||
stabilityControlLevelMax = forms.IntegerField(max_value=100, min_value=0, required=False)
|
||||
disableIdealLine = forms.BooleanField(required=False)
|
||||
disableAutosteer = forms.BooleanField(required=False)
|
||||
disableAutoPitLimiter = forms.BooleanField(required=False)
|
||||
disableAutoGear = forms.BooleanField(required=False)
|
||||
disableAutoClutch = forms.BooleanField(required=False)
|
||||
disableAutoEngineStart = forms.BooleanField(required=False)
|
||||
disableAutoWiper = forms.BooleanField(required=False)
|
||||
disableAutoLights = forms.BooleanField(required=False)
|
||||
|
||||
|
||||
class EventRulesForm(BaseForm):
|
||||
"""
|
||||
Form around eventRules.json
|
||||
"""
|
||||
qualifyStandingType = forms.IntegerField(min_value=0, required=False)
|
||||
pitWindowLengthSec = forms.IntegerField(required=False)
|
||||
driverStintTimeSec = forms.IntegerField(required=False)
|
||||
mandatoryPitstopCount = forms.IntegerField(min_value=0, required=False)
|
||||
maxTotalDrivingTime = forms.IntegerField(required=False)
|
||||
maxDriversCount = forms.IntegerField(min_value=0, required=False)
|
||||
isRefuellingAllowedInRace = forms.BooleanField(required=False)
|
||||
isRefuellingTimeFixed = forms.BooleanField(required=False)
|
||||
isMandatoryPitstopRefuellingRequired = forms.BooleanField(required=False)
|
||||
isMandatoryPitstopTyreChangeRequired = forms.BooleanField(required=False)
|
||||
isMandatoryPitstopSwapDriverRequired = forms.BooleanField(required=False)
|
||||
tyreSetCount = forms.IntegerField(min_value=0, required=False)
|
||||
|
||||
|
||||
class InstanceForm(forms.Form):
|
||||
"""
|
||||
Form used to fire up a new server instance
|
||||
"""
|
||||
instanceName = forms.CharField(
|
||||
max_length=100,
|
||||
widget=forms.TextInput(attrs={"onkeyup": "nospaces(this)"}))
|
||||
|
||||
def __init__(self, data):
|
||||
super().__init__(data)
|
||||
self.settings = SettingsForm(data)
|
||||
self.configuration = ConfigurationForm(data)
|
||||
self.assistRules = AssistRulesForm(data)
|
||||
self.eventRules = EventRulesForm(data)
|
||||
|
||||
# There is an issue with the 'required' error, so set this field
|
||||
# to not-required. This is ok, since it is always pre-filled.
|
||||
# This field has to be instantiated here in order to pick-up new configs.
|
||||
self.fields['event'] = getCfgsField(label='Event', required=False)
|
||||
|
||||
def is_valid(self):
|
||||
return self.settings.is_valid() and self.configuration.is_valid() and self.assistRules.is_valid() and self.eventRules.is_valid()
|
0
ACC/instances/__init__.py
Normal file
0
ACC/instances/__init__.py
Normal file
5
ACC/instances/apps.py
Normal file
5
ACC/instances/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class InstancesConfig(AppConfig):
|
||||
name = 'instances'
|
0
ACC/instances/migrations/__init__.py
Normal file
0
ACC/instances/migrations/__init__.py
Normal file
50
ACC/instances/templates/instances/instance.html
Normal file
50
ACC/instances/templates/instances/instance.html
Normal file
@@ -0,0 +1,50 @@
|
||||
{% extends "main.html" %}
|
||||
{% block content %}
|
||||
|
||||
<style>
|
||||
.container {
|
||||
padding: 20px 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
{% load django_bootstrap_breadcrumbs %}
|
||||
{% block breadcrumbs %}
|
||||
{% clear_breadcrumbs %}
|
||||
{% for b, u in path %}
|
||||
{% breadcrumb_raw_safe b u %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% load django_bootstrap5 %}
|
||||
{% bootstrap_css %}
|
||||
{% bootstrap_javascript %}
|
||||
|
||||
<div class="container">
|
||||
<div class="card" style="padding: 10px 16px 20px 16px;">
|
||||
{% render_breadcrumbs %}
|
||||
|
||||
{% for r in resources %}
|
||||
<h6 style="font-weight:bold">{{r.label}} | <a href="{{request.path}}{{r.path}}">{{r.text}}</a> </h6>
|
||||
<textarea id="{{r.path}}" style="height: 200px;" disabled></textarea>
|
||||
<script>
|
||||
(function {{r.path}}() {
|
||||
$.post(
|
||||
'{{request.path}}{{r.path}}',
|
||||
{% if r.update %} { lines: 10 } {% else %} { } {% endif %}
|
||||
).done(function(data) {
|
||||
if (typeof data !== "string") data = JSON.stringify(data, null, 4);
|
||||
$('textarea#{{r.path}}').val(data);
|
||||
{% if r.update %} setTimeout({{r.path}}, 2000); {% endif %}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
<div style="padding: 10px 16px 20px 16px;">
|
||||
<a href="results"><input type="button" value="Results" class="btn btn-success"/></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
166
ACC/instances/templates/instances/instances.html
Normal file
166
ACC/instances/templates/instances/instances.html
Normal file
@@ -0,0 +1,166 @@
|
||||
{% extends "main.html" %}
|
||||
{% block content %}
|
||||
|
||||
<style>
|
||||
.container {
|
||||
padding: 20px 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
{% load django_bootstrap5 %}
|
||||
{% bootstrap_css %}
|
||||
{% bootstrap_javascript %}
|
||||
|
||||
<div class="container">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
|
||||
class="card red"
|
||||
{% else %} class="card green"
|
||||
{% endif %}>
|
||||
<div id="message" {% if message.tags %} class="{{ message.tags }}" {% endif %} style="padding: 10px 5px;">
|
||||
<strong>{{ message|escape }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<div class="card">
|
||||
<h5 style="font-weight:bold">Instance config</h5>
|
||||
<form name="instance_form" action="start" method="POST"
|
||||
onsubmit="return (document.forms['instance_form']['cfg'].value.length>0)">{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
<div class="accordion" id="instance-config">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="settings-header">
|
||||
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#settings" aria-expanded="true" aria-controls="settings">
|
||||
Settings
|
||||
</button>
|
||||
</h2>
|
||||
<div id="settings" class="accordion-collapse collapse show" aria-labelledby="settings-header" data-bs-parent="#instance-config">
|
||||
<div class="accordion-body">
|
||||
{% bootstrap_form form.settings %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="config-header">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#config" aria-expanded="false" aria-controls="config">
|
||||
Configuration
|
||||
</button>
|
||||
</h2>
|
||||
<div id="config" class="accordion-collapse collapse" aria-labelledby="config-header" data-bs-parent="#instance-config">
|
||||
<div class="accordion-body">
|
||||
{% bootstrap_form form.configuration %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="assist-rules-header">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#assist-rules" aria-expanded="false" aria-controls="assist-rules">
|
||||
AssistRules
|
||||
</button>
|
||||
</h2>
|
||||
<div id="assist-rules" class="accordion-collapse collapse" aria-labelledby="assist-rules-header" data-bs-parent="#instance-config">
|
||||
<div class="accordion-body">
|
||||
{% bootstrap_form form.assistRules %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="event-rules-header">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#event-rules" aria-expanded="false" aria-controls="event-rules">
|
||||
EventRules
|
||||
</button>
|
||||
</h2>
|
||||
<div id="event-rules" class="accordion-collapse collapse" aria-labelledby="event-rules-header" data-bs-parent="#instance-config">
|
||||
<div class="accordion-body">
|
||||
{% bootstrap_form form.eventRules %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-success" type="submit">Start</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<h5 style="font-weight:bold">Server instances</h5><br>
|
||||
<table>
|
||||
<tr><td>Instance Name</td><td>Server Name</td><td>Config</td><td>UDP</td><td>TCP</td><td>PID</td><td></td></tr>
|
||||
{% for name,executor in executors.items %}
|
||||
<tr id="row{{forloop.counter0}}">
|
||||
<td><a href="/instances/{{name}}" class="btn btn-primary">{{name}}</a></td>
|
||||
<td style="">{{executor.serverName}}</td>
|
||||
<td style="">{{executor.config}}</td>
|
||||
<td style="">{{executor.udpPort}}</td>
|
||||
<td style="">{{executor.tcpPort}}</td>
|
||||
|
||||
<td id="pid{{forloop.counter0}}" style="">
|
||||
{% if executor.is_alive %}
|
||||
{{executor.p.pid}}
|
||||
{% else %}
|
||||
exited with {{executor.retval}}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if executor.is_alive %}
|
||||
<td>
|
||||
<button id="stop{{forloop.counter0}}" class="btn btn-danger" onclick="stop(this, '{{name}}')">Stop</button>
|
||||
</td><td></td>
|
||||
{% else %}
|
||||
<td>
|
||||
<button id="restart{{forloop.counter0}}" class="btn btn-success" onclick="start(this, '{{name}}')">Start</button>
|
||||
</td><td>
|
||||
<button id="delete{{forloop.counter0}}" class="btn btn-danger" onclick="del(this, '{{name}}')">Delete</button>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$('#message').on('click', function(){
|
||||
$(this).parent().remove();
|
||||
});
|
||||
|
||||
function start(el, name) {
|
||||
el.innerHTML = 'Starting'
|
||||
el.disabled = true;
|
||||
var delel = $('#delete'+el.id.replace('restart',''))[0];
|
||||
delel.disabled = true;
|
||||
$.post("/instances/"+name+"/start", function(json) {
|
||||
$('td#pid'+el.id.replace('restart','')).html(json['pid']);
|
||||
delel.id = el.id.replace('restart','stop');
|
||||
delel.innerHTML = 'Stop';
|
||||
delel.onclick = function() {stop(el, name)};
|
||||
delel.disabled = false;
|
||||
el.remove();
|
||||
});
|
||||
}
|
||||
function stop(el, name) {
|
||||
el.innerHTML = 'Stopping'
|
||||
el.disabled = true;
|
||||
$.post("/instances/"+name+"/stop", function(json) {
|
||||
$('td#pid'+el.id.replace('stop','')).html('exited with '+json['retval']);
|
||||
el.id = el.id.replace('stop','delete')
|
||||
el.innerHTML = 'Delete';
|
||||
el.onclick = function() {del(el, name)};
|
||||
el.disabled = false;
|
||||
});
|
||||
}
|
||||
function del(el, name) {
|
||||
el.disabled = true;
|
||||
$.post("/instances/"+name+"/delete", function(json) {
|
||||
$('tr#row'+el.id.replace('delete','')).remove();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
20
ACC/instances/urls.py
Normal file
20
ACC/instances/urls.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.index, name='instances'),
|
||||
path('start', views.create, name='start'),
|
||||
path('<name>/', views.instance, name='instance'),
|
||||
path('<name>/start', views.start, name='start'),
|
||||
path('<name>/stop', views.stop, name='stop'),
|
||||
path('<name>/delete', views.delete, name='delete'),
|
||||
path('<name>/stderr', views.stderr, name='stderr'),
|
||||
path('<name>/stdout', views.stdout, name='stdout'),
|
||||
path('<name>/serverlog', views.serverlog, name='serverlog'),
|
||||
path('<name>/configuration', views.download_configuration_file, name='configuration'),
|
||||
path('<name>/assistRules', views.download_assistRules_file, name='assistRules'),
|
||||
path('<name>/eventRules', views.download_eventRules_file, name='eventRules'),
|
||||
path('<name>/event', views.download_event_file, name='event'),
|
||||
path('<name>/settings', views.download_settings_file, name='settings'),
|
||||
]
|
330
ACC/instances/views.py
Normal file
330
ACC/instances/views.py
Normal file
@@ -0,0 +1,330 @@
|
||||
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))
|
||||
|
Reference in New Issue
Block a user