记录python子进程的语法错误和未捕获异常,并将它们打印到终端 问题

记录python子进程的语法错误和未捕获异常,并将它们打印到终端 问题,python,subprocess,stderr,Python,Subprocess,Stderr,我一直在尝试编写一个程序,记录子进程的未捕获异常和语法错误。简单,对吗?只需将管道stderr输送到正确的位置 但是,子流程是另一个python程序,我将其称为test.py,它需要像未捕获其输出/错误一样运行。也就是说,运行logger程序需要让用户看起来就像正常运行了python test.py 使问题进一步复杂化的是,如果未使用readline,则raw\u输入实际上会被发送到stderr。不幸的是,我不能只导入readline,因为我无法控制使用错误记录器运行的文件 注意事项: 我在运

我一直在尝试编写一个程序,记录子进程的未捕获异常和语法错误。简单,对吗?只需将管道
stderr
输送到正确的位置

但是,子流程是另一个python程序,我将其称为
test.py
,它需要像未捕获其输出/错误一样运行。也就是说,运行logger程序需要让用户看起来就像正常运行了
python test.py

使问题进一步复杂化的是,如果未使用
readline
,则
raw\u输入实际上会被发送到
stderr
。不幸的是,我不能只导入readline,因为我无法控制使用错误记录器运行的文件

注意事项:

  • 我在运行此代码的机器上受到相当大的限制。我无法安装
    pexpect
    或编辑
    *customize.py
    文件(因为程序将由许多不同的用户运行)。我真的觉得无论如何都应该有一个stdlib解决方案
  • 这只适用于Mac电脑
  • 这样做的动机是,我是一个研究新程序员所犯错误的团队的一员
我试过的 我尝试了以下方法,但没有成功:

  • 仅使用问题中的
    tee
    (未能生成
    raw\u输入
    提示);我在几个SO问题中发现的
    tee
    的python实现也有类似的问题
  • 覆盖
    sys.excepthook
    (无法使其适用于子流程)
  • 最上面的答案似乎很有希望,但它未能正确显示
    raw\u输入
    提示
  • 对于实际写入日志文件来说,这似乎很有用,但似乎并没有抓住问题的关键
  • 自定义标准读取器
  • 无休止的谷歌搜索
    • 不太适合您的任务。尽管您可以通过使用
      -u
      选项禁用缓冲来解决“
      原始输入()
      提示”问题:

      errf = open('err.txt', 'wb') # any object with .write() method
      rc = call([sys.executable, '-u', 'test.py'], stderr=errf, 
                bufsize=0, close_fds=True)
      errf.close()
      
      更合适的解决方案可能基于或
      pty

      运行记录器程序需要让用户看起来像正常运行python test.py一样

      您不需要安装
      pexpect
      这是纯Python,您可以将其与代码一起安装

      这是一个基于tee的模拟(
      test.py
      以非交互方式运行):

      有必要合并stdout/stderr,因为尚不清楚
      raw_input()
      getpass.getpass()
      可能会打印它们的提示

      在这种情况下,也不需要螺纹:

      #!/usr/bin/env python
      import sys
      from subprocess import Popen, PIPE, STDOUT
      
      with open('log','ab') as file:
          p = Popen([sys.executable, '-u', 'test.py'],
                    stdout=PIPE, stderr=STDOUT,
                    close_fds=True,
                    bufsize=0)
          for c in iter(lambda: p.stdout.read(1), ''):
              for f in [sys.stdout, file]:
                  f.write(c)
                  f.flush()
          p.stdout.close()
          rc = p.wait()
      
      注意:最后一个示例和基于tee的解决方案不会捕获
      getpass.getpass()
      提示符,但基于
      pexpect
      pty
      的解决方案会:

      #!/usr/bin/env python
      import os
      import pty
      import sys
      
      with open('log', 'ab') as file:
          def read(fd):
              data = os.read(fd, 1024)
              file.write(data)
              file.flush()
              return data
      
          pty.spawn([sys.executable, "test.py"], read)
      

      我不知道
      pty.spawn()
      是否能在Mac电脑上工作。

      根据@nneonneo在问题评论中的建议,我制作了这个程序,似乎可以完成任务。(请注意,当前,记录器文件的名称必须使用“pylog”才能正确打印到最终用户。)


      什么使它不合适?那些其他模块在幕后不会做类似的事情吗?(我现在正在阅读它们。)而且,
      -u
      标志似乎无法解决问题。(我正在使用另一个答案和其中的代码中的
      call
      函数。)@Matthewaddams:当程序检测到它以交互方式运行(使用tty)时,除了I/O缓冲行为外,它还可能改变其行为的其他方面。我已经用
      test.py
      print raw_input('input string to reverse')[::-1]
      对它进行了测试,它可以正常工作。顺便说一句,
      raw_input()
      将其提示符打印到stdout(如文档所示),而不是stderr。因此,如果我将
      import readline
      添加到
      test.py
      ,它将按预期工作。我显然不想这样做,因为我不写输入文件。(我仍然很困惑,但你得到了我的支持。)@Matthewaddams:(Python 3.2)和(Python 2.7)如果有tty,则在C级别使用
      PyOS\u Readline()
      ,并在非交互式情况下打印到标准输出。如果未启用
      readline
      ,则确实
      PyOS\u readline()
      会打印其内容。如果问题得到解决,您可以将
      import readline
      添加到
      sitecustomize.py
      usercustomize.py
      。您需要如何分发程序?您是否允许使用,a来打包您的程序
      pexpect
      是纯Python,如果不允许安装任何东西,可以将其与程序打包。子进程实际上必须是子进程吗?您可以使用
      execfile
      运行用户提供的代码吗?@nneo是的,它必须是子流程,因为用户的代码必须具有类似于
      sys.argv
      的内容,才能按预期运行。
      sys.argv
      完全没有问题。您可以替换
      sys.argv
      。您目前是如何围绕用户代码运行记录器的?他们是否明确地调用它?如果没有,你的钩子是什么?我以前做过这种事情,信不信由你——我曾经在Python自动签名系统上工作过。您可以让脚本在经过消毒的环境中执行,而无需跳转,它们的代码执行起来就像在单独的解释器中调用一样。试一试——将自己的代码放入目标环境的灵活性让您可以做比其他情况下更复杂的事情(如自省堆栈跟踪和捕获代码和执行环境)。为什么要捕获
      SystemExit
      ?这就是
      sys.exit()
      的工作方式。对于
      1,您可以使用
      traceback.print\u exception
      很容易故意或无意地从子程序内部弄乱基于exec的解决方案,例如,
      import\u main\uuuuuuuuu;删除主回溯
      #!/usr/bin/env python import sys from subprocess import Popen, PIPE, STDOUT with open('log','ab') as file: p = Popen([sys.executable, '-u', 'test.py'], stdout=PIPE, stderr=STDOUT, close_fds=True, bufsize=0) for c in iter(lambda: p.stdout.read(1), ''): for f in [sys.stdout, file]: f.write(c) f.flush() p.stdout.close() rc = p.wait()
