diff --git a/ui/main_window.py b/ui/main_window.py index 0a4291d..1b5e4c1 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -183,9 +183,17 @@ class MainWindow(QMainWindow): self.btn_clear_out.clicked.connect(self.output.clear) self.btn_copy = QPushButton("复制输出") self.btn_copy.clicked.connect(lambda: QApplication.clipboard().setText(self.output.toPlainText())) + self.btn_debug = QPushButton("🔍 诊断") + self.btn_debug.setToolTip("打印 SSH 连接和 worker 状态") + self.btn_debug.clicked.connect(self._debug_state) + self.btn_test = QPushButton("▶ 测试命令") + self.btn_test.setToolTip("执行 'echo hello-sshclient && pwd && uname -a' 验证连通性") + self.btn_test.clicked.connect(lambda: self.cmd_input.setText("echo hello-sshclient && pwd && uname -a")) bottom.addWidget(self.btn_clear_out) bottom.addWidget(self.btn_copy) + bottom.addWidget(self.btn_debug) bottom.addStretch(1) + bottom.addWidget(self.btn_test) v.addLayout(bottom) return w @@ -364,31 +372,71 @@ class MainWindow(QMainWindow): if not conn or not conn.connected: QMessageBox.warning(self, "提示", "当前主机未连接") return - self._append_out(f"\n$ {cmd}\n") + # 先在输出区显示命令(即使后续 worker 失败也能看到用户输入了啥) + self._append_out(f"\n$ {cmd}") self.cmd_input.clear() self.btn_run.setEnabled(False) + self.btn_run.setText("执行中...") timeout = self.timeout_spin.value() self.cmd_worker = CommandWorker(conn, cmd, timeout) self.cmd_worker.finished_with.connect(self._on_cmd_done) self.cmd_worker.start() + # 强制刷新滚动到底 + self.output.repaint() def _on_cmd_done(self, code: int, out: str, err: str): self.btn_run.setEnabled(True) + self.btn_run.setText("执行") if out: self._append_out(out) + else: + self._append_out("(stdout 为空)") if err: self._append_out_err(err) - self._append_out(f"[exit={code}]\n") + self._append_out(f"[exit={code}]") self.statusBar().showMessage(f"命令完成 (退出码 {code})", 3000) + # 强制刷新 + 滚到底 + self.output.repaint() def _append_out(self, text: str): - self.output.appendPlainText(text.rstrip("\n")) + # 不再 rstrip("\n"),保留原样;appendPlainText 本身会处理换行 + self.output.appendPlainText(text) + # 滚到底 + sb = self.output.verticalScrollBar() + sb.setValue(sb.maximum()) + self.output.ensureCursorVisible() + + def _append_out_err(self, text: str): + self.output.appendPlainText("[ERR] " + text.replace("\n", "\n[ERR] ")) sb = self.output.verticalScrollBar() sb.setValue(sb.maximum()) - def _append_out_err(self, text: str): - # 简单的 ANSI/颜色:stderr 用红色(PlainTextEdit 不支持富文本,所以加 [ERR] 前缀) - self.output.appendPlainText("[ERR] " + text.rstrip("\n").replace("\n", "\n[ERR] ")) + def _debug_state(self): + """诊断按钮:打印当前 SSH 连接和 worker 状态""" + lines = ["=== Debug State ==="] + lines.append(f"current_host_id: {self.current_host_id}") + if self.current_host_id: + h = self.manager.get_host(self.current_host_id) + lines.append(f"host config: {h}") + conn = self.manager.get_connection(self.current_host_id) + if conn: + lines.append(f"conn.connected: {conn.connected}") + lines.append(f"conn.client: {conn.client}") + lines.append(f"conn.sftp: {conn.sftp}") + if conn.client: + try: + transport = conn.client.get_transport() + lines.append(f"transport active: {transport.is_active() if transport else 'None'}") + except Exception as e: + lines.append(f"transport error: {e}") + else: + lines.append("conn: None (not connected)") + lines.append(f"cmd_worker: {self.cmd_worker}") + if self.cmd_worker: + lines.append(f"cmd_worker.isRunning: {self.cmd_worker.isRunning()}") + lines.append("=== End Debug ===") + self._append_out("\n".join(lines)) + self.statusBar().showMessage("诊断信息已输出", 3000) # ============================================================ # AI / 关于 diff --git a/ui/workers.py b/ui/workers.py index d31bd35..1e90273 100644 --- a/ui/workers.py +++ b/ui/workers.py @@ -40,9 +40,13 @@ class CommandWorker(QThread): def run(self): try: + if not self.conn or not self.conn.connected or not self.conn.client: + self.finished_with.emit(-1, "", "连接已断开或 client 不可用") + return code, out, err = self.conn.exec_command(self.command, timeout=self.timeout) except Exception as e: - code, out, err = -1, "", str(e) + import traceback + code, out, err = -1, "", f"执行异常: {e}\n{traceback.format_exc()}" self.finished_with.emit(code, out, err)