1、问题的发现

  今天,一个在windows上运行良好的python脚本放到linux下报错,提示错误 BrokenPipeError: [Errno 32]Broken pipe。经调查是subprocess.run方法的timeout参数在linux上的表现和windows上不一致导致的。

try:
ret = subprocess.run(cmd, shell=True, check=True, timeout=5,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
except Exception as e:
logging.debug(f"Runner FAIL")

2、问题描述

 为了描述这个问题,做了下面这个例子。subprocess.run调用了1个需要10s才能执行完的程序,但是却设定了1s的超时时间。理论上这段代码应该在1s后因超时退出,但事实并不如此。

import subprocess
import time t = time.perf_counter()
args = 'python -c "import time; time.sleep(10)"'
try:
p = subprocess.run(args, shell=True, check=True,timeout=1,stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
except Exception as e:
print(f"except is {e}")
print(f'coast:{time.perf_counter() - t:.8f}s')

 在windows上测试:

PS C:\Users\peng\Desktop> Get-ComputerInfo | select WindowsProductName, WindowsVersion, OsHardwareAbstractionLayer

WindowsProductName WindowsVersion OsHardwareAbstractionLayer
------------------ -------------- --------------------------
Windows 10 Pro 2009 10.0.19041.2251
PS C:\Users\peng\Desktop> python .\test_subprocess.py
except is Command 'python -c "import time; time.sleep(10)"' timed out after 1 seconds
coast:10.03642740s
PS C:\Users\peng\Desktop>

 在linux上测试:

21:51:31 wp@PowerEdge:~/bak$ cat /etc/os-release
NAME="Ubuntu"
VERSION="20.04.3 LTS (Focal Fossa)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 20.04.3 LTS"
VERSION_ID="20.04"
21:56:45 wp@PowerEdge:~/bak$ python test_subprocess.py
except is Command 'python -c "import time; time.sleep(10)"' timed out after 1 seconds
coast:1.00303393s
21:57:02 wp@PowerEdge:~/bak$

 可见,subprocess.run的timeout参数在windows下并没有生效。subprocess.run执行指定的命令,等待命令执行完成后返回一个包含执行结果的CompletedProcess类的实例。这个函数的原型为:

subprocess.run(args, *, stdin=None, input=None, stdout=None, stderr=None, capture_output=False,
shell=False, cwd=None, timeout=None, check=False, encoding=None, errors=None, text=None, env=None, universal_newlines=None)
  • args:表示要执行的命令。必须是一个字符串,字符串参数列表。
  • stdin、stdout 和 stderr:子进程的标准输入、输出和错误。其值可以是 subprocess.PIPE、subprocess.DEVNULL、一个已经存在的文件描述符、已经打开的文件对象或者 None。subprocess.PIPE 表示为子进程创建新的管道。subprocess.DEVNULL 表示使用 os.devnull。默认使用的是 None,表示什么都不做。另外,stderr 可以合并到 stdout 里一起输出。
  • timeout:设置命令超时时间。如果命令执行时间超时,子进程将被杀死,并弹出 TimeoutExpired 异常。
  • check:如果该参数设置为 True,并且进程退出状态码不是 0,则弹 出 CalledProcessError 异常。
  • encoding: 如果指定了该参数,则 stdin、stdout 和 stderr 可以接收字符串数据,并以该编码方式编码。否则只接收 bytes 类型的数据。
  • shell:如果该参数为 True,将通过操作系统的 shell 执行指定的命令。

3、问题分析

  subprocess.run 会等待进程终止并处理TimeoutExpired异常。在POSIX上,异常对象包含读取部分的stdout和stderr字节。上面测试在windows上失效的主要问题是使用了shell模式,启动了管道,管道句柄可能由一个或多个后代进程继承(如通过shell=True),所以当超时发生时,即使关闭了shell程序,而由shell启动的其他程序,本例中是python程序依然在运行中,所以阻止了subprocess.run退出直至使用管道的所有进程退出。如果改为shell=False,则在windows上也出现1s的结果:

python  .\test_subprocess.py
except is Command 'python -c "import time; time.sleep(10)"' timed out after 1 seconds
coast:1.00460970s

  可以说这是windows实现上的一个缺陷,具体的可见:

https://github.com/python/cpython/issues/87512

[subprocess] run() sometimes ignores timeout in Windows #87512


subprocess.run() handles TimeoutExpired by terminating the process and waiting on it. On POSIX, the exception object contains the partially read stdout and stderr bytes. For example:

cmd = 'echo spam; echo eggs >&2; sleep 2'
try: p = subprocess.run(cmd, shell=True, capture_output=True,
text=True, timeout=1)
except subprocess.TimeoutExpired as e: ex = e >>> ex.stdout, ex.stderr
(b'spam\n', b'eggs\n')

 On Windows, subprocess.run() has to finish reading output with a second communicate() call, after which it manually sets the exception's stdout and stderr attributes.

The poses the problem that the second communicate() call may block indefinitely, even though the child process has terminated.

 The primary issue is that the pipe handles may be inherited by one or more descendant processes (e.g. via shell=True), which are all regarded as potential writers that keep the pipe from closing. Reading from an open pipe that's empty will block until data becomes available. This is generally desirable for efficiency, compared to polling in a loop. But in this case, the downside is that run() in Windows will effectively ignore the given timeout.

 Another problem is that _communicate() writes the input to stdin on the calling thread with a single write() call. If the input exceeds the pipe capacity (4 KiB by default -- but a pipesize 'suggested' size could be supported), the write will block until the child process reads the excess data. This could block indefinitely, which will effectively ignore a given timeout. The POSIX implementation, in contrast, correctly handles a timeout in this case.

 Also, Popen.exit() closes the stdout, stderr, and stdin files without regard to the _communicate() worker threads. This may seem innocuous, but if a worker thread is blocked on synchronous I/O with one of these files, WinAPI CloseHandle() will also block if it's closing the last handle for the file in the current process. (In this case, the kernel I/O manager has a close procedure that waits to acquire the file for the current thread before performing various housekeeping operations, primarily in the filesystem, such as clearing byte-range locks set by the current process.) A blocked close() is easy to demonstrate. For example:

args = 'python -c "import time; time.sleep(99)"'
p = subprocess.Popen(args, shell=True, stdout=subprocess.PIPE)
try: p.communicate(timeout=1)
except: pass p.kill() # terminates the shell process -- not python.exe
with p: pass # stdout.close() blocks until python.exe exits

The Windows implementation of Popen._communicate() could be redesigned as follows:

  • read in chunks, with a size from 1 byte up to the maximum available,

    as determined by _winapi.PeekNamedPipe()
  • write to the child's stdin on a separate thread
  • after communicate() has started, ensure that synchronous I/O in worker

    threads has been canceled via CancelSynchronousIo() before closing

    the pipes.

    The _winapi module would need to wrap OpenThread() and CancelSynchronousIo(), plus define the TERMINATE_THREAD (0x0001) access right.

With the proposed changes, subprocess.run() would no longer special case TimeoutExpired on Windows.

BrokenPipeError错误和subprocess.run()超时参数在Windows上无效的更多相关文章

  1. MySQL 各种超时参数的含义

    MySQL 各种超时参数的含义 今日在查看锁超时的设置时,看到show variables like '%timeout%';语句输出结果中的十几种超时参数时突然想整理一下,不知道大家有没有想过,这么 ...

  2. php-fpm nginx 超时参数设置

    php-fpm:request_terminate_timeout = 30php.ini:max_execution_time = 30 request_terminate_timeout 适用于, ...

  3. Flink run提交参数

    折腾了好几天,终于搞定了Flink run提交参数,记录一下. 背景: 之前一直报错,akka,AskTimeoutException,尝试添加akka.ask.timeout=120000s, 依然 ...

  4. Delphi HTTPRIO控件怎么设置超时参数

    HTTPRIO控件怎么设置超时参数 //HTTPRIO1: THTTPRIO  设置5分钟超时 HTTPRIO1.HTTPWebNode.ConnectTimeout := 5000; Connect ...

  5. subprocess.run()用法python3.7

    def run(*popenargs, input=None, capture_output=False, timeout=None, check=False, **kwargs): "&q ...

  6. 如何查看docker run启动参数命令

    通过runlike去查看一个容器的docker run启动参数 安装pip yum install -y python-pip 安装runlike pip install runlike 查看dock ...

  7. Docker run 命令参数及使用

    Docker run 命令参数及使用 Docker run :创建一个新的容器并运行一个命令 语法 docker run [OPTIONS] IMAGE [COMMAND] [ARG...] OPTI ...

  8. Oracle 统计量NO_INVALIDATE参数配置(上)

    转载:http://blog.itpub.net/17203031/viewspace-1067312/ Oracle统计量对于CBO执行是至关重要的.RBO是建立在数据结构的基础上的,DDL结构.约 ...

  9. Http multipart/form-data多参数Post方式上传数据

    最近,工作中遇到需要使用java实现http发送get.post请求,简单的之前经常用到,但是这次遇到了上传文件的情况,之前也没深入了解过上传文件的实现,这次才知道通过post接口也可以,是否还有其他 ...

  10. HTTP 错误 404.2 - Not Found 由于 Web 服务器上的“ISAPI 和 CGI 限制”列表设置,无法提供您请求的页面

    详细错误:HTTP 错误 404.2 - Not Found. 由于 Web 服务器上的“ISAPI 和 CGI 限制”列表设置,无法提供您请求的页面. 出现环境:win7 + IIS7.0 解决办法 ...

随机推荐

  1. 关于Elasticsearch使用java的说明

    从Elastic 7.0开始,我们可以不安装JAVA.安装包包含一个相匹配的JAVA版本在里面. Elasticsearch包含来自JDK维护者(GPLv2 + CE)的捆绑版OpenJDK. 要使用 ...

  2. Fluentd部署:错误排查

    介绍一下排查Fluentd运行时错误的几种方法. 查看日志 如果感觉Fluentd运行异常,请先查看日志.td-agent安装后,默认日志存放在/var/log/td-agent/td-agent.l ...

  3. 4_Spring

    一. Spring Spring的基本组成: 1.最完善的轻量级核心框架. 2.通用的事务管理抽象层. 3.JDBC抽象层. 4.集成了Toplink, Hibernate, JDO, and iBA ...

  4. c#-03关于类和继承的基本知识

    一.类继承 通过类继承可以定义一个新类,新类纳入一个已经声明的类进行扩展 已经存在的类叫做基类,而通过继承出的类叫做派生类,派生类的组成为: 本身声明中的成员 基类的成员 派生类无法删除基类成员,但可 ...

  5. day03-MySQL基础知识02

    MySQL基础知识02 4.CRUD 数据库CRUD语句:增(create).删(delete).改(update).查(Retrieve) Insert 语句 (添加数据) Update 语句(更新 ...

  6. C++面向对象编程之point-like classes的智能指针和迭代器、function-like classes即仿函数

    1.智能指针 智能指针里面包含其他指针的形式和 委托 感觉比较像; 智能指针一定都需要重载 * 和 -> 操作符 ; 这个符号它作用后还能再继续作用下去; 2.迭代器: 这里主要关注 * 和 - ...

  7. 微软出品自动化神器【Playwright+Java】系列(五) 之 常见点击事件操作

    写在前面 明天就是周五了,这周有那么一两天心情特别不好,真的是做什么都没兴致,所以导致整个人都很丧,什么都不想做. 本打算周一就更新这篇文章的,但由于公司一直加班,每天到家很晚,都是挤时间去学,理解后 ...

  8. rowkey设计原则和方法

    rowkey设计首先应当遵循三大原则: 1.rowkey长度原则 rowkey是一个二进制码流,可以为任意字符串,最大长度为64kb,实际应用中一般为10-100bytes,它以byte[]形式保存, ...

  9. RAID5 IO处理之replace代码详解

    1 作用 从字面意思理解,replacement即是替换.我们知道硬盘都有一定的使用寿命,可以在硬盘失效之前通过该功能将就盘的数据迁移至新盘.因为replacement的流程是从旧盘中读出数据直接写入 ...

  10. Vue学习之--------事件的基本使用、事件修饰符、键盘事件(2022/7/7)

    文章目录 1.事件处理 1.1. 事件的基本使用 1.1.1 .基础知识 1.1.2. 代码实例 1.1.3.测试效果 1.2.事件修饰符 1.2.1. 基础知识 1.2.2 .代码实例 1.2.3 ...