ywydog




ClassIsland 侧没啥,主要逻辑全在 Python 脚本里(AI 生成),该脚本能被

自动化调用。
源代码:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
根据程序运行逻辑.txt 实现的自动更新脚本。
需要安装的第三方库:
pip install send2trash win10toast
"""
import os
import sys
import time
# import shutil
import random
import configparser
import subprocess
import tkinter as tk
from tkinter import messagebox
import send2trash
from win10toast import ToastNotifier
# ==================== 辅助函数 ====================
def is_explorer_running():
"""检测 explorer.exe 是否在运行(不弹出控制台窗口)"""
try:
result = subprocess.run(
['tasklist', '/FI', 'IMAGENAME eq explorer.exe', '/NH'],
capture_output=True, text=True, encoding='gbk',
creationflags=subprocess.CREATE_NO_WINDOW
)
return 'explorer.exe' in result.stdout
except Exception:
return False
def compare_versions(ver1: str, ver2: str) -> int:
"""
比较两个版本号(格式:x.x.x.x.x.x.x)
返回值:
1 if ver1 > ver2
-1 if ver1 < ver2
0 if equal
"""
def normalize(v):
return [int(x) for x in v.strip().split('.')]
v1 = normalize(ver1)
v2 = normalize(ver2)
for a, b in zip(v1, v2):
if a != b:
return 1 if a > b else -1
return 0
def show_toast(title, msg, duration=3):
"""显示 Windows Toast 通知"""
try:
toaster = ToastNotifier()
toaster.show_toast(title, msg, duration=duration, threaded=True)
except Exception:
pass
def open_folder(path):
"""在资源管理器中打开文件夹"""
try:
os.startfile(path)
except Exception as e:
print(f"打开文件夹 {path} 失败: {e}")
def run_uri(uri):
"""运行 classisland 协议 URI"""
try:
os.startfile(uri)
except Exception as e:
print(f"运行 URI {uri} 失败: {e}")
# ==================== 临时更新脚本的内容 ====================
UPDATER_TEMPLATE = '''#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import sys
import time
import shutil
import subprocess
import send2trash
def is_process_running(pid=None, name=None):
"""检查进程是否存在(不弹出控制台窗口)"""
if pid is not None:
try:
output = subprocess.check_output(
['tasklist', '/FI', f'PID eq {pid}', '/NH'],
timeout=2, text=True, encoding='gbk',
creationflags=subprocess.CREATE_NO_WINDOW
)
return str(pid) in output and 'No tasks' not in output
except:
return False
elif name is not None:
try:
output = subprocess.check_output(
['tasklist', '/FI', f'IMAGENAME eq {name}', '/NH'],
timeout=2, text=True, encoding='gbk',
creationflags=subprocess.CREATE_NO_WINDOW
)
return name in output and 'No tasks' not in output
except:
return False
return False
def wait_process_exit(pid=None, name=None, interval=1):
"""等待进程退出"""
while is_process_running(pid=pid, name=name):
time.sleep(interval)
def run_uri(uri):
"""运行 classisland 协议 URI"""
try:
os.startfile(uri)
except Exception as e:
print(f"运行 URI {uri} 失败: {{e}}")
def remove_readonly(func, path, exc_info):
"""删除只读文件时的回调函数"""
os.chmod(path, 0o777)
func(path)
def safe_rmtree(path, max_retries=3):
"""安全删除目录,支持重试和只读处理"""
for attempt in range(max_retries):
try:
if os.path.exists(path):
shutil.rmtree(path, onerror=remove_readonly)
return True
except Exception as e:
if attempt == max_retries - 1:
print(f"删除 {path} 失败: {e}")
return False
time.sleep(1)
def main():
# 切换工作目录到临时目录,避免锁定要删除的文件夹
temp_dir = os.environ.get('TEMP', os.path.expanduser('~\\\\AppData\\\\Local\\\\Temp'))
os.chdir(temp_dir)
# 获取命令行参数
if len(sys.argv) < 2:
print("错误:缺少父进程 PID 参数")
sys.exit(1)
parent_pid = int(sys.argv[1])
# 1. 等待当前脚本进程退出
print(f"等待父进程 (PID={parent_pid}) 退出...")
wait_process_exit(pid=parent_pid)
print("父进程已退出")
# 2. 运行 update_ci URI,通知主程序准备更新
print("运行 update_ci URI...")
run_uri('classisland://app/api/automation/run/update_ci')
# 3. 等待 ClassIsland.Desktop.exe 退出
print("等待 ClassIsland.Desktop.exe 退出...")
wait_process_exit(name='ClassIsland.Desktop.exe')
print("ClassIsland.Desktop.exe 已退出")
# 4. 执行更新操作
print("开始更新...")
# 复制 F:\\ClassIsland\\Main 到 D:\\Program_Files
src_main = r'F:\\ClassIsland\\Main'
dst_main = r'D:\\Program_Files\\Main'
if os.path.exists(dst_main):
safe_rmtree(dst_main)
shutil.copytree(src_main, dst_main)
# 将旧的 D:\\Program_Files\\ClassIsland 移到回收站
old_ci = r'D:\\Program_Files\\ClassIsland'
if os.path.exists(old_ci):
try:
send2trash.send2trash(old_ci)
except Exception as e:
print(f"移动到回收站失败: {{e}},尝试直接删除")
safe_rmtree(old_ci)
# 重命名 Main -> ClassIsland
os.rename(dst_main, r'D:\\Program_Files\\ClassIsland')
print("更新完成")
# 5. 运行 update_finished URI,通知主程序更新已完成
print("运行 update_finished URI...")
run_uri('classisland://app/api/automation/run/update_finished')
# 删除临时脚本自身
try:
os.remove(__file__)
except:
pass
if __name__ == '__main__':
main()
'''
# ==================== 主要流程 ====================
def main():
# ---------- 检测 F 盘 ----------
if not os.path.exists('F:\\'):
sys.exit(0)
# 全局标志:是否已打开 F 盘资源管理器
F_opened = False
# 配置项(从 ini 读取或默认设置)
config = {}
# ---------- 分支一:判断 F:\ClassIsland 是否存在 ----------
class_island_path = 'F:\\ClassIsland'
if not os.path.exists(class_island_path):
# 分支三:打开 F:\,设置标志
open_folder('F:\\')
F_opened = True
# 直接跳转到版本比较部分(分支十)
else:
# 分支二:读取配置文件
ini_file = os.path.join(class_island_path, 'Config', 'Config.ini')
try:
cp = configparser.ConfigParser()
cp.read(ini_file, encoding='utf-8')
if 'ClassIsland' in cp:
config = dict(cp.items('ClassIsland'))
else:
raise KeyError('缺少 [ClassIsland] 节')
except Exception:
# 分支四:配置损坏或不存在
show_toast('提示', '配置文件损坏或不存在', duration=3)
config = {
'allow_exit_fullscreen_time': 'True',
'allow_update': 'True',
'allow_auto_update': 'False'
}
# 分支五:检测 explorer.exe 是否运行
if is_explorer_running():
# 分支六:根据 F_opened 决定是否打开 F:\
if not F_opened:
# 分支七:打开 F:\
# open_folder('F:\\')
F_opened = True
# 分支六之后进入分支十
else:
# 分支八:检查 allow_exit_fullscreen_time
allow_exit = config.get('allow_exit_fullscreen_time', 'False').lower() == 'true'
if allow_exit:
# 分支九:启动 explorer.exe,延时,运行 URI
try:
subprocess.Popen(['explorer.exe'], creationflags=subprocess.CREATE_NO_WINDOW)
except Exception:
pass
time.sleep(1)
run_uri('classisland://app/api/automation/run/exit_fullscreen_time')
# 分支九结束后,进入分支六
if not F_opened:
# open_folder('F:\\')
F_opened = True
# ========== 分支十及之后 ==========
# 分支十:读取 F:\ClassIsland\Main\Version
version1_file = os.path.join('F:\\ClassIsland', 'Main', 'Version')
try:
with open(version1_file, 'r', encoding='utf-8') as f:
version1 = f.read().strip()
except Exception:
# 读取失败,退出脚本
sys.exit(0)
# 分支十一:读取 D:\Program_Files\ClassIsland\Version
version2_file = os.path.join('D:\\Program_Files', 'ClassIsland', 'Version')
try:
with open(version2_file, 'r', encoding='utf-8') as f:
version2 = f.read().strip()
except Exception:
# 分支十三:提示文件损坏或不存在
show_toast('提示', 'D:\\Program_Files\\ClassIsland\\Version 文件损坏或不存在', duration=3)
sys.exit(0)
# 分支十二:比较版本
if compare_versions(version1, version2) <= 0:
# 版本1不大于版本2,退出
sys.exit(0)
# 分支十四:检查 allow_update
if config.get('allow_update', 'False').lower() != 'true':
sys.exit(0)
# 分支十五:检查 allow_auto_update
auto_update = config.get('allow_auto_update', 'False').lower() == 'true'
if not auto_update:
# 分支十六:使用 tkinter 询问用户
# ========== 高 DPI 支持 ==========
if sys.platform == 'win32':
try:
import ctypes
ctypes.windll.user32.SetProcessDPIAware()
except Exception:
pass
# ================================
root = tk.Tk()
root.withdraw() # 隐藏主窗口
result = messagebox.askyesno('ClassIsland 更新', '"F:\\"内有 ClassIsland 新版本:{0} → {1}。\n立即更新?'.format(version2, version1), icon='question')
root.destroy()
if not result:
sys.exit(0)
# ==================== 分支十七:创建临时更新脚本并退出 ====================
# 生成临时脚本文件名
temp_dir = os.environ.get('TEMP', os.path.expanduser('~\\AppData\\Local\\Temp'))
updater_name = f'classisland_updater_{random.randint(100000, 999999)}.py'
updater_path = os.path.join(temp_dir, updater_name)
# 写入临时脚本
with open(updater_path, 'w', encoding='utf-8') as f:
f.write(UPDATER_TEMPLATE)
# 启动临时脚本,传入当前进程 PID,隐藏窗口
subprocess.Popen(
[sys.executable, updater_path, str(os.getpid())],
creationflags=subprocess.CREATE_NO_WINDOW
)
# 退出当前脚本
sys.exit(0)
if __name__ == '__main__':
main()