first commit
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
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 已集成脚本执行功能')
|
||||
@@ -0,0 +1,275 @@
|
||||
# This script adds ScriptWorker and a script execution tab to gui.py
|
||||
import os
|
||||
|
||||
gui_path = "src/virtualbox_client/gui.py"
|
||||
with open(gui_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# 1. Add ScriptWorker before WorkspaceWorker
|
||||
old_worker = "class WorkspaceWorker(QObject):"
|
||||
|
||||
new_worker = """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:
|
||||
import os as _os
|
||||
script_name = _os.path.basename(self.local_path)
|
||||
remote_path = "/tmp/" + script_name
|
||||
log_path = "/tmp/" + script_name + ".log"
|
||||
local_log_path = script_name + ".log"
|
||||
|
||||
self.log_signal.emit("[1/3] Upload: " + self.local_path + " -> " + remote_path, "info")
|
||||
if not vm.upload_file(self.local_path, remote_path):
|
||||
self.log_signal.emit("Upload failed", "error")
|
||||
return
|
||||
|
||||
self.log_signal.emit("[2/3] Execute: " + remote_path, "info")
|
||||
cmd = (
|
||||
"chmod +x " + remote_path + " && "
|
||||
"echo '" + self.config["ssh_pass"] + "' | sudo -S " + remote_path + " 2>&1 | "
|
||||
"tee " + log_path
|
||||
)
|
||||
out, err, code = vm.exec_command(cmd, get_pty=False)
|
||||
self.log_signal.emit("Exit code: " + str(code), "info")
|
||||
|
||||
self.log_signal.emit("[3/3] Download log: " + log_path + " -> " + local_log_path, "info")
|
||||
if vm.download_file(log_path, local_log_path):
|
||||
self.log_signal.emit("Log saved: " + local_log_path, "success")
|
||||
else:
|
||||
self.log_signal.emit("Tee log download failed, saving terminal output", "warning")
|
||||
if out:
|
||||
with open(local_log_path, "w") as f:
|
||||
f.write(out)
|
||||
self.log_signal.emit("Output saved: " + local_log_path, "success")
|
||||
|
||||
vm.exec_command("rm -f \\"" + remote_path + "\\" \\"" + log_path + "\\"")
|
||||
self.log_signal.emit("Remote temp files cleaned", "info")
|
||||
self.log_signal.emit("Script execution complete!", "success")
|
||||
self.finished_signal.emit(True)
|
||||
finally:
|
||||
builtins.print = original_print
|
||||
|
||||
except ImportError as e:
|
||||
self.log_signal.emit("Missing dependency: " + str(e), "error")
|
||||
self.error_signal.emit(str(e))
|
||||
except Exception as e:
|
||||
self.log_signal.emit("Script error: " + str(e), "error")
|
||||
self.error_signal.emit(str(e))
|
||||
finally:
|
||||
if _com_initialized:
|
||||
try:
|
||||
import ctypes
|
||||
ctypes.windll.ole32.CoUninitialize()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
""" + old_worker
|
||||
|
||||
content = content.replace(old_worker, new_worker)
|
||||
|
||||
# 2. Add script tab after file transfer tab
|
||||
old_tab_end = ' file_layout.addStretch()\n self.tab_widget.addTab(file_tab, "\U0001f4c1 \u6587\u4ef6\u4f20\u8f93")'
|
||||
|
||||
new_tab_end = ''' file_layout.addStretch()
|
||||
self.tab_widget.addTab(file_tab, "\U0001f4c1 \u6587\u4ef6\u4f20\u8f93")
|
||||
|
||||
# ====== Script Execution Tab ======
|
||||
script_tab = QWidget()
|
||||
script_layout = QVBoxLayout(script_tab)
|
||||
script_layout.setContentsMargins(8, 8, 8, 8)
|
||||
|
||||
script_group = QGroupBox("Upload and Execute Script")
|
||||
script_group_layout = QVBoxLayout(script_group)
|
||||
|
||||
script_path_layout = QHBoxLayout()
|
||||
self.script_path_edit = QLineEdit(placeholderText="Select local .sh script file...")
|
||||
script_path_layout.addWidget(self.script_path_edit)
|
||||
self.script_browse_btn = QPushButton("Browse")
|
||||
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("Local Script:"))
|
||||
script_group_layout.addLayout(script_path_layout)
|
||||
|
||||
self.run_script_btn = QPushButton("Upload and Execute Script")
|
||||
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("How it works")
|
||||
desc_layout = QVBoxLayout(desc_group)
|
||||
desc_label = QLabel(
|
||||
"1. Select a local .sh script file\\n"
|
||||
"2. Click button to auto:\\n"
|
||||
" Upload script to /tmp/\\n"
|
||||
" Execute with sudo\\n"
|
||||
" Download execution log\\n"
|
||||
" Clean remote temp files"
|
||||
)
|
||||
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, "Script")'''
|
||||
|
||||
content = content.replace(old_tab_end, new_tab_end)
|
||||
|
||||
# 3. Update _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. Add methods before MainWindow
|
||||
old_mw = "class MainWindow(QMainWindow):"
|
||||
|
||||
new_mw = """ _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, "Select script", "", "Shell scripts (*.sh);;All files (*)"
|
||||
)
|
||||
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, "Hint", "Please select a script file")
|
||||
return
|
||||
if not os.path.isfile(local_path):
|
||||
QMessageBox.warning(self, "Hint", "File not found: " + local_path)
|
||||
return
|
||||
|
||||
self._cleanup_script()
|
||||
self._set_ui_running(True)
|
||||
self.tab_widget.setCurrentIndex(0)
|
||||
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("Script execution completed", "success")
|
||||
|
||||
def _on_script_error(self, error_msg: str):
|
||||
self._set_ui_running(False)
|
||||
self.log_panel.append_log("Script error: " + error_msg, "error")
|
||||
|
||||
|
||||
""" + old_mw
|
||||
|
||||
content = content.replace(old_mw, new_mw)
|
||||
|
||||
with open(gui_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
print("Done")
|
||||
@@ -0,0 +1,18 @@
|
||||
import ast
|
||||
import sys
|
||||
|
||||
files = [
|
||||
"src/virtualbox_client/gui.py",
|
||||
"src/virtualbox_client/utils/vbox_ssh_manager.py",
|
||||
"src/virtualbox_client/utils/vm_agent.py",
|
||||
]
|
||||
|
||||
for f in files:
|
||||
try:
|
||||
tree = ast.parse(open(f, encoding="utf-8").read())
|
||||
print(f"OK: {f.split(chr(47))[-1]} ({len(tree.body)} nodes)")
|
||||
except SyntaxError as e:
|
||||
print(f"FAIL: {f.split(chr(47))[-1]}: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
print("All files pass syntax check")
|
||||
@@ -0,0 +1,13 @@
|
||||
import os
|
||||
import shutil
|
||||
|
||||
# 需要遍历的目录
|
||||
root_dir = "./"
|
||||
# 遍历目录
|
||||
for dirpath, dirnames, filenames in os.walk(root_dir):
|
||||
if "__pycache__" in dirnames:
|
||||
# 获取 __pycache__ 目录的全路径
|
||||
pycache_dir = os.path.join(dirpath, "__pycache__")
|
||||
# 删除目录
|
||||
shutil.rmtree(pycache_dir)
|
||||
print(f"Removed: {pycache_dir}")
|
||||
@@ -0,0 +1,45 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def generate_markdown_from_py_files(directory, output_file):
|
||||
with open(output_file, "w", encoding="utf-8") as md_file:
|
||||
for root, dirs, files in os.walk(directory):
|
||||
# 排除 venv 目录
|
||||
dirs[:] = [d for d in dirs if d != ".venv"]
|
||||
dirs[:] = [d for d in dirs if d != ".vscode"]
|
||||
dirs[:] = [d for d in dirs if d != "node_modules"]
|
||||
dirs[:] = [d for d in dirs if d != "build"]
|
||||
dirs[:] = [d for d in dirs if d != "dist"]
|
||||
dirs[:] = [d for d in dirs if d != ".sdk"]
|
||||
for file in files:
|
||||
if (
|
||||
file.endswith(".py")
|
||||
or file.endswith(".rs")
|
||||
or file.endswith(".toml")
|
||||
or file.endswith(".md")
|
||||
or file.endswith(".css")
|
||||
or file.endswith(".vue")
|
||||
or file.endswith(".ts")
|
||||
or file.endswith(".json")
|
||||
or file.endswith(".html")
|
||||
or file.endswith(".sh")
|
||||
or file == "Dockerfile"
|
||||
):
|
||||
file_path = os.path.join(root, file)
|
||||
if file == "output.md" or file == "package-lock.json":
|
||||
continue
|
||||
md_file.write(f"`{file_path}`\n")
|
||||
md_file.write("```\n")
|
||||
with open(file_path, "r", encoding="utf-8") as py_file:
|
||||
md_file.write(py_file.read())
|
||||
md_file.write("\n```\n\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 指定目录和输出文件名
|
||||
target_directory = sys.argv[1] # 替换为你的目标目录
|
||||
output_markdown_file = "output.md" # 输出的 Markdown 文件名
|
||||
|
||||
generate_markdown_from_py_files(target_directory, output_markdown_file)
|
||||
print(f"Markdown 文件已生成:{output_markdown_file}")
|
||||
@@ -0,0 +1,30 @@
|
||||
content = open("src/virtualbox_client/gui.py", encoding="utf-8").read()
|
||||
|
||||
# 1. 修复 except 语法
|
||||
content = content.replace(
|
||||
"except TypeError, RuntimeError:", "except (TypeError, RuntimeError):"
|
||||
)
|
||||
|
||||
# 2. 修复 _rename_item 中的路径分隔符
|
||||
old = """ dst_dir = os.path.dirname(src_path)
|
||||
dst_path = os.path.join(dst_dir, new_name.strip())"""
|
||||
new = """ dst_dir = src_path.rsplit('/', 1)[0] if '/' in src_path else src_path.rsplit('\\\\', 1)[0]
|
||||
dst_path = dst_dir.rstrip('/') + '/' + new_name.strip()"""
|
||||
content = content.replace(old, new)
|
||||
|
||||
# 3. 修复 _copy_item 中的路径分隔符
|
||||
old2 = """ dst_dir = os.path.dirname(src_path)
|
||||
dst_path = os.path.join(dst_dir, new_name)"""
|
||||
new2 = """ dst_dir = src_path.rsplit('/', 1)[0] if '/' in src_path else src_path.rsplit('\\\\', 1)[0]
|
||||
dst_path = dst_dir.rstrip('/') + '/' + new_name"""
|
||||
content = content.replace(old2, new2)
|
||||
|
||||
# 4. 修复 _upload_file 中的 remote_dir 路径
|
||||
old3 = """ remote_dir = os.path.dirname(remote_path)
|
||||
vm.exec_command(f'mkdir -p "{remote_dir}"')"""
|
||||
new3 = """ remote_dir = remote_path.rsplit('/', 1)[0] if '/' in remote_path else remote_path.rsplit('\\\\', 1)[0]
|
||||
vm.exec_command(f'mkdir -p "{remote_dir}"')"""
|
||||
content = content.replace(old3, new3)
|
||||
|
||||
open("src/virtualbox_client/gui.py", "w", encoding="utf-8").write(content)
|
||||
print("✅ 所有修复完成")
|
||||
@@ -0,0 +1,226 @@
|
||||
import os
|
||||
|
||||
# 1. 给 SSHManager 添加 upload_directory 方法
|
||||
ssh_path = "src/virtualbox_client/utils/ssh_manager.py"
|
||||
with open(ssh_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
old_method = """ def upload_file(self, local_path, remote_path):
|
||||
\"\"\"上传本地文件到远程服务器,返回是否成功\"\"\"
|
||||
client = self.get_ssh_client()
|
||||
if not client:
|
||||
return False
|
||||
try:
|
||||
sftp = client.open_sftp()
|
||||
sftp.put(local_path, remote_path)
|
||||
sftp.close()
|
||||
print(f\"✔ 上传成功: {local_path} -> {remote_path}\")
|
||||
return True
|
||||
except Exception as e:
|
||||
if self._cancelled.is_set():
|
||||
return False
|
||||
print(f\"✘ 上传失败: {e}\")
|
||||
return False
|
||||
"""
|
||||
|
||||
new_method = """ def upload_file(self, local_path, remote_path):
|
||||
\"\"\"上传本地文件到远程服务器,返回是否成功\"\"\"
|
||||
client = self.get_ssh_client()
|
||||
if not client:
|
||||
return False
|
||||
try:
|
||||
sftp = client.open_sftp()
|
||||
sftp.put(local_path, remote_path)
|
||||
sftp.close()
|
||||
print(f\"✔ 上传成功: {local_path} -> {remote_path}\")
|
||||
return True
|
||||
except Exception as e:
|
||||
if self._cancelled.is_set():
|
||||
return False
|
||||
print(f\"✘ 上传失败: {e}\")
|
||||
return False
|
||||
|
||||
def upload_directory(self, local_dir, remote_dir):
|
||||
\"\"\"递归上传整个文件夹到远程服务器,返回是否成功\"\"\"
|
||||
import os as _os
|
||||
client = self.get_ssh_client()
|
||||
if not client:
|
||||
return False
|
||||
try:
|
||||
sftp = client.open_sftp()
|
||||
local_dir = _os.path.normpath(local_dir)
|
||||
remote_dir = remote_dir.rstrip('/')
|
||||
# 创建远程根目录
|
||||
self.exec_command(f'mkdir -p \"{remote_dir}\"')
|
||||
for root, dirs, files in _os.walk(local_dir):
|
||||
# 计算相对路径
|
||||
rel_path = _os.path.relpath(root, local_dir)
|
||||
if rel_path == '.':
|
||||
rel_path = ''
|
||||
# 创建远程子目录
|
||||
target_dir = remote_dir + ('/' + rel_path.replace('\\\\', '/') if rel_path else '')
|
||||
self.exec_command(f'mkdir -p \"{target_dir}\"')
|
||||
for fname in files:
|
||||
if self._cancelled.is_set():
|
||||
sftp.close()
|
||||
return False
|
||||
local_file = _os.path.join(root, fname)
|
||||
remote_file = target_dir + '/' + fname
|
||||
sftp.put(local_file, remote_file)
|
||||
print(f\" ✔ {local_file} -> {remote_file}\")
|
||||
sftp.close()
|
||||
print(f\"✔ 文件夹上传成功: {local_dir} -> {remote_dir}\")
|
||||
return True
|
||||
except Exception as e:
|
||||
if self._cancelled.is_set():
|
||||
return False
|
||||
print(f\"✘ 文件夹上传失败: {e}\")
|
||||
return False
|
||||
"""
|
||||
|
||||
content = content.replace(old_method, new_method)
|
||||
with open(ssh_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
print("✅ SSHManager 已添加 upload_directory")
|
||||
|
||||
# 2. 给 VBoxSSHManager 添加 upload_directory 方法
|
||||
vbox_path = "src/virtualbox_client/utils/vbox_ssh_manager.py"
|
||||
with open(vbox_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
content = content.replace(old_method, new_method)
|
||||
with open(vbox_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
print("✅ VBoxSSHManager 已添加 upload_directory")
|
||||
|
||||
# 3. 修改 gui.py - 添加上传文件夹功能
|
||||
gui_path = "src/virtualbox_client/gui.py"
|
||||
with open(gui_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# 在 WorkspaceWorker 中添加 OPERATION_UPLOAD_DIR
|
||||
old_op = ' OPERATION_UPLOAD = "upload"\n'
|
||||
new_op = ' OPERATION_UPLOAD = "upload"\n OPERATION_UPLOAD_DIR = "upload_dir"\n'
|
||||
content = content.replace(old_op, new_op)
|
||||
|
||||
# 在 run 方法中添加分支
|
||||
old_run = """ elif self.operation == WorkspaceWorker.OPERATION_UPLOAD:
|
||||
result = self._upload_file(vm)"""
|
||||
new_run = """ elif self.operation == WorkspaceWorker.OPERATION_UPLOAD:
|
||||
result = self._upload_file(vm)
|
||||
elif self.operation == WorkspaceWorker.OPERATION_UPLOAD_DIR:
|
||||
result = self._upload_directory(vm)"""
|
||||
content = content.replace(old_run, new_run)
|
||||
|
||||
# 在 _upload_file 之后添加 _upload_directory 方法
|
||||
old_upload = """ def _upload_file(self, vm) -> bool:
|
||||
local_path = self.kwargs.get("local_path", "")
|
||||
remote_path = self.kwargs.get("remote_path", "")
|
||||
if not local_path or not remote_path:
|
||||
return False
|
||||
remote_dir = remote_path.rsplit('/', 1)[0] if '/' in remote_path else remote_path.rsplit('\\\\', 1)[0]
|
||||
vm.exec_command(f'mkdir -p "{remote_dir}"')
|
||||
return vm.upload_file(local_path, remote_path)"""
|
||||
|
||||
new_upload = """ def _upload_file(self, vm) -> bool:
|
||||
local_path = self.kwargs.get("local_path", "")
|
||||
remote_path = self.kwargs.get("remote_path", "")
|
||||
if not local_path or not remote_path:
|
||||
return False
|
||||
remote_dir = remote_path.rsplit('/', 1)[0] if '/' in remote_path else remote_path.rsplit('\\\\', 1)[0]
|
||||
vm.exec_command(f'mkdir -p "{remote_dir}"')
|
||||
return vm.upload_file(local_path, remote_path)
|
||||
|
||||
def _upload_directory(self, vm) -> bool:
|
||||
local_dir = self.kwargs.get("local_path", "")
|
||||
remote_dir = self.kwargs.get("remote_path", "")
|
||||
if not local_dir or not remote_dir:
|
||||
return False
|
||||
return vm.upload_directory(local_dir, remote_dir)"""
|
||||
|
||||
content = content.replace(old_upload, new_upload)
|
||||
|
||||
# 修改 _on_worker_finished 添加 UPLOAD_DIR 处理
|
||||
old_finish = """ elif (
|
||||
self._worker and self._worker.operation == WorkspaceWorker.OPERATION_UPLOAD
|
||||
):
|
||||
if result:
|
||||
self.log_panel.append_log("✅ 上传成功", "success")
|
||||
else:
|
||||
self.log_panel.append_log("❌ 上传失败", "error")
|
||||
QTimer.singleShot(500, self._soft_refresh)"""
|
||||
|
||||
new_finish = """ elif (
|
||||
self._worker
|
||||
and self._worker.operation
|
||||
in (WorkspaceWorker.OPERATION_UPLOAD, WorkspaceWorker.OPERATION_UPLOAD_DIR)
|
||||
):
|
||||
if result:
|
||||
self.log_panel.append_log("✅ 上传成功", "success")
|
||||
else:
|
||||
self.log_panel.append_log("❌ 上传失败", "error")
|
||||
QTimer.singleShot(500, self._soft_refresh)"""
|
||||
|
||||
content = content.replace(old_finish, new_finish)
|
||||
|
||||
# 在 WorkspacePanel.setup_ui 的工具栏中添加"上传文件夹"按钮
|
||||
old_upload_btn = """ self.upload_btn = QPushButton("📤 上传文件")"""
|
||||
new_upload_btn = """ self.upload_btn = QPushButton("📤 上传文件")"""
|
||||
# 保持原样,在后面添加新按钮
|
||||
# 在 upload_btn 后面添加 upload_dir_btn
|
||||
old_toolbar = """ toolbar.addWidget(self.upload_btn)
|
||||
|
||||
self.mkdir_btn = QPushButton("📂 新建文件夹")"""
|
||||
new_toolbar = """ toolbar.addWidget(self.upload_btn)
|
||||
|
||||
self.upload_dir_btn = QPushButton("📁 上传文件夹")
|
||||
self.upload_dir_btn.setStyleSheet(\"\"\"
|
||||
QPushButton { background-color: #16a085; color: white; padding: 6px 14px;
|
||||
font-size: 12px; font-weight: bold; border: none; border-radius: 4px; }
|
||||
QPushButton:hover { background-color: #1abc9c; }
|
||||
QPushButton:disabled { background-color: #555; color: #999; }
|
||||
\"\"\")
|
||||
self.upload_dir_btn.clicked.connect(self._upload_folder_to_workspace)
|
||||
toolbar.addWidget(self.upload_dir_btn)
|
||||
|
||||
self.mkdir_btn = QPushButton("📂 新建文件夹")"""
|
||||
content = content.replace(old_toolbar, new_toolbar)
|
||||
|
||||
# 更新 _set_busy 禁用 upload_dir_btn
|
||||
old_set_busy = """ self.upload_btn.setEnabled(not busy)
|
||||
self.mkdir_btn.setEnabled(not busy)
|
||||
self.home_btn.setEnabled(not busy)"""
|
||||
new_set_busy = """ self.upload_btn.setEnabled(not busy)
|
||||
self.upload_dir_btn.setEnabled(not busy)
|
||||
self.mkdir_btn.setEnabled(not busy)
|
||||
self.home_btn.setEnabled(not busy)"""
|
||||
content = content.replace(old_set_busy, new_set_busy)
|
||||
|
||||
# 在 _upload_to_workspace 方法之后添加 _upload_folder_to_workspace
|
||||
old_upload_workspace = """ def _upload_to_workspace(self):"""
|
||||
|
||||
new_upload_workspace = """ def _upload_folder_to_workspace(self):
|
||||
if not self._config_panel or not self._validate_ssh_config():
|
||||
return
|
||||
|
||||
local_dir = QFileDialog.getExistingDirectory(self, "选择要上传到工作区的文件夹")
|
||||
if not local_dir:
|
||||
return
|
||||
|
||||
folder_name = os.path.basename(local_dir.rstrip('/\\\\'))
|
||||
remote_path = f"~/workspace/{folder_name}"
|
||||
self._start_worker(
|
||||
WorkspaceWorker.OPERATION_UPLOAD_DIR,
|
||||
local_path=local_dir,
|
||||
remote_path=remote_path,
|
||||
)
|
||||
|
||||
def _upload_to_workspace(self):"""
|
||||
|
||||
content = content.replace(old_upload_workspace, new_upload_workspace)
|
||||
|
||||
with open(gui_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
print("✅ gui.py 已添加上传文件夹功能")
|
||||
|
||||
print("\\n所有修改完成!")
|
||||
Reference in New Issue
Block a user