working prototype

Now there is a lot of TODO items remaining...
This commit is contained in:
Régis Behmo 2024-12-05 15:23:46 +01:00
parent 35554c2cc2
commit 34028c0e7c
2 changed files with 96 additions and 36 deletions

View File

@ -1,19 +1,22 @@
import asyncio import asyncio
from contextlib import contextmanager
import os import os
import shlex import shlex
import subprocess
import threading
# import sys
# import time
import typing as t import typing as t
import aiofiles import aiofiles
# from contextlib import contextmanager
import tutor.env import tutor.env
from quart import Quart, render_template, request, websocket, redirect, url_for from quart import Quart, render_template, request, websocket, redirect, url_for
from tutor import hooks from tutor.exceptions import TutorError
from tutor import fmt, hooks
from tutor.types import Config from tutor.types import Config
import tutor.utils
from tutor.commands.cli import cli
class TutorProject: class TutorProject:
@ -55,13 +58,6 @@ app = Quart(
def run(root: str, **app_kwargs: t.Any) -> None: def run(root: str, **app_kwargs: t.Any) -> None:
# import module to trigger all the right imports and hooks
# TODO how do we handle this now that we call the tutor binary directly? Should we
# even deal with hooks at all? We have to to figure out plugin configuration,
# information, etc.
# pylint: disable=unused-import,import-outside-toplevel
from tutor.commands.cli import cli
hooks.Actions.CORE_READY.do() # discover plugins hooks.Actions.CORE_READY.do() # discover plugins
hooks.Actions.PROJECT_ROOT_READY.do(root) hooks.Actions.PROJECT_ROOT_READY.do(root)
app.logger.info("Dash tutor logs location: %s", TutorProject.tutor_stdout_path()) app.logger.info("Dash tutor logs location: %s", TutorProject.tutor_stdout_path())
@ -75,6 +71,7 @@ async def home():
@app.get("/sidebar/plugins") @app.get("/sidebar/plugins")
async def sidebar_plugins(): async def sidebar_plugins():
# TODO get rid of this view and render from home()
return await render_template( return await render_template(
"sidebar/_plugins.html", "sidebar/_plugins.html",
installed_plugins=sorted(set(hooks.Filters.PLUGINS_INSTALLED.iterate())), installed_plugins=sorted(set(hooks.Filters.PLUGINS_INSTALLED.iterate())),
@ -104,35 +101,97 @@ async def toggle_plugin(name: str):
@app.post("/tutor/cli") @app.post("/tutor/cli")
async def tutor_cli(): async def tutor_cli():
# Run command asynchronously # Run command asynchronously
# TODO return 400 if thread is active
# TODO parse command from JSON request body # TODO parse command from JSON request body
# app.add_background_task(subprocess_exec, ["tutor", "config", "printvalue", "DOCKER_IMAGE_OPENEDX"]) thread = threading.Thread(
# app.add_background_task(subprocess_exec, ["tutor" "local", "launch"]) target=run_tutor_cli,
# await subprocess_exec(["tutor", "config", "printvalue", "pouac"]) # args=[["dev", "dc", "run", "pouac"]],
await subprocess_exec(["tutor", "dev", "dc", "run", "pouac"]) # args=[["config", "printvalue", "DOCKER_IMAGE_OPENEDX"]],
# args=[["config", "printvalue", "POUAC"]],
args=[["local", "launch", "--non-interactive"]],
)
thread.start()
return redirect(url_for("tutor_logs")) return redirect(url_for("tutor_logs"))
async def subprocess_exec(command: list[str]): def run_tutor_cli(args: list[str]) -> None:
path = TutorProject.tutor_stdout_path() """
# if os.path.exists(path): Execute some arbitrary tutor command. Capture the output in a dedicated file.
# # TODO return 400? We can't run two commands at the same time
# return {} TODO Return the exit code?
with open(path, "w", encoding="utf8") as stdout: TODO Refactor this
# Print command """
# TODO this doesn't seem to work. For some reason, the command is added at the with open(TutorProject.tutor_stdout_path(), "w", encoding="utf8") as stdout:
# bottom of the file!!! # useless because overwritten by Popen
stdout.write(f"$ {shlex.join(command)}\n") stdout.write(f"$ tutor {shlex.join(args)}\n")
# Run command
proc = await asyncio.create_subprocess_exec( # Override execute function
*command, with patch_objects(
stdout=stdout, [
stderr=stdout, (tutor.utils, "execute", execute),
stdin=asyncio.subprocess.DEVNULL, (fmt.click, "echo", click_echo),
) (fmt.click, "style", click_style),
while proc.returncode is None: ]
await proc.communicate() ):
await asyncio.sleep(0.1) try:
return {} # Call tutor command
cli(args)
except TutorError as e:
with open(TutorProject.tutor_stdout_path(), "a", encoding="utf8") as stdout:
stdout.write(e.args[0])
except SystemExit as e:
# TODO what to do with e.code?
pass
@contextmanager
def patch_objects(refs):
old_objects = []
for module, object_name, new_object in refs:
# backup old object
old_objects.append((module, object_name, getattr(module, object_name)))
# override object
setattr(module, object_name, new_object)
try:
yield
finally:
# restore old objects
for module, object_name, old_object in old_objects:
setattr(module, object_name, old_object)
def click_echo(text, **kwargs):
with open(TutorProject.tutor_stdout_path(), "a", encoding="utf8") as stdout:
stdout.write(text)
stdout.write("\n")
def click_style(text, **kwargs):
"""
Strip ANSI colors
TODO convert to HTML color codes?
"""
return text
def execute(*command: str) -> int:
"""
TODO refactor this
"""
with open(TutorProject.tutor_stdout_path(), "ab") as stdout:
with subprocess.Popen(command, stdout=stdout, stderr=stdout) as p:
try:
result = p.wait(timeout=None)
except Exception as e:
p.kill()
p.wait()
raise TutorError(f"Command failed: {' '.join(command)}") from e
if result > 0:
raise TutorError(
f"Command failed with status {result}: {' '.join(command)}"
)
return result
@app.get("/tutor/logs") @app.get("/tutor/logs")

View File

@ -55,6 +55,7 @@ body {
.workspace { .workspace {
flex: 1; flex: 1;
padding: 20px; padding: 20px;
overflow: scroll;
.header { .header {
margin-bottom: 16px; margin-bottom: 16px;