找回密码
 会员注册
查看: 21|回复: 0

Pythonsubprocess模块项目实战

[复制链接]

2万

主题

0

回帖

6万

积分

超级版主

积分
64110
发表于 2024-10-11 21:24:50 | 显示全部楼层 |阅读模式
背景我们日常测试中存在大量重复的造数操作,且流程较长,为了提升测试效率,我们搭建了数据构造平台。平台采用了前端+脚本分离的形式,数据构造脚本独立存在,页面和脚本的关联关系通过页面配置进行绑定。页面配置中,包含了脚本的路径以及启动命令,因此,运行脚本的时候需要在服务器上启动子进程中去执行脚本命令。为了能够了解脚本的执行情况,还需要获取脚本的执行状态以及执行日志。平台后端语言是Python,因此,选择了Python中的subprocess模块,本文重点阐述subprocess模块在项目实战中遇到的问题以及解决方案。本文涉及的程序执行环境如下:Python版本:3.8.3操作系统:windows server01Subprocess模块基础subprocess 模块允许我们启动一个新进程,并连接到它们的输入/输出/错误管道,从而获取返回值。subprocess 模块首先推荐使用的是它的 run 方法,更高级的用法可以直接使用 Popen 接口。1. subprocess.run方法subprocess.run()方法是3.5版本新增的,用于可以接受等待进程执行结束后获取返回值的场景,如果可以满足使用需求,官方推荐使用run()方法。subprocess.run()的执行过程是同步的,脚本执行结束之前是阻塞的,只有脚本结束之后才会返回subprocess.CompletedProcess对象。2. subprocess.Popen方法subprocess.Popen()是 subprocess的核心,子进程的创建和管理都靠它处理。Popen()相当于run()的高级版本,更加灵活,使开发人员能够处理run()方法未涵盖的更丰富的场景。subprocess.Popen()是异步的,进程启动以后,我们可以通过预先指定好的stdout 和stderr来实时读取到子进程的输出。subprocess.Popen()常用参数介绍:args:shell命令,可以是字符串或者序列类型(如:list,元组)stdin, stdout, stderr:分别表示程序的标准输入、输出、错误句柄shell:如果该参数为 True,将通过操作系统的 shell 执行指定的命令,args只能是String类型的参数;该参数为False,args可以是序列类型。Popen 对象常用方法:poll():检查进程是否终止,如果终止返回 returncode,否则返回 None,项目中通过该方法返回判断进程是否执行结束。wait(timeout):等待子进程终止,如果进程执行时间较长,可以使用该方法来保证进程执行完整。communicate(input,timeout):和子进程交互,发送和读取数据。send_signal(singnal):发送信号到子进程。terminate():停止子进程,也就是发送SIGTERM信号到子进程。kill():杀死子进程。发送 SIGKILL 信号到子进程。3. run与Popen的同步/异步对比实验Run()和Popen()同步/异步的简单对比如下:从执行结果可以看出,Popen在子进程执行过程中就可以获取到日志,run需要等待进程执行完成才能获取到日志。如果需要执行的命令耗时很短,可以选择run方法。因为我们的数据构造流程通常比较长,需要实时获取日志,所以选择了Popen。02遇到的问题与解决方案在使用Popen的过程中也遇到了一些问题,下面将具体介绍一下遇到的问题以及解决方案。1. 如何保证获取到完整的进程执行日志subprocess.Popen()可以获取到执行过程中的日志了,那我们如何保证进程日志获取的完整性呢?我们来看下具体方案:方案一:这是我们最开始采用的方案。通过获取方法poll()返回的状态码来检查进程是否终止。如果终止,返回 returncode,否则返回 None,代码如下:该方案在使用的过程中存在问题。当子程序已经执行完毕,日志还没有获取完整,会出现日志接收不全的情况。为了解决这种问题,保证日志的完整性,我们选择通过判断日志是否读取完毕作为判断依据,详细参见方案二。方案二:通过判断日志是否读取完毕保证日志完整性。代码如下:这种方法看似解决了日志不全的问题,但是存在着一定的风险。日志为None无法有效保证子进程执行结束(虽然经过多方实践,暂时没有发现日志为None但脚本未执行结束的情况)。为了安全起见,我们还是需要兼顾一下进程的执行状态,具体参见方案三。方案三:通过判断poll()返回状态和日志返回值,也就是说,程序状态结束且返回对象为空,才表示子进程已经执行结束,并且获取到了完整的日志,代码如下:该方案已经比较完善了,通过子进程执行结束并且执行日志为None,保证执行日志的完整性。美中不足的是,日志信息可能会比实际的多一些,当输出先读取完毕,子进程还没有结束,我们会获取到一部分空行,为了日志的美观度,我们可以进一步优化,获取日志的时候,过滤掉空行,代码如下:通过判断输出流和进程的执行状态,完美的解决了上面的问题,保证了日志的完整性与正确性。2. 如何保证脚本进程正常终止当脚本执行以后,我们可能会因为某些原因想终止脚本的运行,如参数错误等。在我们项目代码中,使用Popen.terminate()去终止进程的时候,发现命令只终止了父进程,唤起的子进程仍然在执行。为了找到原因,先看一下项目中创建Popen的代码:参数介绍的时候提到过,shell为True或False时,command的类型是有要求的。因为我们command传值是String类型,参数shell只能设置为True。当shell=True的时,程序会创建一个shell进程,command是shell进程的子进程。我们再来看下Popen.terminate()做了什么?官方的说明如下:Stopthechild.OnPOSIXOSsthemethodsendsSIGTERMtothechild.OnWindowstheWin32APIfunctionTerminateProcess()iscalledtostopthechild也就是说,在POSIX系统中,该方法会发送SIGTERM信号给子进程;在Windows系统中,该方法会调用Win32提供的API TerminateProcess()方法。原因很清晰了,当shell=True的时候,发送SIGTERM能够杀死shell进程,但是无法杀死它的子进程(command);windows系统中同理,TerminateProcess()杀死了shell进程,却没有杀死它的子进程(command)。解决方案如下:方案一:比较优雅的方式,创建Popen对象时,将参数shell设为False。实践发现,当shell=False的时候,Popen.terminate()方法的执行结果是符合预期的;subprocess.Popen(command,shell=False)前面提到过,因为command格式问题,在我们项目中,shell只能设置为True,所以我们又探索了新的解决方案。方案二:手动终止进程。使用第三方工具包psutil,获取全部的子进程并逐一杀掉,该方法在Linux和windows平台通用。代码见下图。在windows服务器下,还可以用以下命令:taskkill /t /f /pid {pid},强制杀掉指定进程以及它的子进程。windows平台的方案无需第三方依赖,所以我们项目中选择了该方案,项目代码如下:以上就是Python中的subprocess模块在我们项目实践中遇到的问题以及解决方案,希望可以给大家提供一些使用思路以及规避掉一系列问题。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2024-12-26 12:37 , Processed in 0.325989 second(s), 25 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表