软件测试学习 之 Python os._exit()&sys.exit()、exit(0)&exit(1) 的用法和区别

本文深入解析Python中的os._exit()与sys.exit()两种程序退出方式的用法与区别,包括它们如何影响程序流程及异常处理机制。同时,对比了exit(0)与exit(1)在退出状态上的差异。
部署运行你感兴趣的模型镜像

转载说明:
-------------------- 
python中 os._exit() 和 sys.exit(), exit(0)和exit(1) 的用法和区别
作者:everest33
出处:博客园
-------------------- 

目录

概述

Python的程序有两中退出方式:os._exit()sys.exit()。本文主要介绍这两种方式的区别和选择。

os._exit() vs sys.exit()

os._exit()会直接将python程序终止,之后的所有代码都不会继续执行。

sys.exit()会引发一个异常:SystemExit,如果这个异常没有被捕获,那么python解释器将会退出。如果有捕获此异常的代码,那么这些代码还是会执行。捕获这个异常可以做一些额外的清理工作。0为正常退出,其他数值(1-127)为不正常,可抛异常事件供捕获。

举例说明

os._exit()
print("os._exit(0):")
try:
    os._exit(0)
    # pass
except BaseException as ex:
    print(ex)
    print('die')
finally:
    print('cleanup')

pycharm编辑器中,os._exit(0)处会有告警:
Access to a protected member _exit of a module
(访问了模块受保护的成员_exit

并且,结果不会打出”die”,也不会打印“cleanup

os._exit(0):
sys._exit()
print("sys.exit(0):")
try:
    sys.exit(0)
except SystemExit as sys_exit:
    print(sys_exit)
    print('die')
finally:
    print('cleanup')

执行结果:

sys.exit(0):
0
die
cleanup

os._exit()和sys._exit()的区别

综上,

  • sys.exit()的退出比较优雅,调用后会引发SystemExit异常,可以捕获此异常做清理工作。
    os._exit()直接将python解释器退出,余下的语句不会执行。
  • 一般情况下使用sys.exit()即可,一般在fork出来的子进程中使用os._exit()
    一般来说,os._exit()用于在线程中退出sys.exit()用于在主线程中退出。
  • exit()跟 C 语言等其他语言的exit()应该是一样的。 os._exit()调用C语言的_exit()函数。
  • builtin.exit是一个Quitter对象,这个对象的call方法会抛出一个SystemExit异常。

exit(0) vs exit(1)

exit(0):无错误退出
exit(1):有错误退出
退出代码是告诉解释器的(或操作系统)
实际执行结果与sys.exit()类似,同样会抛出SystemExit异常

print("exit(0):")
try:
    exit(0)
except SystemExit as sys_exit:
    print(sys_exit)
    print('die')
finally:
    print('cleanup')
print()

print("exit(1):")
try:
    exit(1)
except SystemExit as sys_exit:
    print(sys_exit)
    print('die')
finally:
    print('cleanup')

执行结果:

exit(0):
0
die
cleanup

exit(1):
1
die
cleanup

您可能感兴趣的与本文相关的镜像

Python3.10

Python3.10

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

import os import datetime import shutil import subprocess import re import platform import hashlib def get_commit_id(): cmd = ['git', 'rev-parse', 'HEAD'] try: result = subprocess.check_output(cmd) commit_id = result.decode().strip() if not commit_id: raise Exception('commit id not found') return commit_id except Exception as e: print(e) raise e def remove_pycache(path): for root, dirs, files in os.walk(path): for dir in dirs: if dir == '__pycache__': pycache_dir = os.path.join(root, dir) shutil.rmtree(pycache_dir) def use_shell(): if platform.system() == "Linux": return False return True class SdkPacker: def __init__(self): self.starttime = datetime.datetime.now() self.record('*' * 10 + 'SDK pack init' + '*' * 10) # pip self.pipUrl = os.getenv('SDK_PIP_URL') self.pipTrustHost = os.getenv('SDK_PIP_TRUST_HOST') self.pipArgs = os.getenv('SDK_PIP_ARGS') self.record(f'pipUrl: {self.pipUrl}') self.record(f'pipTrustHost: {self.pipTrustHost}') self.record(f'pipArgs: {self.pipArgs}') # sdk path self.sdkPath = os.path.dirname(os.path.abspath(__file__)) self.outPath = os.path.join(self.sdkPath, 'out') self.enginePath = os.path.join(self.outPath, 'engine') self.remove_path(os.path.join(self.sdkPath, 'build')) self.remove_path(os.path.join(self.sdkPath, 'rpa', 'build')) self.remove_path(self.enginePath) # commit id self.commitId = get_commit_id() self.record(f"commit id:{self.commitId}") # cache path self.cachePath = os.path.join(self.sdkPath, 'out', 'cache') self.cacheZipPath = os.path.join(self.cachePath, f'{self.commitId}.7z') # env self.RPA_PACK_PLATFORM = self.get_env('RPA_PACK_PLATFORM') self.RPA_PACK_ARCH = self.get_env('RPA_PACK_ARCH') self.RPA_VERSION = self.get_env('RPA_PACK_VERSION') if not self.RPA_VERSION or not re.search(r"\d", self.RPA_VERSION): self.RPA_VERSION = "15.0.0" self.RPA_FORCE_REBUILD = self.get_env('RPA_PACK_FORCE_REBUILD') self.platform = platform.system() self.record(f"System: {self.platform}") # tools path self.sdkToolsPath = os.path.join(self.sdkPath, 'out', 'sdk_tools') # output path self.python_out = os.path.join(self.enginePath) if self.RPA_PACK_PLATFORM == 'windows': self.reqsPath = os.path.join(self.sdkPath, 'rpa', 'requirements.txt') self.site_packages = os.path.join(self.enginePath, 'Lib', 'site-packages') elif self.RPA_PACK_PLATFORM in ['linux', 'UOS', 'kylinOS']: self.reqsPath = os.path.join(self.sdkPath, 'rpa', 'requirements_uos.txt') self.site_packages = os.path.join(self.enginePath, 'lib', 'python3.7', 'site-packages') else: raise Exception(f'not support platform: {self.RPA_PACK_PLATFORM} and arch: {self.RPA_PACK_ARCH}') self.seven_zip_out = os.path.join(self.site_packages, 'rpa', 'file_folder') self.ffmpeg_out = os.path.join(self.site_packages, 'rpa', 'win32') self.db2_out = os.path.join(self.site_packages) self.db2_cli_out = os.path.join(self.site_packages, 'ibm_db-3.1.4') self.pip_args = [] if self.pipUrl: self.pip_args.extend(['-i', self.pipUrl]) if self.pipTrustHost: self.pip_args.extend(['--trusted-host', self.pipTrustHost]) if self.pipArgs: self.pip_args.extend(self.pipArgs.split(',')) # self.pip_args.extend(['--no-cache-dir', '--no-warn-script-location']) self.record("sdk pack init end") def run_command(self, command, cwd=None): process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=cwd, shell=use_shell()) while True: output = process.stdout.readline() if output == b'' and process.poll() is not None: break if output: print(output.strip().decode('utf-8', errors='replace')) process.wait() if process.returncode is not 0: raise Exception(f'run command {command} error, return code: {process.returncode}') self.record(f"run command: {command}") def get_env(self, env_key): env_value = os.getenv(env_key) if env_value: self.record(f'{env_key}: {env_value}') else: raise Exception(f'{env_key} not found') return env_value def remove_path(self, path): if os.path.exists(path): try: if os.path.isfile(path): os.remove(path) elif os.path.isdir(path): shutil.rmtree(path) self.record(f"remove {path}: successfully") except Exception as e: self.record(f'remove {path}: {e} error') raise e else: self.record(f"remove {path}: not exists") def record(self, title): end_time = datetime.datetime.now() diff = (end_time - self.starttime) print(f"[{end_time.time()} - {diff.seconds}] {title}") def unzip(self, src, dst): # self.run_command(['7z', 'x', src, '-o' + dst, '-bb0'], cwd=os.path.join(self.sdkToolsPath, '7z')) os.system(f"7z x {src} -o{dst}") self.record(f"unzip {src} to {dict}") def calculate_md5(self, file_path): with open(file_path, "rb") as f: md5_hash = hashlib.md5() for chunk in iter(lambda: f.read(4096), b""): md5_hash.update(chunk) return md5_hash.hexdigest() def copy(self, package_name, *rpa_dir): package = os.path.join(self.sdkToolsPath, package_name) package_out = os.path.join(self.site_packages, 'rpa', *rpa_dir) for file in os.listdir(package): package_file = os.path.join(package, file) shutil.copy(package_file, package_out) self.record(f"{package_file} >> {package_out}") self.record(f"copy {package_name}") def pack(self): # encrypt sdk self.record('*' * 10 + 'SDK pack' + '*' * 10) if self.RPA_FORCE_REBUILD == 'false' and os.path.exists(self.cacheZipPath): self.record('SDK use cache') self.unzip(self.cacheZipPath, self.outPath) else: self.encrypt_sdk() # add version version_path = os.path.join(self.site_packages, 'rpa', 'version', 'version') content = f'{self.RPA_VERSION}\n{datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S")}\n{self.commitId}' with open(version_path, 'w') as f: f.write(content) shutil.copy(version_path, self.enginePath) with open(os.path.join(self.enginePath, 'version'), 'r') as f: self.record(f.read()) # remove cache remove_pycache(self.enginePath) self.record(f"remove engine pycache: {self.enginePath}") self.record("SDK pack end") def link_python(self): pass def is_linux(self): return self.RPA_PACK_PLATFORM in ['linux', 'UOS', 'kylinOS'] def is_windows(self): return self.RPA_PACK_PLATFORM == 'windows' def encrypt_sdk(self): def fix_tk(): # remove mouseinfo tk sys.exit mouseinfo_init_path = os.path.join(self.site_packages, 'mouseinfo', '__init__.py') command = """sed -i "s/sys.exit.*/pass/g" """ + mouseinfo_init_path ret = os.system(command) msg = f"remove mouseinfo tk sys.exit code: {ret}" if ret: raise SystemError(msg) self.record(msg) def install_pywpsrpc(): wheel_path = 'pywpsrpc-2.3.3-cp37-cp37m-manylinux_2_5_x86_64.whl' if self.RPA_PACK_ARCH == "arm64": wheel_path = 'pywpsrpc-2.3.3-cp37-cp37m-manylinux_2_28_aarch64.whl' pywpsrpc_path = os.path.join(self.sdkToolsPath, 'pywpsrpc', wheel_path) self.run_command([python_path, '-m', 'pip', 'install', pywpsrpc_path]) def copy_depends(): shutil.copytree(os.path.join(self.sdkToolsPath, 'deps', 'at-spi2-core'), os.path.join(self.enginePath, 'deps', 'at-spi2-core')) shutil.copytree(os.path.join(self.sdkToolsPath, 'deps', 'wps'), os.path.join(self.enginePath, 'deps', 'wps')) shutil.copytree(os.path.join(self.sdkToolsPath, 'deps', 'xclip'), os.path.join(self.site_packages, 'xclip')) # move python self.record('SDK encrypt') use_cache = False requirements_md5 = self.calculate_md5(self.reqsPath) requirements_cache_path = os.path.join(self.sdkPath, 'out', 'cache', f'{requirements_md5}.7z') if self.RPA_FORCE_REBUILD == 'false': if os.path.exists(requirements_cache_path): use_cache = True python_source = "" python_path = "" if self.is_windows(): python_source = os.path.join(self.sdkToolsPath, 'python') python_path = os.path.join(self.enginePath, "python.exe") elif self.is_linux(): python_source = "/opt/python3.7" python_path = os.path.join(self.enginePath, "bin", "python") if not use_cache: shutil.copytree(python_source, self.enginePath) self.record(f"{python_source} >> {self.enginePath}") if self.is_linux(): os.system(f'apt-get install -y libcairo2-dev libgirepository1.0-dev unixodbc-dev') current_cwd = os.getcwd() self.record(f"current cwd:{current_cwd}") bin_path = os.path.join(self.enginePath, "bin") os.chdir(bin_path) os.system(f'ln -s python3.7 python') os.system(f'ln -s python3.7 python3') self.record("link python3.7 to python python3") os.chdir(current_cwd) # install requirements # comtypes<1.1.11 need 2to3, setuptools<58 support 2to3 self.run_command([python_path, '-m', 'pip', 'install', '--upgrade', 'pip'] + self.pip_args) self.run_command([python_path, '-m', 'pip', 'install', '--upgrade', 'setuptools < 58'] + self.pip_args) self.run_command([python_path, '-m', 'pip', 'install', '-r', self.reqsPath] + self.pip_args) if self.is_windows(): # install db2 shutil.copytree(os.path.join(self.sdkToolsPath, 'db2', 'ibm_db-3.1.4'), os.path.join(self.enginePath, 'Lib', 'site-packages', 'ibm_db-3.1.4')) shutil.copytree(os.path.join(self.sdkToolsPath, 'db2_cli', 'clidriver'), os.path.join(self.enginePath, 'Lib', 'site-packages', 'ibm_db-3.1.4', 'clidriver')) self.run_command([os.path.join(self.enginePath, 'python'), 'setup.py', 'install'], cwd=os.path.join(self.enginePath, 'Lib', 'site-packages', 'ibm_db-3.1.4')) elif self.is_linux(): # install db2 # shutil.copytree(os.path.join(self.sdkToolsPath, 'db2', 'ibm_db-3.1.4'), # os.path.join(self.site_packages, 'ibm_db-3.1.4')) # shutil.copytree(os.path.join(self.sdkToolsPath, 'db2_cli', 'clidriver'), # os.path.join(self.site_packages, 'ibm_db-3.1.4', 'clidriver')) # self.run_command([python_path, 'setup.py', 'install'], # cwd=os.path.join(self.site_packages, 'ibm_db-3.1.4')) fix_tk() install_pywpsrpc() copy_depends() # install cython self.run_command([python_path, '-m', 'pip', 'install', 'cython==0.29.24'] + self.pip_args) self.remove_path(requirements_cache_path) self.run_command(['7z', 'a', '-mx1', requirements_cache_path, self.enginePath], cwd=os.path.join(self.sdkToolsPath, '7z')) else: self.record("requirements use cache") self.unzip(requirements_cache_path, self.outPath) build_path = os.path.join(self.sdkPath, 'build', 'rpa') # encrypt sdk self.run_command([python_path, 'setup.py'], cwd=os.path.join(self.sdkPath, 'rpa')) # uninstall cython self.run_command([python_path, '-m', 'pip', 'uninstall', 'cython', '-y']) # remove pycache remove_pycache(build_path) self.record(f"remove rpa pycache: {build_path}") # copy sdk rpa_path = os.path.join(self.site_packages, 'rpa') shutil.move(build_path, rpa_path) self.record(f"move {build_path} >> {rpa_path}") if self.RPA_PACK_PLATFORM == 'windows': self.copy('activexinput', 'uia', 'activexinput') self.copy("7z", "file_folder") self.copy("ffmpeg", "win32") # save cache self.remove_path(self.cacheZipPath) self.run_command(['7z', 'a', '-mx1', self.cacheZipPath, self.enginePath], cwd=os.path.join(self.sdkToolsPath, '7z')) # self.run_command(['7z', 'a', '-tzip', self.cacheZipPath, '-r', self.enginePath, '-y', '-bb0'], # cwd=os.path.join(self.sdkToolsPath, '7z')) # remove paths self.remove_path(os.path.join(self.sdkPath, 'build')) self.remove_path(build_path) self.record("SDK encrypt end") if __name__ == '__main__': import sys if sys.platform == "win32": # for tests os.environ['RPA_PACK_PLATFORM'] = 'windows' os.environ['RPA_PACK_ARCH'] = 'x64' os.environ['RPA_TARGET_FORMAT'] = 'zip' os.environ['RPA_PACK_GITOKEN'] = 'pack_gitoken' os.environ['RPA_VERSION'] = '1.0.0' os.environ['RPA_GIT_TOKEN'] = 'git_token' os.environ['RPA_FORCE_REBUILD'] = 'false' os.environ['RPA_TOOLS_HOME'] = 'C:\\Repos\\tools' elif sys.platform == "darwin": sys.exit(0) else: os.environ['RPA_PACK_PLATFORM'] = 'linux' os.environ['RPA_PACK_ARCH'] = 'x64' os.environ['RPA_TARGET_FORMAT'] = 'deb' os.environ['RPA_PACK_GITOKEN'] = 'pack_gitoken' os.environ['RPA_VERSION'] = '1.0.0' os.environ['RPA_GIT_TOKEN'] = 'git_token' os.environ['RPA_FORCE_REBUILD'] = 'false' os.environ['RPA_TOOLS_HOME'] = '/home/uos/tools' os.environ['SDK_PIP_URL'] = 'https://repo.datagrand.com/repository/py/simple' packer = SdkPacker() packer.pack() 分解一下项目打包的流程,介绍如何实现打包,如何配置环境,依赖等等,以及是否实现可执行文件,将整个流程用图表表示
07-10
刚才复测是没有问题的,以下是main.py的完整代码: # 主程序入口 import sys import os import logging import tracemalloc from logging import Logger from PyQt6.QtWidgets import QApplication, QMessageBox from src.auto_print_system.ui.main_window import MainWindow #强制使用软件渲染 os.environ["QT_OPENGL"] = "software" # 强制软件渲染 os.environ["QT_QPA_PLATFORM"] = "windows:dxangle" # 使用 DirectX 角度后端 os.environ["QT_DEBUG_BACKINGSTORE"] = "1" # 启用调试输出 if __name__ == "__main__": tracemalloc.start() # 新增内存跟踪初始化 if __name__ != "__main__": pass else: # 设置资源路径 tracemalloc.start() def setup_logging(app_data_path): # ... 日志设置 ... def main(): # 启动内存跟踪 tracemalloc.start() logging.info("已启动 tracemalloc 内存跟踪") app = QApplication(sys.argv) try: logging.info("正在创建主窗口...") window = MainWindow(data_path=app_data_path) logging.info("主窗口创建成功") window.show() logging.info("主窗口已显示") logging.info("进入应用主循环") sys.exit(app.exec()) except Exception as e: logging.exception("应用程序发生未捕获的异常") # 显示错误消息框 from PyQt6.QtWidgets import QMessageBox error_box = QMessageBox.critical( None, "致命错误", f"应用程序遇到致命错误:\n{str(e)}\n\n详细日志请查看应用程序日志文件", QMessageBox.StandardButton.Ok ) sys.exit(1) #添加更详细的错误捕获,解决初始化后闪退问题 def main(): try: app = QApplication(sys.argv) # 安装全局异常钩子 sys.excepthook = handle_exception # ... [窗口创建代码] ... except Exception as e: logging.exception("主函数捕获到异常") show_error_dialog(f"主函数异常: {str(e)}") sys.exit(1) def handle_exception(exc_type, exc_value, exc_traceback): """处理未捕获的异常""" logging.error("未捕获的异常", exc_info=(exc_type, exc_value, exc_traceback)) show_error_dialog(f"未捕获的异常: {str(exc_value)}") sys.exit(1) def show_error_dialog(message): """显示错误对话框""" app = QApplication.instance() or QApplication(sys.argv) error_box = QMessageBox.critical( None, "致命错误", f"{message}\n\n应用程序即将退出。", QMessageBox.StandardButton.Ok ) app.exec() if __name__ == "__main__": # ... 数据目录设置 ... # 设置环境变量(解决可能的OpenGL问题) os.environ["QT_ENABLE_GLYPH_CACHE_WORKAROUND"] = "1" os.environ["QT_LOGGING_RULES"] = "qt.qpa.*=false" logging.info(f"设置环境变量: QT_ENABLE_GLYPH_CACHE_WORKAROUND=1") logging.info(f"设置环境变量: QT_LOGGING_RULES=qt.qpa.*=false") main () def setup_paths(): """设置应用程序路径并返回数据目录路径""" # 获取当前文件所在目录的绝对路径 base_dir = os.path.dirname(os.path.abspath(__file__)) # 添加资源路径到系统路径 resources_path = os.path.join(base_dir, 'auto_print_system', 'resources') if resources_path not in sys.path: sys.path.append(resources_path) # 设置数据目录路径 project_root = os.path.dirname(os.path.dirname(base_dir)) app_data_path = os.path.join(project_root, 'data') # 重命名以避免冲突 os.makedirs(app_data_path, exist_ok=True) return app_data_path # 返回重命名的变量 def configure_logging() -> Logger: """配置应用程序日志""" # 移除不需要的 data_path 参数 logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler("auto_print_system.log"), logging.StreamHandler() ] ) return logging.getLogger(__name__) if __name__ == "__main__": # 初始化 logger = configure_logging() app_data_path = setup_paths() # 重命名变量以避免冲突 logger.info(f"应用程序启动,数据目录: {app_data_path}") # 修正多余空格 # 创建应用 app = QApplication(sys.argv) # 导入主窗口(延迟导入避免Qt初始化问题) from src.auto_print_system.ui.main_window import MainWindow # 创建并显示主窗口 window = MainWindow(data_path=app_data_path) # 使用重命名的变量 window.show() # 修改2: 将exec_()改为exec() exit_code = app.exec() logger.info(f"应用程序退出,代码: {exit_code}") sys.exit(exit_code) 请帮我分析该代码,并指出是否有需要改进的地方
07-11
修改下面的代码 只修改下位机检测功能 单独一个线程执行 软件打开后 每隔30s检测一次 其他功能不能修改完全保持之前的状态 import sys import time import socket import paramiko from threading import Thread from configparser import ConfigParser from pathlib import Path from PySide6.QtWidgets import (QApplication, QWidget, QLineEdit, QPushButton, QHBoxLayout, QVBoxLayout, QMessageBox, QLabel, QComboBox) from PySide6.QtCore import QObject, Signal, Qt # ---------- 配置文件路径 ---------- CONFIG_FILE = "data.ini" # ---------- 工具函数 ---------- def tcp_port_alive(ip: str, port: int, timeout: int = 2) -> bool: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.settimeout(timeout) return s.connect_ex((ip, port)) == 0 def host_reachable(ip: str, port: int = 502) -> tuple[bool, str]: if tcp_port_alive(ip, port): return True, f'TCP {port} 端口开放' return False, '502 端口无法连接' def shutdown_remote_linux(remote_host: str, ssh_user: str, ssh_pass: str, port: int = 22) -> None: ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) try: ssh.connect(remote_host, port=port, username=ssh_user, password=ssh_pass) stdin, stdout, stderr = ssh.exec_command('sudo shutdown -h now') stdout.channel.recv_exit_status() except Exception as e: raise RuntimeError(f"SSH 关机失败: {e}") from e finally: ssh.close() def verify_shutdown(remote_host: str, port: int = 22, timeout: int = 3) -> bool: try: with socket.create_connection((remote_host, port), timeout=timeout): return False except (socket.timeout, socket.error): return True def wait_shutdown_complete(remote_host: str, port: int = 22, max_wait: int = 60) -> None: for _ in range(max_wait): if verify_shutdown(remote_host, port): return time.sleep(1) raise RuntimeError("等待关机超时") def openlinux(remote_host: str, port: int = 22) -> bool: while True: try: with socket.create_connection((remote_host, port), timeout=5): return True except (socket.timeout, socket.error): continue # ---------- 探测线程 ---------- class DetectWorker(QObject): done = Signal(bool, str) ssh_status_first = Signal(bool, str) def __init__(self, ip: str, port: int, ssh_ip: str, ssh_port: int = 22): super().__init__() self.ip, self.port = ip, port self.ssh_ip, self._ssh_port = ssh_ip, ssh_port def run(self): ok, msg = host_reachable(self.ip, self.port) ssh_ok = tcp_port_alive(self.ssh_ip, self._ssh_port, timeout=2) if ssh_ok: self.ssh_status_first.emit(True, "下位机已开机") else: self.ssh_status_first.emit(False, "下位机未连接") self.done.emit(ok, msg) # ---------- 继电器 ---------- class RelayControl: def __init__(self): self.client_socket = None self._ip = None self._port = None def tcp_connect(self, ip_addr: str, ip_port: int): self._ip, self._port = ip_addr, ip_port self._reconnect() def _reconnect(self): try: if self.client_socket: self.client_socket.close() except Exception: pass self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.client_socket.settimeout(3) self.client_socket.connect((self._ip, self._port)) def _ensure_socket(self): try: self.client_socket.getpeername() except (OSError, AttributeError): self._reconnect() def tcp_disconnect(self): try: self.client_socket.close() except Exception: pass self.client_socket = None def power_up_1(self): self._ensure_socket() self.client_socket.sendall(bytes.fromhex('00000000000601052040FF00')) def power_up_2(self): self._ensure_socket() self.client_socket.sendall(bytes.fromhex('000100000006010520400000')) def power_down_1(self): self._ensure_socket() self.client_socket.sendall(bytes.fromhex('00020000000601052041FF00')) def power_down_2(self): self._ensure_socket() self.client_socket.sendall(bytes.fromhex('000300000006010520410000')) # ---------- 启动/停止线程(仅 PLC) ---------- class Worker(QObject): finished = Signal() error = Signal(str) def __init__(self, relay: RelayControl, up: bool): super().__init__() self.relay = relay self.up = up def run(self): try: if self.up: self.relay.power_up_1() time.sleep(1) self.relay.power_up_2() else: self.relay.power_down_1() time.sleep(1) self.relay.power_down_2() self.finished.emit() except Exception as e: self.error.emit(str(e)) # ---------- 独立关机线程(仅 SSH) ---------- class ShutdownWorker(QObject): finished = Signal() error = Signal(str) def __init__(self, ssh_ip: str, ssh_user: str, ssh_pwd: str): super().__init__() self.ssh_ip = ssh_ip self.ssh_user = ssh_user self.ssh_pwd = ssh_pwd def run(self): try: shutdown_remote_linux(self.ssh_ip, self.ssh_user, self.ssh_pwd) wait_shutdown_complete(self.ssh_ip) self.finished.emit() except Exception as e: self.error.emit(str(e)) # ---------- 指示灯线程 ---------- class IndicatorWorker(QObject): status_changed = Signal(bool, str) def __init__(self, target_ip: str, port: int = 22): super().__init__() self.target_ip = target_ip self.port = port self._running = True self._wait_for_on = False self._wait_for_off = False def wait_for_on(self): self._wait_for_on = True self._wait_for_off = False def wait_for_off(self): self._wait_for_off = True self._wait_for_on = False def stop(self): self._running = False def run(self): while self._running: if self._wait_for_on: if openlinux(self.target_ip, self.port): self.status_changed.emit(True, "下位机已开机") self._wait_for_on = False elif self._wait_for_off: if verify_shutdown(self.target_ip, self.port): self.status_changed.emit(False, "下位机已关机") self._wait_for_off = False else: time.sleep(0.2) # ---------- GUI 主窗口 ---------- class MainWindow(QWidget): def __init__(self): super().__init__() # ════════════ 可调常量 ════════════ WIN_WIDTH = 480 WIN_HEIGHT = 150 IP_WIDTH = 110 PORT_WIDTH = 80 USER_WIDTH = 80 BTN_WIDTH = 80 FONT_SIZE = 13 BG_COLOR = "#f5f5f5" BTN_COLOR = "#e5e5e5" BTN_HOVER = "#d0d0d0" BTN_PRESSED = "#c0c0c0" # ═════════════════════════════════ self.setFixedSize(WIN_WIDTH, WIN_HEIGHT) self.setWindowTitle('HIL 开关机控制') self.setStyleSheet(f""" QWidget{{ background-color:{BG_COLOR}; font:{FONT_SIZE}px "Microsoft YaHei"; }} QLineEdit{{ height:24px; border:1px solid #ccc; border-radius:4px; padding:0 4px; background:white; }} QPushButton{{ height:26px; border:1px solid #bbb; border-radius:4px; background:{BTN_COLOR}; min-width:{BTN_WIDTH}px; }} QPushButton:hover{{ background:{BTN_HOVER}; }} QPushButton:pressed{{ background:{BTN_PRESSED}; }} QLabel{{ color:#333; }} QComboBox{{ height:24px; border:1px solid #ccc; border-radius:4px; padding:0 4px; }} """) # ---------------- 初始化配置 ---------------- self.config = ConfigParser() self.load_config() # ---------------- 创建控件 ---------------- self.profile_combo = QComboBox() self.profile_combo.setFixedWidth(120) self.update_profile_list() self.ip_edit = QLineEdit('') self.port_edit = QLineEdit('') self.port_edit.setInputMask('00000') self.ssh_ip_edit = QLineEdit('') self.ssh_user_edit = QLineEdit('') self.ssh_pwd_edit = QLineEdit('') self.ssh_pwd_edit.setEchoMode(QLineEdit.Password) self.ssh_pwd_edit.setMaxLength(32) self.ssh_pwd_edit.setFixedWidth(110) # 统一宽度 self.ip_edit.setFixedWidth(IP_WIDTH) self.ssh_ip_edit.setFixedWidth(IP_WIDTH) self.port_edit.setFixedWidth(PORT_WIDTH) self.ssh_user_edit.setFixedWidth(USER_WIDTH) self.profile_combo.setFixedWidth(IP_WIDTH) self.conn_btn = QPushButton('连接') self.conn_btn.setCheckable(True) self.conn_btn.setFixedWidth(BTN_WIDTH) self.conn_btn.clicked.connect(self.on_toggle_connect) self._set_conn_style(False) self.start_btn = QPushButton('启动') self.stop_btn = QPushButton('停止') self.shutdown_btn = QPushButton('关闭Linux') self.start_btn.setFixedWidth(BTN_WIDTH) self.stop_btn.setFixedWidth(BTN_WIDTH) self.shutdown_btn.setFixedWidth(BTN_WIDTH) self.start_btn.clicked.connect(lambda: self.on_ctrl(up=True)) self.stop_btn.clicked.connect(lambda: self.on_ctrl(up=False)) self.shutdown_btn.clicked.connect(self.on_shutdown) self.indicator = QLabel('●') self.indicator.setFixedWidth(31) self.indicator.setStyleSheet("color: gray; font-size: 32px;") self.indicator_text = QLabel("下位机未连接") # ---------------- 布局 ---------------- row0 = QHBoxLayout() row0.addWidget(QLabel('设备选择:')) row0.addWidget(self.profile_combo) row0.addWidget(self.conn_btn) # ✅ 将“连接”按钮放在第一行 row0.addStretch() # 右侧留白 row1 = QHBoxLayout() row1.addWidget(QLabel('继电器IP:')) row1.addWidget(self.ip_edit) row1.addWidget(QLabel('端 口:')) row1.addWidget(self.port_edit) row1.addStretch() row2 = QHBoxLayout() row2.addWidget(QLabel('下位机IP:')) row2.addWidget(self.ssh_ip_edit) row2.addWidget(QLabel('用户名:')) row2.addWidget(self.ssh_user_edit) row2.addWidget(QLabel('密码:')) row2.addWidget(self.ssh_pwd_edit) row2.addStretch() row3 = QHBoxLayout() row3.addWidget(self.start_btn) row3.addWidget(self.stop_btn) row3.addWidget(self.shutdown_btn) row3.addWidget(QLabel('下位机状态:')) row3.addWidget(self.indicator) row3.addWidget(self.indicator_text) row3.addStretch() main = QVBoxLayout(self) main.addLayout(row0) # 第一行:配置 + 连接 main.addLayout(row1) # 第二行:继电器信息 main.addLayout(row2) # 第三行:SSH 信息 main.addLayout(row3) # 第四行:控制+状态 main.addStretch() # 业务对象 self.relay = RelayControl() self._thread = None self.indicator_thread = None self.indicator_worker = None self._ssh_ever_connected = False self._set_ctrl_enabled(False) # 信号绑定 self.profile_combo.currentTextChanged.connect(self.on_profile_selected) # 默认加载第一个配置 if self.profile_combo.count() > 0: self.on_profile_selected(self.profile_combo.currentText()) def load_config(self): if not Path(CONFIG_FILE).exists(): # 创建默认配置 self.config['开发环境'] = { 'relay_ip': '192.168.1.3', 'relay_port': '502', 'ssh_ip': '192.168.1.119', 'ssh_user': 'root', 'ssh_password': 'aertp2020' } self.config['测试环境'] = { 'relay_ip': '192.168.2.3', 'relay_port': '502', 'ssh_ip': '192.168.2.119', 'ssh_user': 'root', 'ssh_password': 'testpass123' } with open(CONFIG_FILE, 'w', encoding='utf-8') as f: self.config.write(f) else: self.config.read(CONFIG_FILE, encoding='utf-8') def update_profile_list(self): self.profile_combo.clear() self.profile_combo.addItems(self.config.sections()) def on_profile_selected(self, name): if not name or not self.config.has_section(name): return sec = self.config[name] self.ip_edit.setText(sec.get('relay_ip', '')) self.port_edit.setText(sec.get('relay_port', '')) self.ssh_ip_edit.setText(sec.get('ssh_ip', '')) self.ssh_user_edit.setText(sec.get('ssh_user', '')) self.ssh_pwd_edit.setText(sec.get('ssh_password', '')) # ---------------- 连接/断开 ---------------- def on_toggle_connect(self): if self.conn_btn.isChecked(): ip = self.ip_edit.text().strip() if not ip: QMessageBox.warning(self, '提示', 'IP 地址不能为空') self.conn_btn.setChecked(False) return try: port = int(self.port_edit.text()) except ValueError: QMessageBox.warning(self, '提示', '端口必须是数字') self.conn_btn.setChecked(False) return # === 禁止切换配置 === self.profile_combo.setEnabled(False) self.conn_btn.setEnabled(False) self.conn_btn.setText('连接中…') self.detector = DetectWorker(ip, port, self.ssh_ip_edit.text().strip()) self.detector.ssh_status_first.connect(self._set_indicator) self.detector.done.connect(self._on_detect_done) Thread(target=self.detector.run, daemon=True).start() else: try: self.relay.tcp_disconnect() except Exception: pass self._set_conn_style(False) self._set_ctrl_enabled(False) self._stop_indicator() self._set_indicator(False, "下位机未连接") # === 断开后恢复选择 === self.profile_combo.setEnabled(True) def _on_detect_done(self, ok: bool, msg: str): self.conn_btn.setEnabled(True) if not ok: QMessageBox.warning(self, '不可达', f'{self.ip_edit.text().strip()} 不可达({msg})') self.conn_btn.setChecked(False) self._set_conn_style(False) # === 连接失败,恢复选择 === self.profile_combo.setEnabled(True) return try: self.relay.tcp_connect(self.ip_edit.text().strip(), int(self.port_edit.text())) self._set_conn_style(True) self._set_ctrl_enabled(True) self._start_indicator() except Exception as e: QMessageBox.critical(self, '错误', f'连接失败:\n{e}') self.conn_btn.setChecked(False) self._set_conn_style(False) # === 连接失败,恢复选择 === self.profile_combo.setEnabled(True) # ---------------- 启动/停止(仅PLC) ---------------- def on_ctrl(self, up: bool): self.start_btn.setEnabled(False) self.stop_btn.setEnabled(False) self.shutdown_btn.setEnabled(False) if up: self.indicator_worker.wait_for_on() self.worker = Worker(self.relay, up=up) self.worker.finished.connect(self._done) self.worker.error.connect(self._error) self._thread = Thread(target=self.worker.run, daemon=True) self._thread.start() # ---------------- 独立关闭Linux(仅SSH) ---------------- def on_shutdown(self): self.start_btn.setEnabled(False) self.stop_btn.setEnabled(False) self.shutdown_btn.setEnabled(False) self.indicator_worker.wait_for_off() self.shutdown_worker = ShutdownWorker( self.ssh_ip_edit.text().strip(), self.ssh_user_edit.text().strip(), self.ssh_pwd_edit.text().strip()) self.shutdown_worker.finished.connect(self._shutdown_done) self.shutdown_worker.error.connect(self._shutdown_error) Thread(target=self.shutdown_worker.run, daemon=True).start() # ---------------- 指示灯 ---------------- def _start_indicator(self): self._stop_indicator() self.indicator_worker = IndicatorWorker(self.ssh_ip_edit.text().strip()) self.indicator_worker.status_changed.connect(self._set_indicator) self.indicator_thread = Thread(target=self.indicator_worker.run, daemon=True) self.indicator_thread.start() def _stop_indicator(self): if self.indicator_worker: self.indicator_worker.stop() self.indicator_worker = None def _set_indicator(self, on: bool, text: str = ""): color = "green" if on else "gray" self.indicator.setStyleSheet(f"color: {color}; font-size: 32px;") if text: self.indicator_text.setText(text) if on and not self._ssh_ever_connected: self._ssh_ever_connected = True self.shutdown_btn.setEnabled(True) # ---------------- 回调 ---------------- def _done(self): self.start_btn.setEnabled(True) self.stop_btn.setEnabled(True) self.shutdown_btn.setEnabled(True) QMessageBox.information(self, '成功', '指令执行完成') def _error(self, msg): self.start_btn.setEnabled(True) self.stop_btn.setEnabled(True) self.shutdown_btn.setEnabled(True) QMessageBox.critical(self, '错误', f'指令执行失败:\n{msg}') def _shutdown_done(self): self.start_btn.setEnabled(True) self.stop_btn.setEnabled(True) self.shutdown_btn.setEnabled(True) QMessageBox.information(self, '成功', '下位机已关闭') def _shutdown_error(self, msg): self.start_btn.setEnabled(True) self.stop_btn.setEnabled(True) self.shutdown_btn.setEnabled(True) QMessageBox.critical(self, '错误', f'关闭下位机失败:\n{msg}') # ---------------- UI 辅助 ---------------- def _set_conn_style(self, connected: bool): if connected: self.conn_btn.setText('已连接') self.conn_btn.setStyleSheet('background-color:#4CAF50;color:white;') else: self.conn_btn.setText('连接') self.conn_btn.setStyleSheet('background-color:#e5e5e5;color:black;') def _set_ctrl_enabled(self, on: bool): self.start_btn.setEnabled(on) self.stop_btn.setEnabled(on) self.shutdown_btn.setEnabled(on and self._ssh_ever_connected) # -------------------- main -------------------- if __name__ == '__main__': app = QApplication(sys.argv) w = MainWindow() w.show() sys.exit(app.exec())
11-26
你要求: > **只修改“下位机检测功能”**,改为: ✅ 软件启动后,**自动、每隔30秒检测一次下位机状态(SSH连接)**, ✅ 使用 **单独线程执行**, ✅ 其他所有功能保持不变。 --- ### ✅ 修改目标分析 原代码中,“下位机是否开机”的检测逻辑是通过 `IndicatorWorker` 类完成的,但它只在以下情况触发: - 点击“启动” - 点击“关闭Linux” - 手动调用 `.wait_for_on()` 或 `.wait_for_off()` 而现在你的需求是:**无论有没有操作,软件一打开就持续每30秒检测一次 SSH 是否可达,并更新指示灯状态。** --- ### ✅ 解决方案 我们将做如下改动: 1. 新增一个后台线程类 `PeriodicSSHCheckWorker`,继承自 `QObject`,每 30 秒检查一次 SSH IP 的 22 端口。 2. 发现状态变化时,通过信号通知 UI 更新指示灯。 3. 在 `MainWindow.__init__` 中启动该线程(仅一次),并随窗口关闭自动退出。 4. 不影响原有 `IndicatorWorker` 对“等待开机/关机”的监听逻辑 —— 它们可以共存。 --- ### ✅ 修改后的完整代码(仅展示被修改的部分) ```python # ---------- 周期性SSH检测线程 ---------- class PeriodicSSHCheckWorker(QObject): status_changed = Signal(bool, str) # True/"下位机已开机" or False/"下位机未连接" def __init__(self, ssh_ip: str, port: int = 22): super().__init__() self.ssh_ip = ssh_ip self.port = port self._running = True def stop(self): self._running = False def run(self): last_status = None while self._running: try: current_ok = tcp_port_alive(self.ssh_ip, self.port, timeout=3) current_status = "下位机已开机" if current_ok else "下位机未连接" if current_ok != last_status: self.status_changed.emit(current_ok, current_status) last_status = current_ok except Exception: if last_status is not None: self.status_changed.emit(False, "下位机未连接") last_status = None # 每隔30秒检测一次 for _ in range(30): if not self._running: break time.sleep(1) # ---------- GUI 主窗口 ---------- class MainWindow(QWidget): def __init__(self): super().__init__() # ... (其他不变的初始化代码保持一致) # 业务对象 self.relay = RelayControl() self._thread = None self.indicator_thread = None self.indicator_worker = None self.periodic_thread = None # <<< 新增:周期检测线程 self.periodic_worker = None # <<< 新增:周期检测工作对象 self._ssh_ever_connected = False self._set_ctrl_enabled(False) # 信号绑定 self.profile_combo.currentTextChanged.connect(self.on_profile_selected) # 默认加载第一个配置 if self.profile_combo.count() > 0: self.on_profile_selected(self.profile_combo.currentText()) # === 启动周期性检测线程 === self._start_periodic_ssh_check() # === 新增方法:启动周期性检测 === def _start_periodic_ssh_check(self): ssh_ip = self.ssh_ip_edit.text().strip() if not ssh_ip: return self.periodic_worker = PeriodicSSHCheckWorker(ssh_ip) self.periodic_worker.status_changed.connect(self._set_indicator) self.periodic_thread = Thread(target=self.periodic_worker.run, daemon=True) self.periodic_thread.start() def _stop_periodic_ssh_check(self): if self.periodic_worker: self.periodic_worker.stop() self.periodic_worker = None # --- 修改 on_profile_selected:切换配置时也更新周期检测线程 --- def on_profile_selected(self, name): if not name or not self.config.has_section(name): return sec = self.config[name] old_ssh_ip = self.ssh_ip_edit.text().strip() new_ssh_ip = sec.get('ssh_ip', '') self.ip_edit.setText(sec.get('relay_ip', '')) self.port_edit.setText(sec.get('relay_port', '')) self.ssh_ip_edit.setText(new_ssh_ip) self.ssh_user_edit.setText(sec.get('ssh_user', '')) self.ssh_pwd_edit.setText(sec.get('ssh_password', '')) # 如果SSH IP变了,重启周期检测线程 if new_ssh_ip and (new_ssh_ip != old_ssh_ip): self._restart_periodic_ssh_check(new_ssh_ip) def _restart_periodic_ssh_check(self, ssh_ip: str): self._stop_periodic_ssh_check() self.periodic_worker = PeriodicSSHCheckWorker(ssh_ip) self.periodic_worker.status_changed.connect(self._set_indicator) self.periodic_thread = Thread(target=self.periodic_worker.run, daemon=True) self.periodic_thread.start() # ... (其余所有方法完全保持不变) ``` --- ### ✅ 功能说明 | 特性 | 实现方式 | |------|----------| | 自动检测 | 软件一打开就启动线程 | | 每 30 秒检测一次 | `time.sleep(1)` 循环 30 次实现精确控制 | | 单独线程 | 使用 `Thread + QObject` 组合,不阻塞 UI | | 状态变化才更新 | 避免频繁刷新 UI | | 多环境切换支持 | 切换 profile 时自动重启检测线程 | | 与原功能兼容 | 原有的 `IndicatorWorker` 仍用于“等待开机完成”等场景 | --- ### ✅ 注意事项 - `tcp_port_alive(ip, port)` 函数已在顶部定义,无需重复添加。 - 所有新旧功能互不干扰: - `IndicatorWorker` 用于主动等待某个状态达成(如刚开机后等待上线); - `PeriodicSSHCheckWorker` 用于被动持续监控当前状态。 --- ### ✅ 最终效果 > 软件一打开 → 自动读取当前配置的 `ssh_ip` → 启动后台线程 → 每 30 秒探测一次 → 自动更新状态标签绿色/灰色指示灯。 即使你不点“连接继电器”,也能看到“下位机是否在线”。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值