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 content %} |
||||
<pre>{{ output }}</pre> |
||||
<a href="{{ next }}" class="button">OK</a> |
||||
<pre id="log" data-validation-button-id="next_button">Chargement...</pre> |
||||
<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 %} |
||||
|
@ -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