Initial YakPanel commit
This commit is contained in:
935
class_v2/projectModelV2/btpyvm.py
Normal file
935
class_v2/projectModelV2/btpyvm.py
Normal file
@@ -0,0 +1,935 @@
|
||||
# coding: utf-8
|
||||
# -------------------------------------------------------------------
|
||||
# YakPanel
|
||||
# -------------------------------------------------------------------
|
||||
# Copyright (c) 2014-2099 YakPanel(www.yakpanel.com) All rights reserved.
|
||||
# -------------------------------------------------------------------
|
||||
# Author: yakpanel
|
||||
# -------------------------------------------------------------------
|
||||
# py virtual environment manager
|
||||
# ------------------------------
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import tarfile
|
||||
import threading
|
||||
import time
|
||||
from platform import machine
|
||||
|
||||
import requests
|
||||
import traceback
|
||||
import subprocess
|
||||
import argparse
|
||||
from typing import Optional, Tuple, List, Union, Dict, TextIO
|
||||
from xml.etree import cElementTree
|
||||
|
||||
os.chdir("/www/server/panel")
|
||||
if "class/" not in sys.path:
|
||||
sys.path.insert(0, "class/")
|
||||
if "/www/server/panel" not in sys.path:
|
||||
sys.path.insert(0, "/www/server/panel")
|
||||
|
||||
from mod.project.python.pyenv_tool import EnvironmentManager
|
||||
|
||||
import public
|
||||
|
||||
|
||||
class _VmSTD:
|
||||
out = sys.stdout
|
||||
err = sys.stderr
|
||||
|
||||
|
||||
_vm_std = _VmSTD()
|
||||
|
||||
|
||||
def is_aarch64() -> bool:
|
||||
_arch = machine().lower()
|
||||
if _arch in ("aarch64", "arm64"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def parse_version_to_list(version: str) -> Tuple[int, int, int]:
|
||||
tmp = version.split(".")
|
||||
if len(tmp) == 1:
|
||||
return int(tmp[0]), 0, 0
|
||||
elif len(tmp) == 2:
|
||||
return int(tmp[0]), int(tmp[1]), 0
|
||||
else:
|
||||
return int(tmp[0]), int(tmp[1]), int(tmp[2])
|
||||
|
||||
|
||||
def _get_index_of_python(url_list: List[str], timeout=10) -> Optional[Dict]:
|
||||
winner: Dict = {}
|
||||
done_event = threading.Event()
|
||||
lock = threading.Lock() # 并发访问winner
|
||||
|
||||
def get_result(test_url):
|
||||
try:
|
||||
response = requests.get(test_url, timeout=timeout)
|
||||
text = response.text
|
||||
if not text:
|
||||
return
|
||||
with lock:
|
||||
# Only record the first successful response
|
||||
if not winner:
|
||||
winner["data"] = text
|
||||
winner["time"] = time.time()
|
||||
winner["url"] = test_url
|
||||
done_event.set()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for url in url_list:
|
||||
threading.Thread(target=get_result, args=(url,), daemon=True).start()
|
||||
# 阻塞直到第一个成功响应或所有线程超时
|
||||
done_event.wait(timeout=timeout)
|
||||
return winner if winner else None
|
||||
|
||||
|
||||
def get_index_of_python() -> Optional[Dict]:
|
||||
url_list = [
|
||||
"https://repo.huaweicloud.com/python/", # China mirror (Huawei Cloud)
|
||||
"https://npmmirror.com/mirrors/python/", # China mirror (Aliyun)
|
||||
"https://www.python.org/ftp/python/", # Official (官方国际)
|
||||
"https://mirrors.dotsrc.org/python/", # Europe mirror (欧洲)
|
||||
]
|
||||
print(public.lang("Checking network status......"), file=_vm_std.out, flush=True)
|
||||
res = _get_index_of_python(url_list, timeout=10)
|
||||
if res is None:
|
||||
res = _get_index_of_python(url_list, timeout=60)
|
||||
if res is None:
|
||||
print(
|
||||
public.lang("Unable to connect to network, querying CPython interpreter version......"),
|
||||
file=_vm_std.out,
|
||||
flush=True
|
||||
)
|
||||
return res
|
||||
|
||||
|
||||
def get_index_of_pypy_python() -> Optional[Dict]:
|
||||
url_list = [
|
||||
"https://buildbot.pypy.org/mirror/", # PyPy build mirror
|
||||
"https://downloads.python.org/pypy/", # Official (international)
|
||||
]
|
||||
print(public.lang("Checking network status......"), file=_vm_std.out, flush=True)
|
||||
res = _get_index_of_python(url_list, timeout=10)
|
||||
if res is None:
|
||||
res = _get_index_of_python(url_list, timeout=60)
|
||||
if res is None:
|
||||
print(
|
||||
public.lang("Unable to connect to network, querying PyPy interpreter version......"),
|
||||
file=_vm_std.out,
|
||||
flush=True
|
||||
)
|
||||
return res
|
||||
|
||||
|
||||
class PythonVersion:
|
||||
def __init__(self, v: str, is_pypy: bool = False, filename: str = None):
|
||||
self.version = v
|
||||
self.is_pypy = is_pypy
|
||||
self.bt_python_path = "/www/server/pyporject_evn/versions"
|
||||
self.bt_pypy_path = "/www/server/pyporject_evn/pypy_versions"
|
||||
self._file_name = filename.strip() if isinstance(filename, str) else None
|
||||
|
||||
self._ver_t = None
|
||||
if not os.path.exists(self.bt_python_path):
|
||||
os.makedirs(self.bt_python_path)
|
||||
|
||||
if not os.path.exists(self.bt_pypy_path):
|
||||
os.makedirs(self.bt_pypy_path)
|
||||
|
||||
@property
|
||||
def ver_t(self) -> Tuple[int, int, int]:
|
||||
if self._ver_t is not None:
|
||||
return self._ver_t
|
||||
self._ver_t = parse_version_to_list(self.version)
|
||||
return self._ver_t
|
||||
|
||||
@property
|
||||
def installed(self) -> bool:
|
||||
if self.is_pypy:
|
||||
return os.path.exists(self.bt_pypy_path + "/" + self.version)
|
||||
return os.path.exists(self.bt_python_path + "/" + self.version)
|
||||
|
||||
@staticmethod
|
||||
def check(file) -> bool:
|
||||
print(public.lang("[2/3] Verifying source file......"), file=_vm_std.out, flush=True)
|
||||
if not os.path.exists(file):
|
||||
print(public.lang("File does not exist, cannot verify"), file=_vm_std.out, flush=True)
|
||||
return False
|
||||
if os.path.getsize(file) < 1024 * 1024 * 10:
|
||||
print(public.lang("File content is incomplete"), file=_vm_std.out, flush=True)
|
||||
os.remove(file)
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def file_name(self) -> str:
|
||||
if self._file_name:
|
||||
return self._file_name
|
||||
if self.is_pypy and not self._file_name:
|
||||
raise Exception(public.lang("No file name"))
|
||||
return "Python-{}.tar.xz".format(self.version)
|
||||
|
||||
@staticmethod
|
||||
def _download_file(dst, url):
|
||||
print(url, file=_vm_std.out, flush=True)
|
||||
response = requests.get(url, stream=True, headers={
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0"
|
||||
})
|
||||
total_size = int(response.headers.get('content-length', 0))
|
||||
print(
|
||||
public.lang("Source file size to download: %.2fM") % (total_size / (1024 * 1024)),
|
||||
file=_vm_std.out,
|
||||
flush=True
|
||||
)
|
||||
if total_size == 0:
|
||||
print(public.lang("File download error!"), file=_vm_std.out, flush=True)
|
||||
downloaded_size = 0
|
||||
block_size = 1024 * 1024
|
||||
with open(dst, 'wb') as f:
|
||||
for data in response.iter_content(block_size):
|
||||
f.write(data)
|
||||
downloaded_size += len(data)
|
||||
progress = (downloaded_size / total_size) * 100
|
||||
print(
|
||||
public.lang("Downloading....") + "\t %.2f%% completed" % progress,
|
||||
end='\r',
|
||||
flush=True,
|
||||
file=_vm_std.out
|
||||
)
|
||||
response.close()
|
||||
|
||||
def download(self, base_url) -> bool:
|
||||
if self.is_pypy:
|
||||
cache_dir = os.path.join(self.bt_pypy_path, "cached")
|
||||
else:
|
||||
cache_dir = os.path.join(self.bt_python_path, "cached")
|
||||
if not os.path.exists(cache_dir):
|
||||
os.makedirs(cache_dir)
|
||||
dst = os.path.join(cache_dir, self.file_name)
|
||||
if os.path.exists(dst) and os.path.getsize(dst) > 1024 * 1024 * 10:
|
||||
print(public.lang("[1/3] Using cached source file......"), file=_vm_std.out, flush=True)
|
||||
return self.check(dst)
|
||||
|
||||
print(public.lang("[1/3] Downloading source file......"), file=_vm_std.out, flush=True)
|
||||
print(public.lang("Downloading source file......"), file=_vm_std.out, flush=True)
|
||||
down_url = "{}{}/{}".format(base_url, self.version, self.file_name)
|
||||
if self.is_pypy:
|
||||
down_url = "{}{}".format(base_url, self.file_name)
|
||||
self._download_file(dst, down_url)
|
||||
print(public.lang("Download completed"), file=_vm_std.out, flush=True)
|
||||
return self.check(dst)
|
||||
|
||||
def _install(self, extended_args='') -> bool:
|
||||
if self.is_pypy:
|
||||
return self._install_pypy()
|
||||
print(public.lang("[3/3] Extracting and installing....."), file=_vm_std.out, flush=True)
|
||||
install_sh = "{}/script/install_python.sh".format(public.get_panel_path())
|
||||
check_openssl_args, extended_args = self._parse_extended_args(extended_args)
|
||||
sh_str = "bash {} {} {} '{}'".format(
|
||||
install_sh,
|
||||
self.version,
|
||||
check_openssl_args,
|
||||
extended_args
|
||||
)
|
||||
p = subprocess.Popen(sh_str, stdout=_vm_std.out, stderr=_vm_std.out, shell=True)
|
||||
p.wait()
|
||||
if not os.path.exists(self.bt_python_path + "/" + self.version):
|
||||
return False
|
||||
self.install_pip_tool(self.bt_python_path + "/" + self.version)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _parse_extended_args(extended_args) -> Tuple[str, str]:
|
||||
rep_openssl = re.compile(r"--with-openssl=(?P<path>\S+)")
|
||||
res = rep_openssl.search(extended_args)
|
||||
if res:
|
||||
path = res.group("path")
|
||||
if os.path.exists(path):
|
||||
return "not_check_openssl", extended_args
|
||||
else:
|
||||
extended_args = extended_args.replace(res.group(), "")
|
||||
return "check_openssl", extended_args
|
||||
return "check_openssl", extended_args
|
||||
|
||||
def _install_pypy(self) -> bool:
|
||||
print(public.lang("[3/3] Extracting and installing....."), file=_vm_std.out, flush=True)
|
||||
cache_dir = os.path.join(self.bt_pypy_path, "cached")
|
||||
d_file = os.path.join(cache_dir, self.file_name)
|
||||
tar = tarfile.open(d_file, "r|bz2")
|
||||
tar.extractall(self.bt_pypy_path)
|
||||
tar.close()
|
||||
os.renames(self.bt_pypy_path + "/" + self.file_name[:-8], self.bt_pypy_path + "/" + self.version)
|
||||
if not os.path.exists(self.bt_pypy_path + "/" + self.version):
|
||||
return False
|
||||
public.writeFile("{}/{}/is_pypy.pl".format(self.bt_pypy_path, self.version), "")
|
||||
self.install_pip_tool(self.bt_pypy_path + "/" + self.version)
|
||||
return True
|
||||
|
||||
def install_pip_tool(self, python_path):
|
||||
print(public.lang("Installing pip tool....."), file=_vm_std.out, flush=True)
|
||||
python_bin = "{}/bin/python3".format(python_path)
|
||||
pip_bin = "{}/bin/pip3".format(python_path)
|
||||
if not os.path.exists(python_bin):
|
||||
python_bin = "{}/bin/python".format(python_path)
|
||||
pip_bin = "{}/bin/pip".format(python_path)
|
||||
|
||||
if self._ver_t[:2] < (3, 4):
|
||||
_ver = "{}.{}".format(*self._ver_t[:2])
|
||||
if self._ver_t[:2] in ((3, 1), (3, 0)):
|
||||
_ver = "3.2"
|
||||
|
||||
cache_dir = os.path.join(self.bt_python_path, "cached")
|
||||
if not os.path.exists(cache_dir):
|
||||
os.makedirs(cache_dir)
|
||||
get_pip_file = os.path.join(cache_dir, "get-pip{}.py".format(_ver))
|
||||
if not os.path.exists(get_pip_file):
|
||||
url = "{}/install/plugin/pythonmamager/pip/get-pip{}.py".format(public.get_url(), _ver)
|
||||
self._download_file(get_pip_file, url)
|
||||
|
||||
shutil.copyfile(get_pip_file, os.path.join(python_path, "get-pip.py"))
|
||||
|
||||
sh_str = "{} {}".format(python_bin, os.path.join(python_path, "get-pip.py"))
|
||||
p = subprocess.Popen(sh_str, stdout=_vm_std.out, stderr=_vm_std.out, shell=True)
|
||||
p.wait()
|
||||
print(public.lang("pip tool installation finished"), file=_vm_std.out, flush=True)
|
||||
else:
|
||||
sh_str = "{} -m ensurepip".format(python_bin)
|
||||
p = subprocess.Popen(sh_str, stdout=_vm_std.out, stderr=_vm_std.out, shell=True)
|
||||
p.wait()
|
||||
print(public.lang("pip tool installation finished"), file=_vm_std.out, flush=True)
|
||||
|
||||
if not os.path.exists(pip_bin):
|
||||
print(public.lang("pip tool installation failed!!!!"), file=_vm_std.out, flush=True)
|
||||
else:
|
||||
self.update_pip_tool(pip_bin)
|
||||
|
||||
@staticmethod
|
||||
def update_pip_tool(pip_bin: str):
|
||||
update_str = "{} install --upgrade pip setuptools".format(pip_bin)
|
||||
p = subprocess.Popen(update_str, stdout=_vm_std.out, stderr=_vm_std.out, shell=True)
|
||||
p.wait()
|
||||
|
||||
def install(self, base_url, extended_args='') -> Tuple[bool, str]:
|
||||
print(public.lang("Start installing......"), file=_vm_std.out, flush=True)
|
||||
if not self.is_pypy:
|
||||
dst = os.path.join(self.bt_python_path, self.version)
|
||||
else:
|
||||
dst = os.path.join(self.bt_pypy_path, self.version)
|
||||
if os.path.isdir(dst):
|
||||
return True, public.lang("Already installed")
|
||||
# download file
|
||||
if not self.download(base_url):
|
||||
return False, public.lang("File download and verification failed!")
|
||||
# install python
|
||||
if not self._install(extended_args):
|
||||
return False, public.lang("Extraction and installation failed!")
|
||||
print(public.lang("Installation completed!"), file=_vm_std.out, flush=True)
|
||||
home_path = self.bt_python_path + "/" + self.version
|
||||
if self.is_pypy:
|
||||
home_path = self.bt_pypy_path + "/" + self.version
|
||||
|
||||
bin_path = "{}/bin/python".format(home_path)
|
||||
if not os.path.exists(bin_path):
|
||||
bin_path = "{}/bin/python3".format(home_path)
|
||||
if not os.path.exists(bin_path):
|
||||
print(public.lang("Python installation failed!"))
|
||||
return False, public.lang("Python installation failed!")
|
||||
|
||||
if bin_path == "{}/bin/python3".format(home_path):
|
||||
os.symlink(os.path.realpath(bin_path), "{}/bin/python".format(home_path))
|
||||
elif bin_path == "{}/bin/python".format(home_path) and not os.path.exists("{}/bin/python3".format(home_path)):
|
||||
os.symlink(os.path.realpath(bin_path), "{}/bin/python3".format(home_path))
|
||||
|
||||
EnvironmentManager.add_python_env("system", bin_path)
|
||||
return True, ""
|
||||
|
||||
@staticmethod
|
||||
def parse_version(version: str) -> Tuple[bool, str]:
|
||||
v_rep = re.compile(r"(?P<target>\d+\.\d{1,2}(\.\d{1,2})?)")
|
||||
v_res = v_rep.search(version)
|
||||
if v_res:
|
||||
v = v_res.group("target")
|
||||
return True, v
|
||||
else:
|
||||
return False, ""
|
||||
|
||||
|
||||
class _PyCommandManager(object):
|
||||
_FORMAT_DATA = """
|
||||
# Start-Python-Env command line environment settings
|
||||
export PATH="{}${{PATH}}"
|
||||
# End-Python-Env
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def check_use():
|
||||
out, _ = public.ExecShell("lsattr /etc/profile")
|
||||
return out.find("--i") == -1
|
||||
|
||||
def set_python_env(self, python_path: str) -> Tuple[bool, str]:
|
||||
if python_path is None:
|
||||
python_path = "" # 清除设置
|
||||
else:
|
||||
python_path = python_path + ":"
|
||||
|
||||
if not self.check_use():
|
||||
return False, public.lang("System hardening appears to be enabled, operation not allowed")
|
||||
try:
|
||||
rep = re.compile(r'# +Start-Python-Env[^\n]*\n(export +PATH=".*")\n# +End-Python-Env')
|
||||
profile_data = public.readFile("/etc/profile")
|
||||
if not isinstance(profile_data, str):
|
||||
return False, public.lang("Configuration file load error")
|
||||
tmp_res = rep.search(profile_data)
|
||||
if tmp_res is not None:
|
||||
new_profile_data = rep.sub(self._FORMAT_DATA.format(python_path).strip("\n"), profile_data, 1)
|
||||
else:
|
||||
new_profile_data = profile_data + self._FORMAT_DATA.format(python_path)
|
||||
public.writeFile("/etc/profile", new_profile_data)
|
||||
return True, public.lang("Configuration set successfully")
|
||||
except:
|
||||
return False, public.lang("Setting error")
|
||||
|
||||
@staticmethod
|
||||
def get_python_env() -> Optional[str]:
|
||||
profile_data = public.readFile("/etc/profile")
|
||||
if not isinstance(profile_data, str):
|
||||
return None
|
||||
rep = re.compile(r'# +Start-Python-Env[^\n]*\n(export +PATH="(?P<target>.*)")\n# +End-Python-Env')
|
||||
tmp_res = rep.search(profile_data)
|
||||
if tmp_res is None:
|
||||
return None
|
||||
path_data = tmp_res.group("target")
|
||||
python_path = path_data.split(":")[0].strip()
|
||||
if os.path.exists(python_path):
|
||||
return python_path
|
||||
return None
|
||||
|
||||
|
||||
class PYVM(object):
|
||||
bt_python_path = "/www/server/pyporject_evn/versions"
|
||||
bt_pypy_path = "/www/server/pyporject_evn/pypy_versions"
|
||||
_c_py_version_default = (
|
||||
"2.7.18", "3.0.1", "3.1.5", "3.2.6", "3.3.7", "3.4.10", "3.5.10", "3.6.15", "3.7.17", "3.8.19",
|
||||
"3.9.19", "3.10.14", "3.11.9", "3.12.3"
|
||||
)
|
||||
|
||||
_pypy_version_default = (
|
||||
("3.10.14", "pypy3.10-v7.3.16-linux64.tar.bz2"),
|
||||
("3.9.19", "pypy3.10-v7.3.16-linux64.tar.bz2"),
|
||||
("3.8.16", "pypy3.8-v7.3.11-linux64.tar.bz2"),
|
||||
("3.7.13", "pypy3.7-v7.3.9-linux64.tar.bz2"),
|
||||
("3.6.12", "pypy3.6-v7.3.3-linux64.tar.bz2"),
|
||||
("2.7.18", "pypy2.7-v7.3.16-linux64.tar.bz2"),
|
||||
)
|
||||
|
||||
def __init__(self, use_shell=False):
|
||||
if not os.path.exists(self.bt_python_path):
|
||||
os.makedirs(self.bt_python_path)
|
||||
if not os.path.exists(self.bt_pypy_path):
|
||||
os.makedirs(self.bt_pypy_path)
|
||||
self.use_shell = use_shell
|
||||
self._cpy_base_url = None
|
||||
self._pypy_base_url = None
|
||||
self.stable_versions: Optional[List[PythonVersion]] = None
|
||||
self._py_cmd_mgr = _PyCommandManager()
|
||||
self.is_pypy = False
|
||||
self.async_version = False
|
||||
|
||||
def now_python_path(self) -> Optional[str]:
|
||||
return self._py_cmd_mgr.get_python_env()
|
||||
|
||||
def set_python_path(self, python_path) -> Tuple[bool, str]:
|
||||
return self._py_cmd_mgr.set_python_env(python_path)
|
||||
|
||||
@staticmethod
|
||||
def check_use():
|
||||
res = os.popen("lsattr /etc/profile")
|
||||
return res.read().find("--i--") == -1
|
||||
|
||||
@property
|
||||
def base_url(self):
|
||||
if self.is_pypy:
|
||||
if self._pypy_base_url is not None:
|
||||
return self._pypy_base_url
|
||||
res = get_index_of_pypy_python()
|
||||
if res is not None:
|
||||
self._pypy_base_url = res["url"]
|
||||
return self._pypy_base_url
|
||||
else:
|
||||
if self._cpy_base_url is not None:
|
||||
return self._cpy_base_url
|
||||
res = get_index_of_python()
|
||||
if res is not None:
|
||||
self._cpy_base_url = res["url"]
|
||||
return self._cpy_base_url
|
||||
return None
|
||||
|
||||
# 获取版本
|
||||
def get_py_version(self, force=False):
|
||||
if isinstance(self.stable_versions, list) and len(self.stable_versions) > 1:
|
||||
return self.stable_versions
|
||||
if not force:
|
||||
self.stable_versions = self._get_versions_by_local()
|
||||
|
||||
if not force and self.async_version and not self.stable_versions:
|
||||
self._async_get_versions()
|
||||
if self.is_pypy:
|
||||
self.stable_versions = [
|
||||
PythonVersion(v, is_pypy=True, filename=f) for v, f in self._pypy_version_default
|
||||
]
|
||||
return self.stable_versions
|
||||
self.stable_versions = [PythonVersion(i, is_pypy=False) for i in self._c_py_version_default]
|
||||
return self.stable_versions
|
||||
|
||||
if not self.stable_versions:
|
||||
if self.use_shell:
|
||||
print(
|
||||
public.lang("No local record file found, requesting Python version data from cloud,"
|
||||
" this may take a while, please wait"),
|
||||
file=_vm_std.out
|
||||
)
|
||||
self.stable_versions, err = self._get_versions_by_cloud()
|
||||
# 缓存数据到本地
|
||||
if isinstance(self.stable_versions, list) and len(self.stable_versions) > 1:
|
||||
self._save_cached(self.stable_versions)
|
||||
else:
|
||||
print(err, file=_vm_std.out)
|
||||
|
||||
if force and not self.stable_versions:
|
||||
self.stable_versions = self._get_versions_by_local()
|
||||
|
||||
if not self.stable_versions:
|
||||
self.stable_versions = []
|
||||
|
||||
return self.stable_versions
|
||||
|
||||
def _async_get_versions(self):
|
||||
pyvm_mgr = copy.deepcopy(self)
|
||||
|
||||
def get_versions():
|
||||
pyvm_mgr.async_version = False
|
||||
pyvm_mgr.get_py_version(force=True)
|
||||
|
||||
task = threading.Thread(target=get_versions)
|
||||
task.start()
|
||||
|
||||
def _get_versions_by_local(self) -> [Optional[List[PythonVersion]]]:
|
||||
"""
|
||||
获取本地稳定版本的缓存数据
|
||||
"""
|
||||
local_path = "/www/server/panel/data/pyvm"
|
||||
if not os.path.exists(local_path):
|
||||
os.makedirs(local_path)
|
||||
return None
|
||||
stable_file = os.path.join(local_path, "stable_versions.txt")
|
||||
if self.is_pypy:
|
||||
stable_file = os.path.join(local_path, "pypy_versions.txt")
|
||||
if not os.path.isfile(stable_file):
|
||||
return None
|
||||
with open(stable_file, "r") as f:
|
||||
if self.is_pypy:
|
||||
stable_versions = []
|
||||
for line in f.readlines():
|
||||
v, filename = line.split("|")
|
||||
stable_versions.append(PythonVersion(v, is_pypy=True, filename=filename))
|
||||
else:
|
||||
stable_versions = [PythonVersion(line.strip()) for line in f.readlines()]
|
||||
|
||||
return stable_versions
|
||||
|
||||
def _get_versions_by_cloud(self) -> Tuple[Optional[List[PythonVersion]], Optional[str]]:
|
||||
"""
|
||||
获取云端支持的稳定版本 排除2.7的稳定版本以外的其他版本
|
||||
"""
|
||||
if self.is_pypy:
|
||||
return self._get_pypy_versions_by_cloud()
|
||||
|
||||
res = get_index_of_python()
|
||||
if res is None:
|
||||
return None, public.lang("Unable to connect to cloud, please check network connection")
|
||||
self._base_url: str = res["url"]
|
||||
data_txt: str = res["data"]
|
||||
|
||||
try:
|
||||
stable_go_versions = self.__parser_xml(data_txt)
|
||||
return stable_go_versions, None
|
||||
except:
|
||||
traceback.print_exc(file=_vm_std.err)
|
||||
return None, public.lang("Parse error")
|
||||
|
||||
def _get_pypy_versions_by_cloud(self) -> Tuple[Optional[List[PythonVersion]], Optional[str]]:
|
||||
"""
|
||||
获取云端支持的稳定版本 排除2.7的稳定版本以外的其他版本
|
||||
"""
|
||||
|
||||
if self.base_url is None:
|
||||
return None, public.lang("Unable to connect to cloud, please check network connection")
|
||||
try:
|
||||
stable_versions = []
|
||||
ver_json = json.loads(requests.get(self.base_url + "versions.json").text)
|
||||
arch = 'aarch64' if is_aarch64() else "x64"
|
||||
for i in ver_json:
|
||||
if i["stable"] is True and i["latest_pypy"] is True:
|
||||
for file in i["files"]:
|
||||
if file["arch"] == arch and file["platform"] == "linux":
|
||||
stable_versions.append(
|
||||
PythonVersion(i["python_version"], is_pypy=True, filename=file["filename"])
|
||||
)
|
||||
return stable_versions, None
|
||||
except:
|
||||
traceback.print_exc(file=_vm_std.err)
|
||||
return None, public.lang("Parse error")
|
||||
|
||||
def __parser_xml(self, data_txt: str) -> List[PythonVersion]:
|
||||
res_list = []
|
||||
# 只取pre部分
|
||||
start = data_txt.rfind("<pre>")
|
||||
end = data_txt.rfind("</pre>") + len("</pre>")
|
||||
if not start > 0 or not end > 0:
|
||||
return res_list
|
||||
data_txt = data_txt[start:end] # 去除hr标签导致的错误
|
||||
last_2 = {
|
||||
"data": (2, 0, 0),
|
||||
"version": None,
|
||||
}
|
||||
|
||||
root = cElementTree.fromstring(data_txt)
|
||||
for data in root.findall("./a"):
|
||||
v_str = data.text
|
||||
if v_str.startswith("2."):
|
||||
ver = v_str.strip("/")
|
||||
t_version = parse_version_to_list(ver)
|
||||
if t_version > last_2["data"]:
|
||||
last_2["data"] = t_version
|
||||
last_2["version"] = ver
|
||||
continue
|
||||
if v_str.startswith("3."):
|
||||
p_v = PythonVersion(v_str.strip("/"))
|
||||
res_list.append(p_v)
|
||||
continue
|
||||
|
||||
if last_2["version"]:
|
||||
res_list.insert(0, PythonVersion(last_2["version"]))
|
||||
|
||||
res_list.sort(key=lambda x: x.ver_t)
|
||||
|
||||
need_remove = []
|
||||
for ver in res_list[::-1]:
|
||||
if not self.test_last_version_is_stable(ver):
|
||||
need_remove.append(ver)
|
||||
else:
|
||||
break
|
||||
for ver in need_remove:
|
||||
res_list.remove(ver)
|
||||
|
||||
return res_list
|
||||
|
||||
# 检查最新的版本是否有正式发布版本包
|
||||
def test_last_version_is_stable(self, ver: PythonVersion) -> bool:
|
||||
response = requests.get("{}{}/".format(self.base_url, ver.version), timeout=10)
|
||||
data = response.text
|
||||
if data.find(ver.file_name) != -1:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def _save_cached(self, stable_go_versions: List[PythonVersion]) -> None:
|
||||
local_path = "/www/server/panel/data/pyvm"
|
||||
if not os.path.exists(local_path):
|
||||
os.makedirs(local_path)
|
||||
|
||||
if self.is_pypy:
|
||||
with open(os.path.join(local_path, "pypy_versions.txt"), "w") as f:
|
||||
for py_v in stable_go_versions:
|
||||
f.write(py_v.version + "|" + py_v.file_name + "\n")
|
||||
return
|
||||
|
||||
with open(os.path.join(local_path, "stable_versions.txt"), "w") as f:
|
||||
for py_v in stable_go_versions:
|
||||
f.write(py_v.version + "\n")
|
||||
|
||||
@staticmethod
|
||||
def del_cached():
|
||||
local_path = "/www/server/panel/data/pyvm"
|
||||
stable_file = os.path.join(local_path, "stable_versions.txt")
|
||||
pypy_file = os.path.join(local_path, "pypy_versions.txt")
|
||||
if os.path.isfile(stable_file):
|
||||
os.remove(stable_file)
|
||||
if os.path.isfile(pypy_file):
|
||||
os.remove(pypy_file)
|
||||
|
||||
def api_ls(self) -> Tuple[List[str], List[str]]:
|
||||
return [i.strip() for i in os.listdir(self.bt_python_path) if i.startswith("2") or i.startswith("3")], \
|
||||
[i.strip() for i in os.listdir(self.bt_pypy_path) if i.startswith("2") or i.startswith("3")]
|
||||
|
||||
def cmd_ls(self) -> None:
|
||||
cpy_versions, pypy_versions = self.api_ls()
|
||||
versions = pypy_versions if self.is_pypy else cpy_versions
|
||||
if not versions:
|
||||
print(public.lang("No Python interpreter version is installed"))
|
||||
return
|
||||
print("version: ")
|
||||
for i in versions:
|
||||
print(" " + i)
|
||||
|
||||
def api_ls_remote(self, is_all: bool, force=False) -> Tuple[Optional[List[PythonVersion]], Optional[str]]:
|
||||
self.get_py_version(force)
|
||||
self.stable_versions.sort(key=lambda k: k.ver_t, reverse=True)
|
||||
if is_all:
|
||||
return self.stable_versions, None
|
||||
res_new = []
|
||||
tow_list = [0, 0]
|
||||
for i in self.stable_versions:
|
||||
if i.ver_t[:2] != tow_list:
|
||||
res_new.append(i)
|
||||
tow_list = i.ver_t[:2]
|
||||
|
||||
return res_new, None
|
||||
|
||||
def cmd_ls_remote(self, is_all: bool) -> None:
|
||||
stable, err = self.api_ls_remote(is_all)
|
||||
cpy_installed, pypy_install = self.api_ls()
|
||||
installed = pypy_install if self.is_pypy else cpy_installed
|
||||
if err:
|
||||
print(public.lang("An error occurred while fetching version information"), file=sys.stderr)
|
||||
print(err, file=sys.stderr)
|
||||
print("Stable Version:")
|
||||
for i in stable:
|
||||
if i.version in installed:
|
||||
i.version += " <- installed"
|
||||
print(" " + i.version)
|
||||
|
||||
def _get_version(self, version) -> Union[PythonVersion, str]:
|
||||
stable, err = self.api_ls_remote(True)
|
||||
if err:
|
||||
if self.use_shell:
|
||||
print(public.lang("An error occurred while fetching version information"), file=_vm_std.err)
|
||||
print(err, file=_vm_std.err)
|
||||
return err
|
||||
for i in stable:
|
||||
if i.version == version:
|
||||
return i
|
||||
|
||||
if self.use_shell:
|
||||
print(public.lang("Corresponding version not found"), file=_vm_std.err)
|
||||
return public.lang("Corresponding version not found")
|
||||
|
||||
def re_install_pip_tools(self, version, python_path):
|
||||
py_v = self._get_version(version)
|
||||
if isinstance(py_v, str):
|
||||
return False, py_v
|
||||
if not py_v.installed:
|
||||
return False, public.lang("Version not installed")
|
||||
if not self.is_pypy:
|
||||
public.ExecShell("rm -rf {}/bin/pip*".format(python_path))
|
||||
public.ExecShell(
|
||||
"rm -rf {}/lib/python{}.{}/site-packages/pip*".format(python_path, py_v.ver_t[0], py_v.ver_t[1]))
|
||||
else:
|
||||
public.ExecShell("rm -rf {}/bin/pip*".format(python_path))
|
||||
public.ExecShell(
|
||||
"rm -rf {}/lib/pypy{}.{}/site-packages/pip*".format(python_path, py_v.ver_t[0], py_v.ver_t[1])
|
||||
)
|
||||
|
||||
py_v.install_pip_tool(python_path)
|
||||
|
||||
def api_install(self, version) -> Tuple[bool, str]:
|
||||
py_v = self._get_version(version)
|
||||
if isinstance(py_v, str):
|
||||
return False, py_v
|
||||
if self.base_url is None:
|
||||
return False, public.lang("Internet connect error, please check")
|
||||
return py_v.install(self.base_url)
|
||||
|
||||
def cmd_install(self, version, extended_args='') -> None:
|
||||
py_v = self._get_version(version)
|
||||
if isinstance(py_v, str):
|
||||
pass
|
||||
if self.base_url is None:
|
||||
print("Internet connect error, please check", file=sys.stderr)
|
||||
return
|
||||
_, err = py_v.install(self.base_url, extended_args)
|
||||
if err:
|
||||
print(err, file=sys.stderr)
|
||||
|
||||
def api_uninstall(self, version: str) -> Tuple[bool, str]:
|
||||
if not self.is_pypy:
|
||||
py_path = self.bt_python_path + "/" + version
|
||||
else:
|
||||
py_path = self.bt_pypy_path + "/" + version
|
||||
if os.path.exists(py_path):
|
||||
import shutil
|
||||
shutil.rmtree(py_path)
|
||||
return True, public.lang("Uninstall completed")
|
||||
|
||||
def cmd_uninstall(self, version: str) -> None:
|
||||
_, msg = self.api_uninstall(version)
|
||||
print(msg, file=sys.stdout)
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def set_std(out: TextIO, err: TextIO) -> None:
|
||||
_vm_std.out = out
|
||||
_vm_std.err = err
|
||||
|
||||
@staticmethod
|
||||
def _serializer_of_list(s: list, installed: List[str]) -> List[Dict]:
|
||||
return [{
|
||||
"version": v.version,
|
||||
"type": "stable",
|
||||
"installed": True if v.version in installed else False
|
||||
} for v in s]
|
||||
|
||||
def python_versions(self, refresh=False):
|
||||
res = {
|
||||
'status': True,
|
||||
'cpy_installed': [],
|
||||
'pypy_installed': [],
|
||||
'sdk': {
|
||||
"all": [],
|
||||
"streamline": [],
|
||||
"pypy": [],
|
||||
},
|
||||
'use': self.now_python_path(),
|
||||
'command_path': None,
|
||||
}
|
||||
sdk = res["sdk"]
|
||||
old_type = self.is_pypy
|
||||
cpy_installed, pypy_installed = self.api_ls()
|
||||
cpy_installed.sort(key=lambda x: int(x.split(".")[1]), reverse=True)
|
||||
res['cpy_installed'] = cpy_installed
|
||||
pypy_installed.sort(key=lambda x: int(x.split(".")[1]), reverse=True)
|
||||
res['pypy_installed'] = pypy_installed
|
||||
cpy_command_path = [
|
||||
{
|
||||
"python_path": os.path.join(self.bt_python_path, i, "bin"),
|
||||
"type": "version",
|
||||
"version": i,
|
||||
"is_pypy": False,
|
||||
} for i in cpy_installed
|
||||
]
|
||||
pypy_command_path = [
|
||||
{
|
||||
"python_path": os.path.join(self.bt_pypy_path, i, "bin"),
|
||||
"type": "version",
|
||||
"version": i,
|
||||
"is_pypy": True,
|
||||
} for i in pypy_installed
|
||||
]
|
||||
res["command_path"] = cpy_command_path + pypy_command_path
|
||||
|
||||
# cpy
|
||||
self.is_pypy = False
|
||||
self.get_py_version(refresh)
|
||||
self.stable_versions.sort(key=lambda k: k.ver_t, reverse=True)
|
||||
if not self.stable_versions:
|
||||
sdk["all"] = sdk["streamline"] = [
|
||||
{"version": v, "type": "stable", "installed": True} for v in cpy_installed
|
||||
]
|
||||
else:
|
||||
sdk["all"] = self._serializer_of_list(self.stable_versions, cpy_installed)
|
||||
res_new = []
|
||||
tow_list = [0, 0]
|
||||
for i in self.stable_versions:
|
||||
if i.ver_t[:2] != tow_list:
|
||||
res_new.append(i)
|
||||
tow_list = i.ver_t[:2]
|
||||
sdk["streamline"] = self._serializer_of_list(res_new, cpy_installed)
|
||||
for i in sdk["streamline"]:
|
||||
if i.get("version") in cpy_installed:
|
||||
cpy_installed.remove(i.get("version", ""))
|
||||
if set(cpy_installed):
|
||||
for i in set(cpy_installed):
|
||||
sdk["streamline"].insert(0, {
|
||||
"version": i,
|
||||
"type": "stable",
|
||||
"installed": True
|
||||
})
|
||||
|
||||
# pypy
|
||||
self.is_pypy = True
|
||||
self.stable_versions = []
|
||||
self.get_py_version(refresh)
|
||||
self.stable_versions.sort(key=lambda k: k.ver_t, reverse=True)
|
||||
if not self.stable_versions:
|
||||
sdk["pypy"] = [
|
||||
{"version": v, "type": "stable", "installed": True} for v in pypy_installed
|
||||
]
|
||||
else:
|
||||
sdk["pypy"] = self._serializer_of_list(self.stable_versions, pypy_installed)
|
||||
|
||||
self.stable_versions = None
|
||||
self.is_pypy = old_type
|
||||
|
||||
return res
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description='pyvm Python解释器版本管理器')
|
||||
parser.add_argument('-pypy', action='store_true', help='管理PyPy解释器')
|
||||
# 添加子命令
|
||||
subparsers = parser.add_subparsers(title='operation', dest='command')
|
||||
# 添加ls子命令
|
||||
subparsers.add_parser('ls', help='展示已安装的Python解释器版本')
|
||||
subparsers.add_parser('clear_cache', help='清除版本缓存')
|
||||
# 添加ls子命令
|
||||
parser_ls_r = subparsers.add_parser('ls-remote', help='展示可安装Python解释器版本,默认只展示每个版本中较新的版本')
|
||||
parser_ls_r.add_argument('-a', action='store_true', help='展示可以安装的所有Python解释器版本')
|
||||
# 添加install子命令
|
||||
parser_install = subparsers.add_parser('install', help='安装指定版本')
|
||||
parser_install.add_argument('version', type=str, help='要安装的Python版本,例如3.10.0')
|
||||
parser_install.add_argument(
|
||||
'--extend', type=str, default='',
|
||||
help="传递给Python编译的额外选项,用单引号包围多个选项,如'--disable-ipv6 --enable-loadable-sqlite-extensions'"
|
||||
)
|
||||
|
||||
# 添加uninstall子命令
|
||||
parser_uninstall = subparsers.add_parser('uninstall', help='卸载并删除指定版本')
|
||||
parser_uninstall.add_argument('uninstall_param', type=str, help='完整的版本号')
|
||||
# 添加install_pip子命令
|
||||
parser_uninstall = subparsers.add_parser('install_pip', help='卸载并删除指定版本')
|
||||
parser_uninstall.add_argument('install_pip_param', type=str, help='完整的版本号')
|
||||
|
||||
input_args = parser.parse_args()
|
||||
pyvm = PYVM()
|
||||
if isinstance(pyvm, str):
|
||||
print(pyvm, file=sys.stderr)
|
||||
exit(1)
|
||||
if input_args.pypy:
|
||||
pyvm.is_pypy = True
|
||||
pyvm.use_shell = True
|
||||
if input_args.command == 'clear_cache':
|
||||
pyvm.del_cached()
|
||||
elif input_args.command == 'ls':
|
||||
pyvm.cmd_ls()
|
||||
elif input_args.command == "ls-remote":
|
||||
_is_all = True if input_args.a else False
|
||||
pyvm.cmd_ls_remote(_is_all)
|
||||
elif input_args.command == "install":
|
||||
extended = input_args.extend
|
||||
_flag, _v = PythonVersion.parse_version(input_args.version)
|
||||
if _flag:
|
||||
pyvm.cmd_install(_v, extended)
|
||||
else:
|
||||
print(public.lang("Version parameter error, should be in the format 1.xx.xx"), file=sys.stderr)
|
||||
elif input_args.command == "uninstall":
|
||||
_flag, _v = PythonVersion.parse_version(input_args.uninstall_param)
|
||||
if _flag:
|
||||
pyvm.cmd_uninstall(_v)
|
||||
else:
|
||||
print(public.lang("Version parameter error, should be in the format 1.xx.xx"), file=sys.stderr)
|
||||
elif input_args.command == "install_pip":
|
||||
_flag, _v = PythonVersion.parse_version(input_args.install_pip_param)
|
||||
if _flag:
|
||||
pyvm.re_install_pip_tools(_v, pyvm.bt_python_path + "/" + _v)
|
||||
else:
|
||||
print(public.lang("Version parameter error, should be in the format 1.xx.xx"), file=sys.stderr)
|
||||
else:
|
||||
print(public.lang("Use pyvm -h to view operation commands"), file=sys.stderr)
|
||||
|
||||
Reference in New Issue
Block a user