Youen
2 years ago
12 changed files with 205 additions and 42 deletions
@ -0,0 +1,20 @@ |
|||||||
|
import time |
||||||
|
from flask import Blueprint, request |
||||||
|
from web_utils.get_arg import get_arg |
||||||
|
from web_utils.task import Task |
||||||
|
|
||||||
|
bp = Blueprint('api_task', __name__, url_prefix='/api/task') |
||||||
|
|
||||||
|
@bp.route('/log') |
||||||
|
def log(): |
||||||
|
task_id = get_arg('task_id') |
||||||
|
from_char = int(get_arg('from', '0')) |
||||||
|
|
||||||
|
task = Task.get(task_id) |
||||||
|
if task == None: |
||||||
|
return { "task_finished": True, "new_output": "" } |
||||||
|
|
||||||
|
while task.is_alive() and len(task.get_output_str()) <= from_char: |
||||||
|
time.sleep(100) |
||||||
|
|
||||||
|
return { "task_finished": not task.is_alive(), "new_output": task.get_output_str()[from_char:-1] } |
@ -0,0 +1,34 @@ |
|||||||
|
function log_stream(element, logUrl) { |
||||||
|
element.innerText = ""; |
||||||
|
|
||||||
|
function fetchMore() { |
||||||
|
fetch(logUrl + "&from=" + element.innerText.length) |
||||||
|
.then(response => response.text()) |
||||||
|
.then(jsonStr => { |
||||||
|
let json = JSON.parse(jsonStr); |
||||||
|
|
||||||
|
element.innerText += json.new_output; |
||||||
|
|
||||||
|
const scrollOffset = 60; |
||||||
|
const bodyTop = document.body.getBoundingClientRect().top; |
||||||
|
const viewportHeight = window.innerHeight; |
||||||
|
const elementBottom = element.getBoundingClientRect().bottom; |
||||||
|
|
||||||
|
window.scrollTo({ |
||||||
|
top: elementBottom - viewportHeight - bodyTop + scrollOffset |
||||||
|
}); |
||||||
|
|
||||||
|
if(json.task_finished) { |
||||||
|
let validationButtonId = element.getAttribute('data-validation-button-id'); |
||||||
|
if(validationButtonId) { |
||||||
|
document.getElementById(validationButtonId).classList.remove('disabled', false); |
||||||
|
} |
||||||
|
} |
||||||
|
else { |
||||||
|
setTimeout(fetchMore, 100); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
fetchMore(); |
||||||
|
} |
@ -0,0 +1,23 @@ |
|||||||
|
.button { |
||||||
|
display: inline-block; |
||||||
|
border: 1px solid gray; |
||||||
|
border-radius: 4px; |
||||||
|
padding: 3px 10px; |
||||||
|
margin: 2px; |
||||||
|
color: black; |
||||||
|
} |
||||||
|
|
||||||
|
a.button { |
||||||
|
text-decoration: none; |
||||||
|
} |
||||||
|
|
||||||
|
.button.danger { |
||||||
|
color: darkred; |
||||||
|
border-color: darkred; |
||||||
|
} |
||||||
|
|
||||||
|
.button.disabled { |
||||||
|
color: lightgray; |
||||||
|
border-color: lightgray; |
||||||
|
cursor: default; |
||||||
|
} |
@ -1,8 +1,13 @@ |
|||||||
{% extends 'base.html' %} |
{% extends 'base.html' %} |
||||||
|
|
||||||
{% block title %}Exécution...{% endblock %} |
{% block title %}Exécution...{% endblock %} |
||||||
|
|
||||||
{% block content %} |
{% block content %} |
||||||
<pre>{{ output }}</pre> |
<pre id="log" data-validation-button-id="next_button">Chargement...</pre> |
||||||
<a href="{{ next }}" class="button">OK</a> |
<a id="next_button" href="{{ next }}" class="button disabled">OK</a> |
||||||
|
|
||||||
|
<script type="text/javascript" src="{{ url_for('static', filename='log-stream.js') }}"></script> |
||||||
|
<script type="text/javascript"> |
||||||
|
log_stream(document.getElementById('log'), '{{ url_for('api_task.log', task_id=task.task_id) }}'); |
||||||
|
</script> |
||||||
{% endblock %} |
{% endblock %} |
||||||
|
@ -0,0 +1,64 @@ |
|||||||
|
from threading import Thread, Lock |
||||||
|
from io import StringIO |
||||||
|
from subprocess import Popen, PIPE, STDOUT, DEVNULL |
||||||
|
import uuid |
||||||
|
|
||||||
|
tasks = {} |
||||||
|
|
||||||
|
class Task(Thread): |
||||||
|
def __init__(self): |
||||||
|
Thread.__init__(self) |
||||||
|
self.task_id = uuid.uuid4() |
||||||
|
self.__mutex = Lock() |
||||||
|
self.__output = StringIO() |
||||||
|
tasks[str(self.task_id)] = self |
||||||
|
|
||||||
|
def print(self, str): |
||||||
|
self.__mutex.acquire() |
||||||
|
self.__output.write(str) |
||||||
|
self.__mutex.release() |
||||||
|
|
||||||
|
def get_output_str(self): |
||||||
|
self.__mutex.acquire() |
||||||
|
result = self.__output.getvalue() |
||||||
|
self.__mutex.release() |
||||||
|
return result |
||||||
|
|
||||||
|
@staticmethod |
||||||
|
def get(task_id): |
||||||
|
return tasks[task_id] |
||||||
|
|
||||||
|
class ProcessTask(Task): |
||||||
|
def __init__(self, commands, cwd = None, hide_passwords = []): |
||||||
|
Task.__init__(self) |
||||||
|
self.__commands = commands |
||||||
|
self.__cwd = cwd |
||||||
|
self.__hide_passwords = hide_passwords |
||||||
|
|
||||||
|
def on_fail(self, callback): |
||||||
|
self.__fail_callback = callback |
||||||
|
|
||||||
|
def run(self): |
||||||
|
is_first_command = True |
||||||
|
for command in self.__commands: |
||||||
|
command_display = ' '.join(command) |
||||||
|
for password in self.__hide_passwords: |
||||||
|
command_display = command_display.replace(password, '******') |
||||||
|
|
||||||
|
if not is_first_command: |
||||||
|
self.print('\n') |
||||||
|
is_first_command = False |
||||||
|
self.print('> ' + command_display + '\n') |
||||||
|
|
||||||
|
# We use setsid to make a non-interactive session, otherwise any command that expects an input (confirmation, password, etc.) would hang forever |
||||||
|
self.__process = Popen(['setsid'] + command, cwd = self.__cwd, stdin=DEVNULL, stdout = PIPE, stderr = STDOUT, shell = False) |
||||||
|
|
||||||
|
for line in self.__process.stdout: |
||||||
|
self.print(line.decode()) |
||||||
|
|
||||||
|
self.__process.wait() |
||||||
|
|
||||||
|
if self.__process.returncode != 0: |
||||||
|
if self.__fail_callback: |
||||||
|
self.__fail_callback() |
||||||
|
raise Exception("Command failed ("+str(self.__process.returncode)+")\n"+self.get_output_str()) |
Loading…
Reference in new issue