282 lines
11 KiB
Python
282 lines
11 KiB
Python
import os
|
|
|
|
gui_path = 'src/virtualbox_client/gui.py'
|
|
with open(gui_path, 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
|
|
# ===== 1. 添加 ScriptWorker 类(放在 DownloadWorker 之后) =====
|
|
old = """class WorkspaceWorker(QObject):"""
|
|
|
|
new = """class ScriptWorker(QObject):
|
|
"""上传本地脚本到虚拟机、执行并下载日志"""
|
|
log_signal = Signal(str, str)
|
|
finished_signal = Signal(bool)
|
|
error_signal = Signal(str)
|
|
|
|
def __init__(self, config: dict, local_path: str):
|
|
super().__init__()
|
|
self.config = config
|
|
self.local_path = local_path
|
|
self.cancel_event = threading.Event()
|
|
|
|
def cancel(self):
|
|
self.cancel_event.set()
|
|
|
|
def run(self):
|
|
ssh_only = self.config.get("ssh_only", False)
|
|
_com_initialized = False
|
|
if not ssh_only:
|
|
try:
|
|
import ctypes
|
|
ctypes.windll.ole32.CoInitializeEx(None, 0)
|
|
_com_initialized = True
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
if ssh_only:
|
|
from virtualbox_client.utils.ssh_manager import SSHManager
|
|
vm = SSHManager(
|
|
self.config["ssh_host"], self.config["ssh_port"],
|
|
self.config["ssh_user"], self.config["ssh_pass"],
|
|
)
|
|
else:
|
|
from virtualbox_client.utils.vbox_ssh_manager import VBoxSSHManager
|
|
vm = VBoxSSHManager(
|
|
self.config["vm_name"], self.config["ssh_host"],
|
|
self.config["ssh_port"], self.config["ssh_user"],
|
|
self.config["ssh_pass"],
|
|
)
|
|
vm.set_cancel_event(self.cancel_event)
|
|
|
|
if self.cancel_event.is_set():
|
|
return
|
|
|
|
import builtins
|
|
original_print = print
|
|
def custom_print(*args, **kwargs):
|
|
msg = " ".join(str(a) for a in args)
|
|
self.log_signal.emit(msg, "info")
|
|
original_print(*args, **kwargs)
|
|
builtins.print = custom_print
|
|
|
|
try:
|
|
script_name = os.path.basename(self.local_path)
|
|
remote_path = f"/tmp/{script_name}"
|
|
log_path = f"/tmp/{script_name}.log"
|
|
local_log_path = f"{script_name}.log"
|
|
|
|
# 1. 上传脚本
|
|
self.log_signal.emit(f"📤 [1/3] 上传脚本: {self.local_path} -> {remote_path}", "info")
|
|
if not vm.upload_file(self.local_path, remote_path):
|
|
self.log_signal.emit("❌ 上传失败", "error")
|
|
return
|
|
|
|
# 2. 执行脚本
|
|
self.log_signal.emit(f"⚡ [2/3] 执行脚本: {remote_path}", "info")
|
|
cmd = (
|
|
f"chmod +x {remote_path} && "
|
|
f"echo '{self.config['ssh_pass']}' | sudo -S {remote_path} 2>&1 | "
|
|
f"tee {log_path}"
|
|
)
|
|
out, err, code = vm.exec_command(cmd, get_pty=False)
|
|
self.log_signal.emit(f"✔ 脚本执行完成,退出码: {code}", "info")
|
|
|
|
# 3. 下载日志
|
|
self.log_signal.emit(f"📥 [3/3] 下载日志: {log_path} -> {local_log_path}", "info")
|
|
if vm.download_file(log_path, local_log_path):
|
|
self.log_signal.emit(f"✔ 日志已保存到本地: {local_log_path}", "success")
|
|
else:
|
|
self.log_signal.emit("⚠ tee 日志下载失败,尝试保存终端输出", "warning")
|
|
if out:
|
|
with open(local_log_path, "w") as f:
|
|
f.write(out)
|
|
self.log_signal.emit(f"✔ 终端输出已保存到: {local_log_path}", "success")
|
|
|
|
# 4. 清理远程临时文件
|
|
vm.exec_command(f'rm -f "{remote_path}" "{log_path}"')
|
|
self.log_signal.emit("🧹 远程临时文件已清理", "info")
|
|
|
|
self.log_signal.emit("🎉 脚本执行全部完成!", "success")
|
|
self.finished_signal.emit(True)
|
|
finally:
|
|
builtins.print = original_print
|
|
|
|
except ImportError as e:
|
|
self.log_signal.emit(f"❌ 缺少依赖: {e}", "error")
|
|
self.error_signal.emit(str(e))
|
|
except Exception as e:
|
|
self.log_signal.emit(f"❌ 脚本执行错误: {str(e)}", "error")
|
|
self.error_signal.emit(str(e))
|
|
finally:
|
|
if _com_initialized:
|
|
try:
|
|
import ctypes
|
|
ctypes.windll.ole32.CoUninitialize()
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
class WorkspaceWorker(QObject):"""
|
|
|
|
content = content.replace(old, new)
|
|
|
|
# ===== 2. 在 TaskPanel.setup_ui 的文件传输 tab 后面添加脚本执行 tab =====
|
|
old_file_tab_end = """ file_layout.addStretch()
|
|
self.tab_widget.addTab(file_tab, "📁 文件传输")"""
|
|
|
|
new_file_tab_end = """ file_layout.addStretch()
|
|
self.tab_widget.addTab(file_tab, "📁 文件传输")
|
|
|
|
# ====== 脚本执行 Tab ======
|
|
script_tab = QWidget()
|
|
script_layout = QVBoxLayout(script_tab)
|
|
script_layout.setContentsMargins(8, 8, 8, 8)
|
|
|
|
script_group = QGroupBox("⚡ 上传并执行脚本")
|
|
script_group_layout = QVBoxLayout(script_group)
|
|
|
|
script_path_layout = QHBoxLayout()
|
|
self.script_path_edit = QLineEdit(placeholderText="选择本地 .sh 脚本文件...")
|
|
script_path_layout.addWidget(self.script_path_edit)
|
|
self.script_browse_btn = QPushButton("📂 浏览")
|
|
self.script_browse_btn.setFixedWidth(80)
|
|
self.script_browse_btn.clicked.connect(self._browse_script)
|
|
script_path_layout.addWidget(self.script_browse_btn)
|
|
script_group_layout.addWidget(QLabel("本地脚本路径:"))
|
|
script_group_layout.addLayout(script_path_layout)
|
|
|
|
self.run_script_btn = QPushButton("⚡ 上传并执行脚本")
|
|
self.run_script_btn.setStyleSheet(\"""
|
|
QPushButton { background-color: #8e44ad; color: white; padding: 10px 30px;
|
|
font-size: 14px; font-weight: bold; border: none; border-radius: 6px; }
|
|
QPushButton:hover { background-color: #9b59b6; }
|
|
QPushButton:disabled { background-color: #555; color: #999; }
|
|
\""")
|
|
self.run_script_btn.clicked.connect(self._run_script)
|
|
script_group_layout.addWidget(self.run_script_btn)
|
|
|
|
script_layout.addWidget(script_group)
|
|
|
|
# 执行说明
|
|
desc_group = QGroupBox("📖 说明")
|
|
desc_layout = QVBoxLayout(desc_group)
|
|
desc_label = QLabel(
|
|
"1. 选择一个本地 .sh 脚本文件\\n"
|
|
"2. 点击按钮自动完成:\\n"
|
|
" 📤 上传脚本到虚拟机 /tmp/ 目录\\n"
|
|
" ⚡ 以 sudo 权限执行脚本\\n"
|
|
" 📥 下载执行日志到本地\\n"
|
|
" 🧹 清理远程临时文件"
|
|
)
|
|
desc_label.setStyleSheet("color: #aaa; font-size: 12px; line-height: 1.6;")
|
|
desc_layout.addWidget(desc_label)
|
|
script_layout.addWidget(desc_group)
|
|
|
|
script_layout.addStretch()
|
|
self.tab_widget.addTab(script_tab, "⚡ 脚本执行")"""
|
|
|
|
content = content.replace(old_file_tab_end, new_file_tab_end)
|
|
|
|
# ===== 3. 在 TaskPanel 的 _set_ui_running 中禁用脚本按钮 =====
|
|
old_running = """ def _set_ui_running(self, running: bool):
|
|
self.run_btn.setEnabled(not running)
|
|
self.cancel_btn.setEnabled(running)
|
|
self.task_input.setEnabled(not running)
|
|
if self._config_panel:
|
|
self._config_panel.set_enabled(not running)"""
|
|
|
|
new_running = """ def _set_ui_running(self, running: bool):
|
|
self.run_btn.setEnabled(not running)
|
|
self.cancel_btn.setEnabled(running)
|
|
self.task_input.setEnabled(not running)
|
|
self.run_script_btn.setEnabled(not running)
|
|
self.script_browse_btn.setEnabled(not running)
|
|
self.script_path_edit.setEnabled(not running)
|
|
if self._config_panel:
|
|
self._config_panel.set_enabled(not running)"""
|
|
|
|
content = content.replace(old_running, new_running)
|
|
|
|
# ===== 4. 在 TaskPanel 中添加 _browse_script 和 _run_script 方法 =====
|
|
# 放在 _on_download_error 之后、class MainWindow 之前
|
|
old_mainwindow = """class MainWindow(QMainWindow):"""
|
|
|
|
new_methods = """ _script_thread = None
|
|
_script_worker = None
|
|
|
|
def _cleanup_script(self):
|
|
if self._script_thread:
|
|
try:
|
|
self._script_thread.finished.disconnect()
|
|
except (TypeError, RuntimeError):
|
|
pass
|
|
if self._script_thread.isRunning():
|
|
self._script_thread.quit()
|
|
if not self._script_thread.wait(5000):
|
|
self._script_thread.terminate()
|
|
self._script_thread.wait(3000)
|
|
self._script_thread = None
|
|
if self._script_worker:
|
|
try:
|
|
self._script_worker.log_signal.disconnect()
|
|
self._script_worker.finished_signal.disconnect()
|
|
self._script_worker.error_signal.disconnect()
|
|
except (TypeError, RuntimeError):
|
|
pass
|
|
self._script_worker = None
|
|
|
|
def _browse_script(self):
|
|
file_path, _ = QFileDialog.getOpenFileName(
|
|
self, "选择要执行的脚本", "", "Shell 脚本 (*.sh);;所有文件 (*)"
|
|
)
|
|
if file_path:
|
|
self.script_path_edit.setText(file_path)
|
|
|
|
def _run_script(self):
|
|
if not self._config_panel or not self._config_panel.validate():
|
|
return
|
|
local_path = self.script_path_edit.text().strip()
|
|
if not local_path:
|
|
QMessageBox.warning(self, "提示", "请选择要执行的脚本文件")
|
|
return
|
|
if not os.path.isfile(local_path):
|
|
QMessageBox.warning(self, "提示", f"本地脚本不存在: {local_path}")
|
|
return
|
|
|
|
self._cleanup_script()
|
|
self._set_ui_running(True)
|
|
self.tab_widget.setCurrentIndex(0) # 切换到任务日志 tab
|
|
self.log_panel.clear_log()
|
|
|
|
config = self._config_panel.get_config()
|
|
self._script_worker = ScriptWorker(config, local_path)
|
|
self._script_thread = QThread(self)
|
|
self._script_worker.moveToThread(self._script_thread)
|
|
|
|
self._script_worker.log_signal.connect(self.log_panel.append_log)
|
|
self._script_worker.finished_signal.connect(self._on_script_finished)
|
|
self._script_worker.error_signal.connect(self._on_script_error)
|
|
self._script_thread.started.connect(self._script_worker.run)
|
|
self._script_thread.finished.connect(self._cleanup_script)
|
|
|
|
self._script_thread.start()
|
|
|
|
def _on_script_finished(self, success: bool):
|
|
self._set_ui_running(False)
|
|
if success:
|
|
self.log_panel.append_log("✅ 脚本执行已完成", "success")
|
|
|
|
def _on_script_error(self, error_msg: str):
|
|
self._set_ui_running(False)
|
|
self.log_panel.append_log(f"❌ 脚本执行失败: {error_msg}", "error")
|
|
|
|
|
|
""" + old_mainwindow
|
|
|
|
content = content.replace(old_mainwindow, new_methods)
|
|
|
|
with open(gui_path, 'w', encoding='utf-8') as f:
|
|
f.write(content)
|
|
print('✅ gui.py 已集成脚本执行功能')
|