Smush the WorkflowTasks and Tasks together; liberally apply superglue and duct tape

This commit is contained in:
RunasSudo 2017-12-12 20:02:02 +10:30
parent f538c33d19
commit 788c5c006c
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
14 changed files with 204 additions and 42 deletions

View File

@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from eos.core.objects import *
from eos.core.tasks import *
class WorkflowTask(EmbeddedObject):
class Status:
@ -64,7 +65,7 @@ class WorkflowTask(EmbeddedObject):
@classmethod
def satisfies(cls, descriptor):
return cls._name == descriptor or descriptor in cls.provides or (descriptor in EosObject.objects and issubclass(cls, EosObject.objects[descriptor]))
return cls._name == descriptor or descriptor in cls.provides or (descriptor in EosObject.objects and issubclass(cls, EosObject.lookup(descriptor)))
def on_enter(self):
self.exit()
@ -113,6 +114,22 @@ class Workflow(EmbeddedObject):
except StopIteration:
return None
class WorkflowTaskEntryTask(Task):
election_id = UUIDField()
workflow_task = StringField()
def _run(self):
election = EosObject.lookup('eos.base.election.Election').get_by_id(self.election_id)
task = election.workflow.get_task(self.workflow_task)
task.enter()
election.save()
@property
def label(self):
election = EosObject.lookup('eos.base.election.Election').get_by_id(self.election_id)
task = election.workflow.get_task(self.workflow_task)
return task.label + '' + election.name
# Concrete tasks
# ==============
@ -138,7 +155,7 @@ class TaskDecryptVotes(WorkflowTask):
election = self.recurse_parents('eos.base.election.Election')
for _ in range(len(election.questions)):
election.results.append(EosObject.objects['eos.base.election.RawResult']())
election.results.append(EosObject.lookup('eos.base.election.RawResult')())
for voter in election.voters:
if len(voter.votes) > 0:

View File

@ -37,6 +37,7 @@ if is_python:
import base64
from datetime import datetime
import hashlib
import importlib
import json
import uuid
__pragma__('noskip')
@ -177,9 +178,6 @@ class EosObjectType(type):
cls._name = (cls.__module__ + '.' + cls.__name__).replace('.js.', '.').replace('.python.', '.') #TNYI: qualname
if name != 'EosObject':
EosObject.objects[cls._name] = cls
if '_db_name' not in attrs:
# Don't inherit _db_name, use only if explicitly given
cls._db_name = cls._name
return cls
class EosObject(metaclass=EosObjectType):
@ -195,7 +193,7 @@ class EosObject(metaclass=EosObjectType):
def recurse_parents(self, cls):
#if not isinstance(cls, type):
if isinstance(cls, str):
cls = EosObject.objects[cls]
cls = EosObject.lookup(cls)
if isinstance(self, cls):
return self
@ -208,6 +206,13 @@ class EosObject(metaclass=EosObjectType):
return False
return EosObject.serialise_and_wrap(self) == EosObject.serialise_and_wrap(other)
@staticmethod
def lookup(name):
if name in EosObject.objects:
return EosObject.objects[name]
importlib.import_module(name[:name.rindex('.')])
return EosObject.objects[name]
@staticmethod
def serialise_and_wrap(value, object_type=None, for_hash=False, should_protect=False):
if object_type:
@ -220,7 +225,9 @@ class EosObject(metaclass=EosObjectType):
def deserialise_and_unwrap(value, object_type=None):
if object_type:
return object_type.deserialise(value)
return EosObject.objects[value['type']].deserialise(value['value'])
if value:
return EosObject.lookup(value['type']).deserialise(value['value'])
return None
@staticmethod
def to_json(value):
@ -399,7 +406,25 @@ class DocumentObject(EosObject, metaclass=DocumentObjectType):
attrs[val.internal_name] = val.deserialise(value[val.real_name])
return cls(**attrs)
class TopLevelObject(DocumentObject):
class TopLevelObjectType(DocumentObjectType):
def __new__(meta, name, bases, attrs):
cls = DocumentObjectType.__new__(meta, name, bases, attrs)
# TopLevelObject obviously has no _db_name
if cls._name == 'eos.core.objects.TopLevelObject':
pass
else:
if '_db_name' not in attrs:
cls._db_name = cls._name
# If _db_name is False, then explicitly use the _name. Otherwise, inherit.
if cls._db_name is not False:
for base in bases:
if hasattr(base, '_db_name'):
cls._db_name = base._db_name
break
return cls
class TopLevelObject(DocumentObject, metaclass=TopLevelObjectType):
def save(self):
#res = db[self._name].replace_one({'_id': self.serialise()['_id']}, self.serialise(), upsert=True)
#res = dbinfo.db[self._db_name].replace_one({'_id': self._fields['_id'].serialise(self._id)}, EosObject.serialise_and_wrap(self), upsert=True)
@ -411,6 +436,8 @@ class TopLevelObject(DocumentObject):
@classmethod
def get_by_id(cls, _id):
if not isinstance(_id, str):
_id = str(_id)
return EosObject.deserialise_and_unwrap(dbinfo.provider.get_by_id(cls._db_name, _id))
class EmbeddedObject(DocumentObject):

