First push

This commit is contained in:
Jack
2023-09-27 14:42:25 +01:00
parent 95468bd21a
commit 0126ff3a1d
44 changed files with 2965 additions and 0 deletions

69
ACC/instances/Executor.py Normal file
View 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)

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

View File

5
ACC/instances/apps.py Normal file
View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class InstancesConfig(AppConfig):
name = 'instances'

View File

View 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 %}

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