2024-08-28 10:39:20 +02:00
|
|
|
import logging
|
|
|
|
import os
|
2024-08-28 22:20:36 +02:00
|
|
|
import shutil
|
2024-08-28 10:39:20 +02:00
|
|
|
import subprocess
|
2024-08-29 12:03:37 +02:00
|
|
|
import time
|
2024-08-28 10:39:20 +02:00
|
|
|
|
2024-08-28 20:05:46 +02:00
|
|
|
import psutil
|
|
|
|
|
2024-08-28 22:20:36 +02:00
|
|
|
from core import utils, config
|
|
|
|
from core.vars import logger, PYTHON_EXEC
|
2024-08-29 12:03:37 +02:00
|
|
|
from ui import choices
|
|
|
|
|
|
|
|
|
|
|
|
def find_correct_pid(parent_pid: int) -> int:
|
|
|
|
processes: list[psutil.Process] = [psutil.Process(parent_pid)]
|
|
|
|
create_time = processes[0].create_time()
|
|
|
|
|
|
|
|
time.sleep(0.5) # Wait for child processes to spawn
|
|
|
|
|
|
|
|
for i in range(1, 10):
|
|
|
|
if psutil.pid_exists(processes[0].pid + i):
|
|
|
|
child_process = psutil.Process(processes[0].pid + i)
|
|
|
|
if child_process.create_time() - create_time < 1:
|
|
|
|
processes.append(psutil.Process(processes[0].pid + i))
|
|
|
|
else:
|
|
|
|
time.sleep(0.5 / i)
|
|
|
|
|
|
|
|
return processes[-1].pid
|
2024-08-28 10:39:20 +02:00
|
|
|
|
|
|
|
|
|
|
|
class Stack:
|
2024-08-28 22:20:36 +02:00
|
|
|
def __init__(self, name: str, id: str, port: int, url: str):
|
2024-08-28 10:39:20 +02:00
|
|
|
self.name = name
|
2024-08-28 22:20:36 +02:00
|
|
|
self.id = id
|
|
|
|
self.path = os.path.join(os.path.expanduser("~"), ".ai-suite-rocm", id)
|
2024-08-28 20:05:46 +02:00
|
|
|
|
2024-08-28 10:39:20 +02:00
|
|
|
self.url = url
|
|
|
|
self.port = port
|
|
|
|
|
2024-08-29 12:03:37 +02:00
|
|
|
self.pid = config.get(f"{self.name}-pid")
|
2024-08-28 10:39:20 +02:00
|
|
|
|
2024-08-29 12:26:37 +02:00
|
|
|
|
2024-08-28 10:39:20 +02:00
|
|
|
def install(self):
|
2024-08-29 12:50:18 +02:00
|
|
|
if self.is_installed():
|
|
|
|
self.update()
|
|
|
|
else:
|
|
|
|
self.check_for_broken_install()
|
|
|
|
self.create_venv()
|
|
|
|
self._install()
|
2024-08-29 12:26:37 +02:00
|
|
|
|
2024-08-29 12:50:18 +02:00
|
|
|
self.create_file('.installed', 'true')
|
|
|
|
logger.info(f"Installed {self.name}")
|
2024-08-28 10:39:20 +02:00
|
|
|
|
2024-08-29 12:26:37 +02:00
|
|
|
def _install(self):
|
|
|
|
pass
|
|
|
|
|
2024-08-28 10:39:20 +02:00
|
|
|
def is_installed(self):
|
|
|
|
return self.file_exists('.installed')
|
|
|
|
|
|
|
|
def check_for_broken_install(self):
|
2024-08-28 20:05:46 +02:00
|
|
|
if not self.is_installed():
|
|
|
|
if os.path.exists(self.path):
|
|
|
|
if len(os.listdir(self.path)) > 0:
|
|
|
|
logger.warning("Found files from a previous/borked/crashed installation, cleaning up...")
|
|
|
|
self.bash(f"rm -rf {self.path}")
|
|
|
|
self.create_dir('')
|
|
|
|
else:
|
|
|
|
self.create_dir('')
|
2024-08-28 10:39:20 +02:00
|
|
|
|
|
|
|
def update(self, folder: str = 'webui'):
|
2024-08-29 12:26:37 +02:00
|
|
|
if self.is_installed():
|
2024-08-29 12:50:18 +02:00
|
|
|
status = self.status()
|
|
|
|
if status:
|
|
|
|
self.stop()
|
|
|
|
|
2024-08-28 10:39:20 +02:00
|
|
|
logger.info(f"Updating {self.name}")
|
|
|
|
self.git_pull(folder)
|
2024-08-29 12:50:18 +02:00
|
|
|
self._update()
|
|
|
|
|
|
|
|
if status:
|
|
|
|
self.start()
|
2024-08-28 10:39:20 +02:00
|
|
|
else:
|
2024-08-29 12:26:37 +02:00
|
|
|
logger.warning(f"Could not update {self.name} as {self.name} is not installed")
|
2024-08-28 10:39:20 +02:00
|
|
|
|
2024-08-29 12:50:18 +02:00
|
|
|
def _update(self):
|
|
|
|
pass
|
|
|
|
|
2024-08-28 10:39:20 +02:00
|
|
|
def uninstall(self):
|
2024-08-29 12:26:37 +02:00
|
|
|
logger.info(f"Uninstalling {self.name}")
|
2024-08-29 12:50:18 +02:00
|
|
|
if self.status():
|
|
|
|
self.stop()
|
2024-08-28 10:39:20 +02:00
|
|
|
self.bash(f"rm -rf {self.path}")
|
|
|
|
|
|
|
|
def start(self):
|
2024-08-29 12:50:18 +02:00
|
|
|
if self.status():
|
|
|
|
logger.warning(f"{self.name} is already running")
|
|
|
|
|
2024-08-28 20:05:46 +02:00
|
|
|
if self.is_installed():
|
2024-08-29 12:26:37 +02:00
|
|
|
self._start()
|
2024-08-28 20:05:46 +02:00
|
|
|
else:
|
2024-08-29 12:26:37 +02:00
|
|
|
logger.error(f"{self.name} is not installed")
|
2024-08-28 20:05:46 +02:00
|
|
|
|
2024-08-29 12:26:37 +02:00
|
|
|
def _start(self):
|
2024-08-28 10:39:20 +02:00
|
|
|
pass
|
|
|
|
|
|
|
|
def stop(self):
|
2024-08-29 12:03:37 +02:00
|
|
|
if self.status():
|
|
|
|
logger.debug(f"Killing {self.name} with PID: {self.pid}")
|
|
|
|
psutil.Process(self.pid).kill()
|
2024-08-29 12:50:18 +02:00
|
|
|
else:
|
|
|
|
logger.warning(f"{self.name} is not running")
|
2024-08-29 12:03:37 +02:00
|
|
|
|
|
|
|
self.set_pid(None)
|
|
|
|
|
|
|
|
def set_pid(self, pid):
|
|
|
|
self.pid = pid
|
|
|
|
if pid is not None:
|
|
|
|
config.put(f"{self.name}-pid", pid)
|
|
|
|
else:
|
|
|
|
config.remove(f"{self.name}-pid")
|
2024-08-28 10:39:20 +02:00
|
|
|
|
|
|
|
def restart(self):
|
|
|
|
self.stop()
|
|
|
|
self.start()
|
|
|
|
|
2024-08-28 20:05:46 +02:00
|
|
|
def status(self) -> bool:
|
2024-08-29 12:03:37 +02:00
|
|
|
if self.pid is None:
|
|
|
|
return False
|
|
|
|
|
|
|
|
return psutil.pid_exists(self.pid)
|
2024-08-28 10:39:20 +02:00
|
|
|
|
|
|
|
# Python/Bash utils
|
|
|
|
def create_venv(self):
|
|
|
|
venv_path = os.path.join(self.path, 'venv')
|
|
|
|
if not self.has_venv():
|
2024-08-28 20:05:46 +02:00
|
|
|
logger.info(f"Creating venv for {self.name}")
|
2024-08-28 10:39:20 +02:00
|
|
|
self.bash(f"{PYTHON_EXEC} -m venv {venv_path} --system-site-packages")
|
2024-08-28 20:05:46 +02:00
|
|
|
self.pip_install("pip")
|
2024-08-28 10:39:20 +02:00
|
|
|
else:
|
|
|
|
logger.debug(f"Venv already exists for {self.name}")
|
|
|
|
|
|
|
|
def has_venv(self) -> bool:
|
|
|
|
return self.dir_exists('venv')
|
|
|
|
|
2024-08-28 20:05:46 +02:00
|
|
|
def pip_install(self, package: str | list, no_deps: bool = False, env=[], args=[]):
|
|
|
|
if no_deps:
|
|
|
|
args.append("--no-deps")
|
|
|
|
|
2024-08-28 10:39:20 +02:00
|
|
|
if isinstance(package, list):
|
|
|
|
for p in package:
|
2024-08-28 20:05:46 +02:00
|
|
|
logger.info(f"Installing {p}")
|
|
|
|
self.pip(f"install -U {p}", env=env, args=args)
|
2024-08-28 10:39:20 +02:00
|
|
|
else:
|
2024-08-28 20:05:46 +02:00
|
|
|
logger.info(f"Installing {package}")
|
|
|
|
self.pip(f"install -U {package}", env=env, args=args)
|
2024-08-28 10:39:20 +02:00
|
|
|
|
2024-08-28 20:05:46 +02:00
|
|
|
def install_requirements(self, filename: str = 'requirements.txt', env=[]):
|
|
|
|
logger.info(f"Installing requirements for {self.name} ({filename})")
|
|
|
|
self.pip(f"install -r {filename}", env=env)
|
2024-08-28 10:39:20 +02:00
|
|
|
|
2024-08-28 20:05:46 +02:00
|
|
|
def pip(self, cmd: str, env=[], args=[], current_dir: str = None):
|
2024-08-29 12:03:37 +02:00
|
|
|
self.python(f"-m pip {cmd}", env=env, args=args, current_dir=current_dir)
|
2024-08-28 10:39:20 +02:00
|
|
|
|
2024-08-29 12:03:37 +02:00
|
|
|
def python(self, cmd: str, env=[], args=[], current_dir: str = None, daemon: bool = False):
|
|
|
|
self.bash(f"{' '.join(env)} {self.path}/venv/bin/python {cmd} {' '.join(args)}", current_dir, daemon)
|
2024-08-28 10:39:20 +02:00
|
|
|
|
2024-08-28 20:05:46 +02:00
|
|
|
def bash(self, cmd: str, current_dir: str = None, daemon: bool = False):
|
2024-08-28 10:39:20 +02:00
|
|
|
cmd = f"cd {self.path if current_dir is None else os.path.join(self.path, current_dir)} && {cmd}"
|
|
|
|
|
2024-08-28 20:05:46 +02:00
|
|
|
if daemon:
|
2024-08-29 12:03:37 +02:00
|
|
|
if self.status():
|
|
|
|
choice = choices.already_running.ask()
|
|
|
|
|
|
|
|
if choice is True:
|
|
|
|
self.stop()
|
2024-08-29 12:26:37 +02:00
|
|
|
self._start()
|
2024-08-29 12:03:37 +02:00
|
|
|
return
|
2024-08-28 20:05:46 +02:00
|
|
|
else:
|
2024-08-29 12:03:37 +02:00
|
|
|
# TODO: attach to subprocess / redirect logs?
|
|
|
|
return
|
2024-08-28 20:05:46 +02:00
|
|
|
else:
|
2024-08-29 12:03:37 +02:00
|
|
|
logger.debug(f"Running command as daemon: {cmd}")
|
|
|
|
cmd = f"{cmd} &"
|
|
|
|
process = subprocess.Popen(cmd, shell=True, preexec_fn=os.setpgrp,
|
|
|
|
stdout=config.open_file(f"{self.id}-stdout"),
|
|
|
|
stderr=config.open_file(f"{self.id}-stderr"))
|
|
|
|
self.set_pid(find_correct_pid(process.pid))
|
|
|
|
return
|
2024-08-28 10:39:20 +02:00
|
|
|
else:
|
2024-08-28 20:05:46 +02:00
|
|
|
logger.debug(f"Running command: {cmd}")
|
|
|
|
|
|
|
|
if logger.level == logging.DEBUG:
|
|
|
|
process = subprocess.Popen(cmd, shell=True)
|
|
|
|
process.wait()
|
|
|
|
if process.returncode != 0:
|
|
|
|
raise Exception(f"Failed to run command: {cmd}")
|
|
|
|
else:
|
|
|
|
process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
|
|
out, err = process.communicate()
|
|
|
|
|
|
|
|
if process.returncode != 0:
|
|
|
|
logger.fatal(f"Failed to run command: {cmd}")
|
|
|
|
logger.fatal(f"Error: {err.decode('utf-8')}")
|
|
|
|
logger.fatal(f"Output: {out.decode('utf-8')}")
|
|
|
|
raise Exception(f"Failed to run command: {cmd}")
|
2024-08-28 10:39:20 +02:00
|
|
|
|
|
|
|
# Git utils
|
|
|
|
def git_clone(self, url: str, branch: str = None, dest: str = None):
|
2024-08-28 20:05:46 +02:00
|
|
|
logger.info(f"Cloning {url}")
|
2024-08-28 10:39:20 +02:00
|
|
|
self.bash(f"git clone {f"-b {branch}" if branch is not None else ''} {url} {'' if dest is None else dest}")
|
|
|
|
|
|
|
|
def git_pull(self, repo_folder: str, force: bool = False):
|
|
|
|
self.bash(f"git reset --hard HEAD {'&& git clean -f -d' if force else ''} && git pull", repo_folder)
|
|
|
|
|
|
|
|
# Prebuilt utils
|
|
|
|
def install_from_prebuilt(self, name):
|
|
|
|
for prebuilt in utils.get_prebuilts():
|
|
|
|
if prebuilt['name'].split("-")[0] == name:
|
|
|
|
self.pip(f"install {prebuilt['browser_download_url']}")
|
|
|
|
return
|
|
|
|
|
|
|
|
# File utils
|
|
|
|
def create_file(self, name, content):
|
|
|
|
with open(os.path.join(self.path, name), 'w') as f:
|
|
|
|
f.write(content)
|
|
|
|
|
|
|
|
def create_dir(self, name):
|
2024-08-28 20:05:46 +02:00
|
|
|
if name == '':
|
|
|
|
logger.info(f"Creating directory for {self.name}")
|
|
|
|
|
2024-08-28 10:39:20 +02:00
|
|
|
logger.debug(f"Creating directory {name}")
|
|
|
|
os.makedirs(os.path.join(self.path, name), exist_ok=True)
|
|
|
|
|
|
|
|
def remove_file(self, name):
|
|
|
|
logger.debug(f"Removing file {name}")
|
|
|
|
os.remove(os.path.join(self.path, name))
|
|
|
|
|
|
|
|
def remove_dir(self, name):
|
|
|
|
logger.debug(f"Removing directory {name}")
|
2024-08-28 22:20:36 +02:00
|
|
|
shutil.rmtree(os.path.join(self.path, name))
|
2024-08-28 10:39:20 +02:00
|
|
|
|
|
|
|
def move_file_or_dir(self, src, dest):
|
|
|
|
logger.debug(f"Moving file/dir {src} to {dest}")
|
|
|
|
os.rename(os.path.join(self.path, src), os.path.join(self.path, dest))
|
|
|
|
|
|
|
|
def move_all_files_in_dir(self, src, dest):
|
|
|
|
logger.debug(f"Moving all files in directory {src} to {dest}")
|
|
|
|
for file in os.listdir(os.path.join(self.path, src)):
|
|
|
|
os.rename(os.path.join(self.path, src, file), os.path.join(self.path, dest, file))
|
|
|
|
|
|
|
|
def file_exists(self, name):
|
|
|
|
return os.path.exists(os.path.join(self.path, name))
|
|
|
|
|
|
|
|
def dir_exists(self, name):
|
|
|
|
return os.path.exists(os.path.join(self.path, name))
|
|
|
|
|
|
|
|
def remove_line_in_file(self, contains: str | list, file: str):
|
|
|
|
logger.debug(f"Removing lines containing {contains} in {file}")
|
|
|
|
|
|
|
|
if isinstance(contains, list):
|
|
|
|
for c in contains:
|
|
|
|
self.bash(f"sed -i '/{c}/d' {file}")
|
|
|
|
else:
|
|
|
|
self.bash(f"sed -i '/{contains}/d' {file}")
|
|
|
|
|
|
|
|
def replace_line_in_file(self, match: str, replace: str, file: str):
|
|
|
|
logger.debug(f"Replacing lines containing {match} with {replace} in {file}")
|
|
|
|
self.bash(f"sed -i 's/{match}/{replace}/g' {file}")
|