#!/usr/bin/env python
import os
import pty
import sys

with open('log', 'ab') as file:
    def read(fd):
        data = os.read(fd, 1024)
        file.write(data)
        file.flush()
        return data

    pty.spawn([sys.executable, "test.py"], read)
#!/usr/bin/python

'''
This module logs python errors.
'''

import socket, os, sys, traceback

def sendError(err):
    # log the error (in my actual implementation, this sends the error to a database)
    with open('log','w') as f:
        f.write(err)


def exceptHandler(etype, value, tb):
    """An additional wrapper around our custom exception handler, to prevent errors in
       this program from being seen by end users."""
    try:
        subProgExceptHandler(etype, value, tb)
    except:
        sys.stderr.write('Sorry, but there seems to have been an error in pylog itself. Please run your program using regular python.\n')

def subProgExceptHandler(etype, value, tb):
    """A custom exception handler that both prints error and traceback information in the standard
       Python format, as well as logs it."""
    import linecache

    errorVerbatim = ''

    # The following code mimics a traceback.print_exception(etype, value, tb) call.
    if tb:
        msg = "Traceback (most recent call last):\n"
        sys.stderr.write(msg)
        errorVerbatim += msg

        # The following code is a modified version of the trackeback.print_tb implementation from
        # cypthon 2.7.3
        while tb is not None:
            f = tb.tb_frame                                                      
            lineno = tb.tb_lineno                                                  
            co = f.f_code                                                        
            filename = co.co_filename                                              
            name = co.co_name
            # Filter out exceptions from pylog itself (eg. execfile).
            if not "pylog" in filename:
                msg = '  File "%s", line %d, in %s\n' % (filename, lineno, name)
                sys.stderr.write(msg)       
                errorVerbatim += msg
                linecache.checkcache(filename)                                         
                line = linecache.getline(filename, lineno, f.f_globals)                
                if line: 
                    msg = '    ' + line.strip() + '\n'
                    sys.stderr.write(msg)
                    errorVerbatim += msg
            tb = tb.tb_next                                           

    lines = traceback.format_exception_only(etype, value)
    for line in lines:
        sys.stderr.write(line)
        errorVerbatim += line

    # Send the error data to our database handler via sendError.
    sendError(errorVerbatim)

def main():
    """Executes the program specified by the user in its own sandbox, then sends
       the error to our database for logging and analysis."""
    # Get the user's (sub)program to run.
    try:
        subProgName = sys.argv[1]
        subProgArgs = sys.argv[3:]
    except:
        print 'USAGE: ./pylog FILENAME.py *ARGS'
        sys.exit()

    # Catch exceptions by overriding the system excepthook.
    sys.excepthook = exceptHandler
    # Sandbox user code exeuction to its own global namespace to prevent malicious code injection.
    execfile(subProgName, {'__builtins__': __builtins__, '__name__': '__main__', '__file__': subProgName, '__doc__': None, '__package__': None})

if __name__ == '__main__':
    main()