2024-10-12 14:46:41 +02:00
|
|
|
import os
|
|
|
|
import subprocess
|
|
|
|
import psutil
|
|
|
|
import signal
|
|
|
|
import re
|
|
|
|
import json
|
|
|
|
import git
|
|
|
|
import requests
|
|
|
|
import traceback
|
|
|
|
from tqdm import tqdm
|
|
|
|
import xml.etree.ElementTree as ET
|
|
|
|
import time
|
2024-10-21 11:03:33 +02:00
|
|
|
import shutil
|
2024-10-12 14:46:41 +02:00
|
|
|
|
|
|
|
INSTALL_STATUS_FILE = '/tmp/install_status.json'
|
|
|
|
|
|
|
|
def is_process_running(pid):
|
|
|
|
try:
|
|
|
|
process = psutil.Process(pid)
|
|
|
|
return process.is_running() and process.status() != psutil.STATUS_ZOMBIE
|
|
|
|
except psutil.NoSuchProcess:
|
|
|
|
return False
|
|
|
|
|
|
|
|
def run_app(app_name, command, running_processes):
|
|
|
|
process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, preexec_fn=os.setsid)
|
|
|
|
running_processes[app_name] = {
|
|
|
|
'process': process,
|
|
|
|
'pid': process.pid,
|
|
|
|
'log': [],
|
|
|
|
'status': 'running'
|
|
|
|
}
|
|
|
|
|
|
|
|
for line in process.stdout:
|
|
|
|
running_processes[app_name]['log'].append(line.strip())
|
|
|
|
if len(running_processes[app_name]['log']) > 1000:
|
|
|
|
running_processes[app_name]['log'] = running_processes[app_name]['log'][-1000:]
|
|
|
|
|
|
|
|
running_processes[app_name]['status'] = 'stopped'
|
|
|
|
|
|
|
|
def update_process_status(app_name, running_processes):
|
|
|
|
if app_name in running_processes:
|
|
|
|
if is_process_running(running_processes[app_name]['pid']):
|
|
|
|
running_processes[app_name]['status'] = 'running'
|
|
|
|
else:
|
|
|
|
running_processes[app_name]['status'] = 'stopped'
|
|
|
|
|
|
|
|
def check_app_directories(app_name, app_configs):
|
|
|
|
app_config = app_configs.get(app_name)
|
|
|
|
if not app_config:
|
|
|
|
return False, f"App '{app_name}' not found in configurations."
|
|
|
|
|
|
|
|
venv_path = app_config['venv_path']
|
|
|
|
app_path = app_config['app_path']
|
|
|
|
|
|
|
|
if not os.path.exists(venv_path):
|
|
|
|
return False, f"Virtual environment not found: {venv_path}"
|
|
|
|
|
|
|
|
if not os.path.exists(app_path):
|
|
|
|
return False, f"Application directory not found: {app_path}"
|
|
|
|
|
|
|
|
return True, "App directories found."
|
|
|
|
|
|
|
|
def get_app_status(app_name, running_processes):
|
|
|
|
if app_name in running_processes:
|
|
|
|
update_process_status(app_name, running_processes)
|
|
|
|
return running_processes[app_name]['status']
|
|
|
|
return 'stopped'
|
|
|
|
|
|
|
|
def find_and_kill_process_by_port(port):
|
|
|
|
for conn in psutil.net_connections():
|
|
|
|
if conn.laddr.port == port:
|
|
|
|
try:
|
|
|
|
process = psutil.Process(conn.pid)
|
|
|
|
for child in process.children(recursive=True):
|
|
|
|
child.kill()
|
|
|
|
process.kill()
|
|
|
|
return True
|
|
|
|
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
|
|
|
pass
|
|
|
|
return False
|
|
|
|
|
|
|
|
def force_kill_process_by_name(app_name, app_configs):
|
|
|
|
app_config = app_configs.get(app_name)
|
|
|
|
if not app_config:
|
|
|
|
return False, f"App '{app_name}' not found in configurations."
|
|
|
|
|
|
|
|
port = app_config['port']
|
|
|
|
killed = find_and_kill_process_by_port(port)
|
|
|
|
|
|
|
|
if killed:
|
|
|
|
return True, f"{app_name} processes have been forcefully terminated."
|
|
|
|
else:
|
|
|
|
return False, f"No running processes found for {app_name} on port {port}."
|
|
|
|
|
|
|
|
def update_webui_user_sh(app_name, app_configs):
|
|
|
|
app_config = app_configs.get(app_name)
|
|
|
|
if not app_config:
|
|
|
|
return
|
|
|
|
|
|
|
|
webui_user_sh_path = os.path.join(app_config['app_path'], 'webui-user.sh')
|
|
|
|
if not os.path.exists(webui_user_sh_path):
|
|
|
|
return
|
|
|
|
|
|
|
|
with open(webui_user_sh_path, 'r') as file:
|
|
|
|
content = file.read()
|
|
|
|
|
|
|
|
# Use regex to remove --port and its value
|
|
|
|
updated_content = re.sub(r'--port\s+\d+', '', content)
|
|
|
|
|
|
|
|
with open(webui_user_sh_path, 'w') as file:
|
|
|
|
file.write(updated_content)
|
|
|
|
|
|
|
|
def save_install_status(app_name, status, progress=0, stage=''):
|
|
|
|
data = {
|
|
|
|
'status': status,
|
|
|
|
'progress': progress,
|
|
|
|
'stage': stage
|
|
|
|
}
|
|
|
|
try:
|
|
|
|
with open(INSTALL_STATUS_FILE, 'r') as f:
|
|
|
|
all_statuses = json.load(f)
|
|
|
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
|
|
all_statuses = {}
|
|
|
|
|
|
|
|
all_statuses[app_name] = data
|
|
|
|
|
|
|
|
with open(INSTALL_STATUS_FILE, 'w') as f:
|
|
|
|
json.dump(all_statuses, f)
|
|
|
|
|
|
|
|
def get_install_status(app_name):
|
|
|
|
try:
|
|
|
|
with open(INSTALL_STATUS_FILE, 'r') as f:
|
|
|
|
all_statuses = json.load(f)
|
|
|
|
return all_statuses.get(app_name, {'status': 'not_started', 'progress': 0, 'stage': ''})
|
|
|
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
|
|
return {'status': 'not_started', 'progress': 0, 'stage': ''}
|
|
|
|
|
|
|
|
def download_and_unpack_venv(app_name, app_configs, send_websocket_message):
|
|
|
|
app_config = app_configs.get(app_name)
|
|
|
|
if not app_config:
|
|
|
|
return False, f"App '{app_name}' not found in configurations."
|
|
|
|
|
|
|
|
venv_path = app_config['venv_path']
|
|
|
|
app_path = app_config['app_path']
|
|
|
|
download_url = app_config['download_url']
|
|
|
|
total_size = app_config['size']
|
|
|
|
tar_filename = os.path.basename(download_url)
|
|
|
|
workspace_dir = '/workspace'
|
|
|
|
downloaded_file = os.path.join(workspace_dir, tar_filename)
|
|
|
|
|
|
|
|
try:
|
|
|
|
save_install_status(app_name, 'in_progress', 0, 'Downloading')
|
|
|
|
send_websocket_message('install_log', {'app_name': app_name, 'log': f'Starting download of {total_size / (1024 * 1024):.2f} MB...'})
|
|
|
|
|
|
|
|
response = requests.get(download_url, stream=True)
|
|
|
|
response.raise_for_status()
|
|
|
|
|
|
|
|
block_size = 8192
|
|
|
|
downloaded_size = 0
|
|
|
|
start_time = time.time()
|
|
|
|
|
|
|
|
with open(downloaded_file, 'wb') as file:
|
|
|
|
for chunk in response.iter_content(chunk_size=block_size):
|
|
|
|
if chunk:
|
|
|
|
file.write(chunk)
|
|
|
|
downloaded_size += len(chunk)
|
|
|
|
current_time = time.time()
|
|
|
|
elapsed_time = current_time - start_time
|
|
|
|
|
|
|
|
if elapsed_time > 0:
|
|
|
|
speed = downloaded_size / elapsed_time
|
|
|
|
percentage = (downloaded_size / total_size) * 100
|
|
|
|
eta = (total_size - downloaded_size) / speed if speed > 0 else 0
|
|
|
|
|
|
|
|
send_websocket_message('install_progress', {
|
|
|
|
'app_name': app_name,
|
|
|
|
'percentage': round(percentage, 2),
|
|
|
|
'speed': f"{speed / (1024 * 1024):.2f} MB/s",
|
|
|
|
'eta': f"{eta:.0f}",
|
|
|
|
'stage': 'Downloading',
|
|
|
|
'downloaded': f"{downloaded_size / (1024 * 1024):.2f} MB"
|
|
|
|
})
|
|
|
|
|
|
|
|
send_websocket_message('install_log', {'app_name': app_name, 'log': 'Download completed. Starting unpacking...'})
|
|
|
|
send_websocket_message('install_progress', {'app_name': app_name, 'percentage': 100, 'stage': 'Download Complete'})
|
|
|
|
|
|
|
|
# Ensure the venv directory exists
|
|
|
|
os.makedirs(venv_path, exist_ok=True)
|
|
|
|
|
|
|
|
# Unpack the tar.gz file
|
|
|
|
send_websocket_message('install_progress', {'app_name': app_name, 'percentage': 0, 'stage': 'Unpacking'})
|
|
|
|
unpack_command = f"tar -xzvf {downloaded_file} -C {venv_path}"
|
|
|
|
process = subprocess.Popen(unpack_command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True)
|
|
|
|
|
|
|
|
total_files = sum(1 for _ in subprocess.Popen(f"tar -tvf {downloaded_file}", shell=True, stdout=subprocess.PIPE).stdout)
|
|
|
|
files_processed = 0
|
|
|
|
|
|
|
|
for line in process.stdout:
|
|
|
|
files_processed += 1
|
|
|
|
percentage = min(int((files_processed / total_files) * 100), 100)
|
|
|
|
send_websocket_message('install_progress', {
|
|
|
|
'app_name': app_name,
|
|
|
|
'percentage': percentage,
|
|
|
|
'stage': 'Unpacking',
|
|
|
|
'processed': files_processed,
|
|
|
|
'total': total_files
|
|
|
|
})
|
|
|
|
send_websocket_message('install_log', {'app_name': app_name, 'log': f"Unpacking: {line.strip()}"})
|
|
|
|
|
|
|
|
process.wait()
|
|
|
|
if process.returncode != 0:
|
|
|
|
error_message = f"Unpacking failed: {process.stderr.read() if process.stderr else 'Unknown error'}"
|
|
|
|
send_websocket_message('install_complete', {'app_name': app_name, 'status': 'error', 'message': error_message})
|
|
|
|
save_install_status(app_name, 'failed', 0, 'Failed')
|
|
|
|
return False, error_message
|
|
|
|
|
|
|
|
send_websocket_message('install_progress', {'app_name': app_name, 'percentage': 100, 'stage': 'Unpacking Complete'})
|
|
|
|
|
|
|
|
# Clone the repository if it doesn't exist
|
|
|
|
if not os.path.exists(app_path):
|
|
|
|
send_websocket_message('install_log', {'app_name': app_name, 'log': 'Cloning repository...'})
|
|
|
|
|
|
|
|
repo_url = ''
|
|
|
|
if app_name == 'bcomfy':
|
|
|
|
repo_url = 'https://github.com/comfyanonymous/ComfyUI.git'
|
|
|
|
elif app_name == 'bforge':
|
|
|
|
repo_url = 'https://github.com/lllyasviel/stable-diffusion-webui-forge.git'
|
|
|
|
elif app_name == 'ba1111':
|
|
|
|
repo_url = 'https://github.com/AUTOMATIC1111/stable-diffusion-webui.git'
|
|
|
|
|
|
|
|
try:
|
|
|
|
git.Repo.clone_from(repo_url, app_path, progress=lambda op_code, cur_count, max_count, message: send_websocket_message('install_log', {
|
|
|
|
'app_name': app_name,
|
|
|
|
'log': f"Cloning: {cur_count}/{max_count} {message}"
|
|
|
|
}))
|
|
|
|
send_websocket_message('install_log', {'app_name': app_name, 'log': 'Repository cloned successfully.'})
|
|
|
|
|
|
|
|
# Clone ComfyUI-Manager for Better ComfyUI
|
|
|
|
if app_name == 'bcomfy':
|
|
|
|
custom_nodes_path = os.path.join(app_path, 'custom_nodes')
|
|
|
|
os.makedirs(custom_nodes_path, exist_ok=True)
|
|
|
|
comfyui_manager_path = os.path.join(custom_nodes_path, 'ComfyUI-Manager')
|
|
|
|
if not os.path.exists(comfyui_manager_path):
|
|
|
|
send_websocket_message('install_log', {'app_name': app_name, 'log': 'Cloning ComfyUI-Manager...'})
|
|
|
|
git.Repo.clone_from('https://github.com/ltdrdata/ComfyUI-Manager.git', comfyui_manager_path)
|
|
|
|
send_websocket_message('install_log', {'app_name': app_name, 'log': 'ComfyUI-Manager cloned successfully.'})
|
|
|
|
|
|
|
|
except git.exc.GitCommandError as e:
|
|
|
|
send_websocket_message('install_log', {'app_name': app_name, 'log': f'Error cloning repository: {str(e)}'})
|
|
|
|
return False, f"Error cloning repository: {str(e)}"
|
|
|
|
|
|
|
|
# Clean up the downloaded file
|
|
|
|
send_websocket_message('install_log', {'app_name': app_name, 'log': 'Cleaning up...'})
|
|
|
|
os.remove(downloaded_file)
|
2024-10-21 11:03:33 +02:00
|
|
|
send_websocket_message('install_log', {'app_name': app_name, 'log': 'Installation complete. Refresh page to start app'})
|
2024-10-12 14:46:41 +02:00
|
|
|
|
|
|
|
save_install_status(app_name, 'completed', 100, 'Completed')
|
|
|
|
send_websocket_message('install_complete', {'app_name': app_name, 'status': 'success', 'message': "Virtual environment installed successfully."})
|
|
|
|
return True, "Virtual environment installed successfully."
|
|
|
|
except requests.RequestException as e:
|
|
|
|
error_message = f"Download failed: {str(e)}"
|
|
|
|
send_websocket_message('install_complete', {'app_name': app_name, 'status': 'error', 'message': error_message})
|
|
|
|
save_install_status(app_name, 'failed', 0, 'Failed')
|
|
|
|
return False, error_message
|
|
|
|
except Exception as e:
|
|
|
|
error_message = f"Installation failed: {str(e)}\n{traceback.format_exc()}"
|
|
|
|
save_install_status(app_name, 'failed', 0, 'Failed')
|
|
|
|
send_websocket_message('install_complete', {'app_name': app_name, 'status': 'error', 'message': error_message})
|
|
|
|
return False, error_message
|
|
|
|
|
|
|
|
def fix_custom_nodes(app_name, app_configs):
|
|
|
|
if app_name != 'bcomfy':
|
|
|
|
return False, "This operation is only available for Better ComfyUI."
|
|
|
|
|
|
|
|
venv_path = app_configs['bcomfy']['venv_path']
|
|
|
|
app_path = app_configs['bcomfy']['app_path']
|
|
|
|
|
|
|
|
try:
|
|
|
|
# Activate the virtual environment and run the commands
|
|
|
|
activate_venv = f"source {venv_path}/bin/activate"
|
|
|
|
set_default_command = f"comfy --skip-prompt --no-enable-telemetry set-default {app_path}"
|
|
|
|
restore_dependencies_command = "comfy node restore-dependencies"
|
|
|
|
|
|
|
|
full_command = f"{activate_venv} && {set_default_command} && {restore_dependencies_command}"
|
|
|
|
|
|
|
|
process = subprocess.Popen(full_command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, executable='/bin/bash')
|
|
|
|
output, _ = process.communicate()
|
|
|
|
|
|
|
|
if process.returncode == 0:
|
|
|
|
return True, f"Custom nodes fixed successfully. Output: {output.decode('utf-8')}"
|
|
|
|
else:
|
|
|
|
return False, f"Error fixing custom nodes. Output: {output.decode('utf-8')}"
|
|
|
|
except Exception as e:
|
2024-10-21 11:03:33 +02:00
|
|
|
return False, f"Error fixing custom nodes: {str(e)}"
|
|
|
|
|
|
|
|
# Replace the existing install_app function with this updated version
|
|
|
|
def install_app(app_name, app_configs, send_websocket_message):
|
|
|
|
if app_name in app_configs:
|
|
|
|
return download_and_unpack_venv(app_name, app_configs, send_websocket_message)
|
|
|
|
else:
|
|
|
|
return False, f"Unknown app: {app_name}"
|
|
|
|
|
2024-10-26 22:15:28 +02:00
|
|
|
# unused function
|
|
|
|
def onsolate_update_model_symlinks():
|
|
|
|
# lutzapps - CHANGE #7 - use the new "shared_models" module for app model sharing
|
|
|
|
# remove this whole now unused function
|
|
|
|
return "replaced by utils.shared_models.update_model_symlinks()"
|
|
|
|
|
2024-10-21 11:03:33 +02:00
|
|
|
shared_models_dir = '/workspace/shared_models'
|
|
|
|
apps = {
|
|
|
|
'stable-diffusion-webui': '/workspace/stable-diffusion-webui/models',
|
|
|
|
'stable-diffusion-webui-forge': '/workspace/stable-diffusion-webui-forge/models',
|
|
|
|
'ComfyUI': '/workspace/ComfyUI/models'
|
|
|
|
}
|
|
|
|
model_types = ['Stable-diffusion', 'VAE', 'Lora', 'ESRGAN']
|
|
|
|
|
|
|
|
for model_type in model_types:
|
|
|
|
shared_model_path = os.path.join(shared_models_dir, model_type)
|
|
|
|
|
|
|
|
if not os.path.exists(shared_model_path):
|
|
|
|
continue
|
|
|
|
|
|
|
|
for app, app_models_dir in apps.items():
|
|
|
|
if app == 'ComfyUI':
|
|
|
|
if model_type == 'Stable-diffusion':
|
|
|
|
app_model_path = os.path.join(app_models_dir, 'checkpoints')
|
|
|
|
elif model_type == 'Lora':
|
|
|
|
app_model_path = os.path.join(app_models_dir, 'loras')
|
|
|
|
elif model_type == 'ESRGAN':
|
|
|
|
app_model_path = os.path.join(app_models_dir, 'upscale_models')
|
|
|
|
else:
|
|
|
|
app_model_path = os.path.join(app_models_dir, model_type.lower())
|
|
|
|
else:
|
|
|
|
app_model_path = os.path.join(app_models_dir, model_type)
|
|
|
|
|
|
|
|
# Create the app model directory if it doesn't exist
|
|
|
|
os.makedirs(app_model_path, exist_ok=True)
|
|
|
|
|
|
|
|
# Create symlinks for each file in the shared model directory
|
|
|
|
for filename in os.listdir(shared_model_path):
|
|
|
|
src = os.path.join(shared_model_path, filename)
|
|
|
|
dst = os.path.join(app_model_path, filename)
|
|
|
|
if os.path.isfile(src) and not os.path.exists(dst):
|
|
|
|
os.symlink(src, dst)
|
|
|
|
|
|
|
|
print("Model symlinks updated.")
|