View File

@ -28,8 +28,14 @@ class Task(TopLevelObject):
TIMEOUT = -20
_id = UUIDField()
status = IntField(default=0)
run_strategy = EmbeddedObjectField()
run_at = DateTimeField()
started_at = DateTimeField()
completed_at = DateTimeField()
status = IntField(default=0)
messages = ListField(StringField())
def run(self):
@ -38,26 +44,60 @@ class Task(TopLevelObject):
def _run(self):
pass
class DummyTask(Task):
_db_name = Task._db_name
label = 'A dummy task'
def _run(self):
if is_python:
#__pragma__('skip')
import time
#__pragma__('noskip')
time.sleep(15)
class RunStrategy(DocumentObject):
def run(self, task):
raise Exception('Not implemented')
class DirectRunStrategy(RunStrategy):
def run(self, task):
task.status = Task.Status.PROCESSING
task.save()
class TaskScheduler:
@staticmethod
def pending_tasks():
pending_tasks = []
tasks = Task.get_all()
try:
task._run()
task.status = Task.Status.COMPLETE
task.save()
except Exception as e:
task.status = Task.Status.FAILED
if is_python:
#__pragma__('skip')
import traceback
#__pragma__('noskip')
task.messages.append(traceback.format_exc())
else:
task.messages.append(repr(e))
task.save()
for task in tasks:
if task.status == Task.Status.READY and task.run_at and task.run_at < DateTimeField.now():
pending_tasks.append(task)
return pending_tasks
@staticmethod
def active_tasks():
active_tasks = []
tasks = Task.get_all()
for task in tasks:
if task.status == Task.Status.PROCESSING:
active_tasks.append(task)
return active_tasks
@staticmethod
def completed_tasks(limit=None):
completed_tasks = []
tasks = Task.get_all()
for task in tasks:
if task.status == Task.Status.COMPLETE or task.status < 0:
completed_tasks.append(task)
if limit:
completed_tasks.sort(key=lambda x: x.completed_at)
completed_tasks = completed_tasks[-limit:]
return completed_tasks
@staticmethod
def tick():
for task in TaskScheduler.pending_tasks():
task.run()

View File

@ -15,5 +15,27 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from eos.core.tasks import *
from eos.core.objects import *
class DirectRunStrategy(RunStrategy):
def run(self, task):
task.status = Task.Status.PROCESSING
task.started_at = DateTimeField.now()
task.save()
try:
task._run()
task.status = Task.Status.COMPLETE
task.completed_at = DateTimeField.now()
task.save()
except Exception as e:
task.status = Task.Status.FAILED
task.completed_at = DateTimeField.now()
if is_python:
#__pragma__('skip')
import traceback
#__pragma__('noskip')
task.messages.append(traceback.format_exc())
else:
task.messages.append(repr(e))
task.save()

View File

@ -18,6 +18,7 @@ from eos.core.bigint import *
from eos.core.objects import *
from eos.core.hashing import *
from eos.core.tasks import *
from eos.core.tasks.direct import *
# Common library things
# ===================

View File

@ -17,6 +17,9 @@
import eos.core.objects
import eos.core.bigint
import eos.core.hashing
import eos.core.tests
import eos.core.tasks
import eos.core.tasks.direct
import eos.base.election
import eos.base.workflow

View File

@ -225,8 +225,6 @@ class InternalMixingTrustee(MixingTrustee):
return True
class PSRElection(Election):
_db_name = Election._name
sk = EmbeddedObjectField(SEGPrivateKey, is_protected=True) # TODO: Threshold
public_key = EmbeddedObjectField(SEGPublicKey)

View File

@ -66,7 +66,7 @@ class TaskDecryptVotes(eos.base.workflow.TaskDecryptVotes):
election = self.recurse_parents('eos.base.election.Election')
for _ in range(len(election.questions)):
election.results.append(EosObject.objects['eos.base.election.RawResult']())
election.results.append(EosObject.lookup('eos.base.election.RawResult')())
for i in range(len(election.mixing_trustees[-1].mixed_questions)):
for encrypted_answer in election.mixing_trustees[-1].mixed_questions[i]:

View File

