launcher/launcher/webview/api.py

260 lines
10 KiB
Python

import io
import os
import shutil
import subprocess
import threading
from _winapi import CREATE_NO_WINDOW
from pathlib import Path
from shutil import rmtree
from subprocess import Popen
import requests
from launcher import config
from launcher.config.conf import LaunchPart
from launcher.constants import (
download_git_url,
download_python_url,
get_new_version_url,
upgrade_script_name,
mirror_list,
file_name,
instances_folder_name,
)
from launcher.file.download import init_download, download_file
from launcher.file.extract import extract_7z_file
from launcher.file.utils import ensure_directory_exists, check_command_path
from launcher.instances import manager
from launcher.log import logger
from launcher.sys_config import sys_config
from launcher.webview.events import custom_event, LogType
command_list = {
"download_git": lambda: init_download("git", download_git_url, os.getcwd()),
"download_python": lambda: init_download(
"python", download_python_url, os.getcwd()
),
"lfs": "git\\bin\\git lfs install",
"ensurepip": "python\\python -m ensurepip --default-pip",
"clone": "git\\bin\\git -c lfs.concurrenttransfers=100 clone https://git.zhaozuohong.vip/mower-ng/mower-ng.git --branch slow",
"fetch": lambda: f"..\\git\\bin\\git fetch origin {config.conf.branch} --progress",
"switch": lambda: f"..\\git\\bin\\git -c lfs.concurrenttransfers=100 switch -f {config.conf.branch} --progress",
"reset": lambda: f"..\\git\\bin\\git -c lfs.concurrenttransfers=200 reset --hard origin/{config.conf.branch}",
"pip_tools_install": lambda: f"..\\python\\Scripts\\pip install --no-cache-dir -i {mirror_list[config.conf.mirror]} pip-tools --no-warn-script-location",
"pip_sync": lambda: f"..\\python\\Scripts\\pip-sync -i {mirror_list[config.conf.mirror]} requirements.txt",
"webview": lambda instance_path="": f'..\\python\\pythonw -X utf8 webview_ui.py "{instance_path}"',
}
def parse_stderr(stderr_output):
error_keywords = {
"fatal: destination path 'mower-ng' already exists and is not an empty directory.": "mower-ng文件夹已存在并且非空。",
"index.lock': File exists": "上一个git命令正在执行,请等待执行结束或在任务管理器中杀掉git进程,并确保上方提示的index.lock文件删除后再次运行。",
"Could not resolve host": "网络出现错误,请检查网络是否通畅。",
"ReadTimeoutError": "网络连接超时,请检查网络连接或尝试更换镜像源。",
"No space left on device": "磁盘空间不足。",
}
for keyword, message in error_keywords.items():
if keyword in stderr_output:
return message
return "未定义的错误"
def check_command_end(command_key, output):
end_keywords = {"webview": {"WebSocket客户端建立连接": "mower_ng已成功运行"}}
if command_key in end_keywords:
keywords = end_keywords[command_key]
for keyword in keywords:
if keyword in output:
custom_event(LogType.info, keywords[keyword])
return True
return False
class Api:
def load_config(self):
logger.debug("读取配置文件")
return config.conf.model_dump()
def save_config(self, conf):
logger.debug(f"更新配置文件{conf}")
config.conf = config.Conf(**conf)
config.save_conf()
def get_version(self):
return sys_config.get("version")
def get_new_version(self):
logger.info("获取最新版本号")
response = requests.get(get_new_version_url)
return response.json()
# 更新启动器本身
def update_self(self, download_url):
logger.info(f"开始更新启动器 {download_url}")
current_path = os.getcwd()
download_tmp_folder = os.path.join(current_path, "download_tmp")
# 确保 download_tmp 文件夹存在
ensure_directory_exists(download_tmp_folder)
if not download_file("launcher", download_url, download_tmp_folder):
return "下载新版本失败"
download_path = os.path.join(download_tmp_folder, file_name)
if not extract_7z_file("launcher", download_path, download_tmp_folder, True):
return "解压新版本失败"
exe_path = os.path.join(current_path, "launcher.exe")
folder_path = os.path.join(current_path, "_internal")
new_exe_path = os.path.join(download_tmp_folder, "launcher", "launcher.exe")
new_folder_path = os.path.join(download_tmp_folder, "launcher", "_internal")
script_path = os.path.join(download_tmp_folder, upgrade_script_name)
with open(script_path, "w") as b:
temp_list = "@echo off\n"
temp_list += "timeout /t 3 /nobreak\n" # 等待进程退出
temp_list += f"rmdir {folder_path}\n" # 删除_internal
temp_list += f"del {exe_path}\n" # 删除exe
temp_list += f"xcopy /e /i /y {new_folder_path} {folder_path}\n"
temp_list += f"move {new_exe_path} {exe_path}\n"
temp_list += "timeout /t 1 /nobreak\n" # 等待操作完成
temp_list += f"start {exe_path}\n" # 启动新程序
temp_list += "exit"
b.write(temp_list)
# 不显示cmd窗口
Popen([script_path], creationflags=CREATE_NO_WINDOW)
os._exit(0)
def rm_site_packages(self):
try:
site_packages_path = Path("./python/Lib/site-packages")
if site_packages_path.exists():
rmtree(site_packages_path)
return "site-packages目录移除成功"
return "python\\Lib\\site-packages目录不存在"
except Exception as e:
return repr(e)
def rm_python_scripts(self):
try:
python_scripts_path = Path("./python/Scripts")
if python_scripts_path.exists():
rmtree(python_scripts_path)
return "Scripts目录移除成功"
return "python\\Scripts目录不存在"
except Exception as e:
return repr(e)
def run(self, command_key, cwd=None, params={}):
command = command_list[command_key]
if callable(command):
try:
command = command(**params)
except TypeError:
command = command()
if callable(command):
return "success" if command() else "failed"
if cwd is not None:
custom_event(LogType.info, f"命令执行目录:{cwd}")
custom_event(LogType.execute_command, command)
# 执行命令前先判断命令路径是否存在
exist, command_path = check_command_path(command, cwd)
if not exist:
custom_event(
LogType.error,
f"命令路径不存在:{command_path} 请尝试依赖修复并重新初始化。",
)
return "failed"
try:
stdout_stderr = []
with subprocess.Popen(
command,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
shell=True,
cwd=cwd,
bufsize=0,
universal_newlines=False,
) as p:
def process_lines(text_io):
for line in iter(text_io.readline, ""):
text = line.rstrip("\n").strip()
custom_event(LogType.command_out, text)
if stdout_stderr is not None:
stdout_stderr.append(text)
if check_command_end(command_key, text):
break
detected_encoding = "utf-8"
text_io = io.TextIOWrapper(
p.stdout, encoding=detected_encoding, errors="replace"
)
try:
process_lines(text_io)
except UnicodeDecodeError:
p.stdout.seek(0) # 重新将流指针重置到开头
text_io = io.TextIOWrapper(
p.stdout, encoding="gbk", errors="replace"
)
process_lines(text_io)
finally:
text_io.close()
if p.returncode == 0:
return "success"
else:
error_message = parse_stderr("\n".join(stdout_stderr))
custom_event(LogType.error, f"命令执行失败,{error_message}")
return "failed"
except Exception as e:
logger.exception(e)
custom_event(LogType.error, str(e))
return "failed"
def add_instance(self):
return manager.add_instance()
def delete_instance(self, path):
instances_dir = os.path.join(os.getcwd(), instances_folder_name)
abs_path = os.path.abspath(path)
if os.path.commonpath(
[abs_path, instances_dir]
) == instances_dir and os.path.exists(abs_path):
shutil.rmtree(abs_path)
def start_checked_instance(self):
checked_instances = [
instance for instance in config.conf.instances if instance.checked
]
if not checked_instances:
custom_event(LogType.warning, "没有选中的实例")
return [{"status": False, "message": "No checked instances"}]
def _run_instance(instance: LaunchPart.Instance):
self.run(
"webview",
"mower-ng",
{"instance_path": instance.path},
)
# 创建并启动线程 目前进程依然有阻塞,待优化
for instance in checked_instances:
thread = threading.Thread(
target=_run_instance, args=(instance,), daemon=True
)
thread.start()
def migrate_default_instance(self):
"""迁移默认实例文件到新实例"""
source_path = os.path.join(os.getcwd(), "mower-ng")
return manager.migrate_instance(source_path)
def migrate_instances_config(self):
"""迁移多开配置"""
return manager.migrate_instances_config()
def open_folder(self, path):
if not os.path.exists(path):
custom_event(LogType.error, f"路径不存在:{path}")
else:
os.startfile(path)
custom_event(LogType.info, f"成功打开文件夹:{path}")