mirror of
https://github.com/ChristianLight/tutor.git
synced 2024-05-29 20:30:48 +00:00
0a670d7ead
Annotations were generated with pyannotate: https://github.com/dropbox/pyannotate We are running in strict mode, which is awesome! This affects a large part of the code base, which might be an issue for people running a fork of Tutor. Nonetheless, the behavior should not be affected. If anything, this process has helped find and resolve a few type-related bugs. Thus, this is not considered as a breaking change.
155 lines
4.5 KiB
Python
155 lines
4.5 KiB
Python
import io
|
|
import os
|
|
import platform
|
|
import subprocess
|
|
import sys
|
|
import tarfile
|
|
from typing import Any, Dict
|
|
from urllib.request import urlopen
|
|
|
|
import click
|
|
|
|
# Note: it is important that this module does not depend on config, such that
|
|
# the web ui can be launched even where there is no configuration.
|
|
from .. import fmt
|
|
from .. import env as tutor_env
|
|
from .. import serialize
|
|
from .context import Context
|
|
|
|
|
|
@click.group(
|
|
short_help="Web user interface", help="""Run Tutor commands from a web terminal"""
|
|
)
|
|
def webui() -> None:
|
|
pass
|
|
|
|
|
|
@click.command(help="Start the web UI")
|
|
@click.option(
|
|
"-p",
|
|
"--port",
|
|
default=3737,
|
|
type=int,
|
|
show_default=True,
|
|
help="Port number to listen",
|
|
)
|
|
@click.option(
|
|
"-h", "--host", default="0.0.0.0", show_default=True, help="Host address to listen"
|
|
)
|
|
@click.pass_obj
|
|
def start(context: Context, port: int, host: str) -> None:
|
|
check_gotty_binary(context.root)
|
|
fmt.echo_info("Access the Tutor web UI at http://{}:{}".format(host, port))
|
|
while True:
|
|
config = load_config(context.root)
|
|
user = config["user"]
|
|
password = config["password"]
|
|
command = [
|
|
gotty_path(context.root),
|
|
"--permit-write",
|
|
"--address",
|
|
host,
|
|
"--port",
|
|
str(port),
|
|
"--title-format",
|
|
"Tutor web UI - {{ .Command }} ({{ .Hostname }})",
|
|
]
|
|
if user and password:
|
|
credential = "{}:{}".format(user, password)
|
|
command += ["--credential", credential]
|
|
else:
|
|
fmt.echo_alert(
|
|
"Running web UI without user authentication. Run 'tutor webui configure' to setup authentication"
|
|
)
|
|
command += [sys.argv[0], "ui"]
|
|
p = subprocess.Popen(command)
|
|
while True:
|
|
try:
|
|
p.wait(timeout=2)
|
|
except subprocess.TimeoutExpired:
|
|
new_config = load_config(context.root)
|
|
if new_config != config:
|
|
click.echo(
|
|
"WARNING configuration changed. Tutor web UI is now going to restart. Reload this page to continue."
|
|
)
|
|
p.kill()
|
|
p.wait()
|
|
break
|
|
|
|
|
|
@click.command(help="Configure authentication")
|
|
@click.option("-u", "--user", prompt="User name", help="Authentication user name")
|
|
@click.option(
|
|
"-p",
|
|
"--password",
|
|
prompt=True,
|
|
hide_input=True,
|
|
confirmation_prompt=True,
|
|
help="Authentication password",
|
|
)
|
|
@click.pass_obj
|
|
def configure(context: Context, user: str, password: str) -> None:
|
|
save_webui_config_file(context.root, {"user": user, "password": password})
|
|
fmt.echo_info(
|
|
"The web UI configuration has been updated. "
|
|
"If at any point you wish to reset your username and password, "
|
|
"just delete the following file:\n\n {}".format(config_path(context.root))
|
|
)
|
|
|
|
|
|
def check_gotty_binary(root: str) -> None:
|
|
path = gotty_path(root)
|
|
if os.path.exists(path):
|
|
return
|
|
fmt.echo_info("Downloading gotty to {}...".format(path))
|
|
|
|
# Generate release url
|
|
# Note: I don't know how to handle arm
|
|
architecture = "amd64" if platform.architecture()[0] == "64bit" else "386"
|
|
url = "https://github.com/yudai/gotty/releases/download/v1.0.1/gotty_{system}_{architecture}.tar.gz".format(
|
|
system=platform.system().lower(), architecture=architecture
|
|
)
|
|
|
|
# Download
|
|
response = urlopen(url)
|
|
|
|
# Decompress
|
|
dirname = os.path.dirname(path)
|
|
if not os.path.exists(dirname):
|
|
os.makedirs(dirname)
|
|
compressed = tarfile.open(fileobj=io.BytesIO(response.read()))
|
|
compressed.extract("./gotty", dirname)
|
|
|
|
|
|
def load_config(root: str) -> Dict[str, Any]:
|
|
path = config_path(root)
|
|
if not os.path.exists(path):
|
|
save_webui_config_file(root, {"user": None, "password": None})
|
|
with open(config_path(root)) as f:
|
|
return serialize.load(f)
|
|
|
|
|
|
def save_webui_config_file(root: str, config: Dict[str, Any]) -> None:
|
|
path = config_path(root)
|
|
directory = os.path.dirname(path)
|
|
if not os.path.exists(directory):
|
|
os.makedirs(directory)
|
|
with open(path, "w") as of:
|
|
serialize.dump(config, of)
|
|
|
|
|
|
def gotty_path(root: str) -> str:
|
|
return get_path(root, "gotty")
|
|
|
|
|
|
def config_path(root: str) -> str:
|
|
return get_path(root, "config.yml")
|
|
|
|
|
|
def get_path(root: str, filename: str) -> str:
|
|
return tutor_env.pathjoin(root, "webui", filename)
|
|
|
|
|
|
webui.add_command(start)
|
|
webui.add_command(configure)
|