@ -18,7 +18,9 @@ import click
import flask
from eos.core.objects import *
from eos.core.tasks import *
from eos.base.election import *
from eos.base.workflow import *
from eos.psr.crypto import *
from eos.psr.election import *
from eos.psr.mixnet import *
@ -152,6 +154,12 @@ def verify_election(electionid):
def inject_globals():
return {'eos': eos, 'eosweb': eosweb, 'SHA256': eos.core.hashing.SHA256}
# Tickle the plumbus every request
@app.before_request
def tick_scheduler():
# Process pending tasks
TaskScheduler.tick()
# === Views ===
@app.route('/')
@ -217,12 +225,12 @@ def election_admin_summary(election):
@using_election
@election_admin
def election_admin_enter_task(election):
task = election.workflow.get_task(flask.request.args['task_name'])
if task.status != WorkflowTask.Status.READY:
workflow_task = election.workflow.get_task(flask.request.args['task_name'])
if workflow_task.status != WorkflowTask.Status.READY:
return flask.Response('Task is not yet ready or has already exited', 409)
task.enter()
election.save()
task = WorkflowTaskEntryTask(election_id=election._id, workflow_task=workflow_task._name, status=Task.Status.READY, run_strategy=EosObject.lookup(app.config['TASK_RUN_STRATEGY'])())
task.run()
return flask.redirect(flask.url_for('election_admin_summary', election_id=election._id))

View File

@ -40,6 +40,11 @@
margin-left: -1.75rem;
}
/* Fix display of multiple right-aligned menu items */
.ui.menu:not(.vertical) .right.item ~ .right.item {
margin-left: 0 !important;
}
@media print {
body, html {
/* Default height: 100% causes blank pages */

View File

@ -0,0 +1,42 @@
{#
Eos - Verifiable elections
Copyright © 2017 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
<div class="ui simple dropdown item right" id="active_tasks_menu">
<div class="ui{% if eos.core.tasks.TaskScheduler.active_tasks()|length > 0 %} active{% endif %} mini inline loader" style="margin-right: 1em;"></div>
{{ eos.core.tasks.TaskScheduler.active_tasks()|length }} active tasks
{% if eos.core.tasks.TaskScheduler.pending_tasks()|length > 0 %}
({{ eos.core.tasks.TaskScheduler.pending_tasks()|length }} pending)
{% endif %}
<i class="dropdown icon"></i>
<div class="menu">
<div class="header">Active tasks</div>
{% for task in eos.core.tasks.TaskScheduler.active_tasks() %}
<div class="item">{{ task.label }}</div>
{% endfor %}
<div class="divider"></div>
<div class="header">Pending tasks</div>
{% for task in eos.core.tasks.TaskScheduler.pending_tasks() %}
<div class="item">{{ task.label }}</div>
{% endfor %}
<div class="divider"></div>
<div class="header">Recently completed tasks</div>
{% for task in eos.core.tasks.TaskScheduler.completed_tasks(3) %}
<div class="item">{% if task.status < 0 %}<i class="warning sign icon"></i> {% endif %}{{ task.label }}</div>
{% endfor %}
</div>
</div>

View File

@ -28,6 +28,9 @@
<a href="/" class="header item">Eos Voting for {{ eosweb.app.config['ORG_NAME'] }}</a>
<a href="https://github.com/RunasSudo/Eos" class="item">Source Code</a>
{% if session.user %}
{% if session.user.is_admin() %}
{% include 'active_tasks_menu.html' %}
{% endif %}
<div class="ui simple dropdown item right">
<i class="{% if session.user.is_admin() %}legal{% else %}user circle{% endif %} icon"></i> {{ session.user.name }} <i class="dropdown icon"></i>
<div class="menu">

View File

@ -24,14 +24,8 @@
<ul>
{% for task in election.workflow.tasks %}
{% if task.status == eos.base.workflow.WorkflowTask.Status.READY %}
<li><a href="{{ url_for('election_admin_enter_task', election_id=election._id, task_name=task._name) }}" onclick="return confirmTask(this);">{{ task.label }}</a></li>
<li><a href="{{ url_for('election_admin_enter_task', election_id=election._id, task_name=task._name) }}" onclick="return window.confirm('Are you sure you want to execute the task \'{{ task.label }}\'? This action is irreversible.');">{{ task.label }}</a></li>
{% endif %}
{% endfor %}
</ul>
<script>
function confirmTask(taskLink) {
return window.confirm("Are you sure you want to execute the task \"" + taskLink.innerText + "\"? This action is irreversible.");
}
</script>
{% endblock %}

View File

@ -14,6 +14,8 @@ ADMINS = [
#eos.redditauth.election.RedditUser(username='xxxxxxxx')
]
TASK_RUN_STRATEGY = 'eos.core.tasks.direct.DirectRunStrategy'
# MongoDB
DB_TYPE = 'mongodb'