python:当底层库日志到stdout

时间:2018-02-09 01:36:09

标签: python logging stdout curses

我正在尝试编写一个使用curses和SWIGed C ++库的小型python程序。该库将大量信息记录到STDOUT,这会干扰curses的输出。我想以某种方式拦截该内容,然后通过ncurses很好地显示它。有没有办法做到这一点?

2 个答案:

答案 0 :(得分:2)

最小的演示示例将有望展示这一切是如何运作的。我不打算为此设置SWIG,并选择通过.so调用ctypes文件以模拟外部C库使用情况的快速而肮脏的演示。只需将以下内容放在工作目录中即可。

testlib.c

#include <stdio.h>

int vomit(void);                                                                

int vomit()                                                                     
{                                                                               
    printf("vomiting output onto stdout\n");                                    
    fflush(stdout);                                                             
    return 1;                                                                   
}

使用gcc -shared -Wl,-soname,testlib -o _testlib.so -fPIC testlib.c构建

testlib.py

import ctypes                                                                   
from os.path import dirname                                                     
from os.path import join                                                        

testlib = ctypes.CDLL(join(dirname(__file__), '_testlib.so'))

demo.py(最低限度演示)

import os
import sys
import testlib
from tempfile import mktemp

pipename = mktemp()
os.mkfifo(pipename)
pipe_fno = os.open(pipename, os.O_RDWR | os.O_NONBLOCK)
stdout_fno = os.dup(sys.stdout.fileno())

os.dup2(pipe_fno, 1)
result = testlib.testlib.vomit()
os.dup2(stdout_fno, 1)

buf = bytearray()
while True:
    try:
        buf += os.read(pipe_fno, 1)
    except Exception:
        break

print("the captured output is: %s" % open('scratch').read())
print('the result of the program is: %d' % result)
os.unlink(pipename)

需要注意的是.so 生成的输出可能ctypes系统中以某种方式缓冲(我不知道该部分是如何工作的),而我除非fflush代码在.so内,否则无法找到清除输出的方法以确保它们全部输出;因此,最终的表现可能会出现并发症。

使用线程,这也可以完成(代码变得非常恶劣,但它显示了这个想法):

import os
import sys
import testlib
from threading import Thread
from time import sleep
from tempfile import mktemp

def external():
    # the thread that will call the .so that produces output
    for i in range(7):
        testlib.testlib.vomit()
        sleep(1)

# setup 
stdout_fno = os.dup(sys.stdout.fileno())
pipename = mktemp()
os.mkfifo(pipename)
pipe_fno = os.open(pipename, os.O_RDWR | os.O_NONBLOCK)
os.dup2(pipe_fno, 1)

def main():
    thread = Thread(target=external)
    thread.start()

    buf = bytearray()
    counter = 0
    while thread.isAlive():
        sleep(0.2)
        try:
            while True:
                buf += os.read(pipe_fno, 1)
        except BlockingIOError:
            if buf:
                # do some processing to show that the string is fully
                # captured 
                output = 'external lib: [%s]\n' % buf.strip().decode('utf8')
                # low level write to original stdout
                os.write(stdout_fno, output.encode('utf8')) 
                buf.clear()
        os.write(stdout_fno, b'tick: %d\n' % counter)
        counter += 1

main()

# cleanup
os.dup2(stdout_fno, 1)
os.close(pipe_fno)
os.unlink(pipename)

执行示例:

$ python demo2.py 
external lib: [vomiting output onto stdout]
tick: 0
tick: 1
tick: 2
tick: 3
external lib: [vomiting output onto stdout]
tick: 4

请注意,所有内容都已被捕获。

现在,由于你确实使用了ncurses 也在一个线程中运行该函数,这有点棘手。这是龙。

我们需要ncurses API,它实际上会让我们创建一个新的屏幕来重定向输出,并且ctypes可以再次使用它。不幸的是,我在我的系统上使用DLL的绝对路径;根据需要进行调整。

lib.py

import ctypes

libc = ctypes.CDLL('/lib64/libc.so.6')
ncurses = ctypes.CDLL('/lib64/libncursesw.so.6')


class FILE(ctypes.Structure):
    pass


class SCREEN(ctypes.Structure):
    pass


FILE_p = ctypes.POINTER(FILE)
libc.fdopen.restype = FILE_p
SCREEN_p = ctypes.POINTER(SCREEN)
ncurses.newterm.restype = SCREEN_p
ncurses.set_term.restype = SCREEN_p
fdopen = libc.fdopen
newterm = ncurses.newterm
set_term = ncurses.set_term
delscreen = ncurses.delscreen
endwin = ncurses.endwin

现在我们有了newtermset_term,我们终于可以完成脚本了。从主函数中删除所有内容,并添加以下内容:

# setup the curse window
import curses
from lib import newterm, fdopen, set_term, endwin, delscreen
stdin_fno = sys.stdin.fileno()
stdscr = curses.initscr()
# use the ctypes library to create a new screen and redirect output
# back to the original stdout
screen = newterm(None, fdopen(stdout_fno, 'w'), fdopen(stdin_fno, 'r'))
old_screen = set_term(screen)
stdscr.clear()
curses.noecho()
border = curses.newwin(8, 68, 4, 4)
border.border()
window = curses.newwin(6, 66, 5, 5)
window.scrollok(True) 
window.clear() 
border.refresh()
window.refresh()

def main():

    thread = Thread(target=external)
    thread.start()

    buf = bytearray()
    counter = 0
    while thread.isAlive():
        sleep(0.2)
        try:
            while True:
                buf += os.read(pipe_fno, 1)
        except BlockingIOError:
            if buf:
                output = 'external lib: [%s]\n' % buf.strip().decode('utf8')
                buf.clear()
                window.addstr(output)
                window.refresh()
        window.addstr('tick: %d\n' % counter)
        counter += 1
        window.refresh()

main()

# cleanup
os.dup2(stdout_fno, 1)
endwin()
delscreen(screen)
os.close(pipe_fno)
os.unlink(pipename)

这应该表明使用ncurses的预期结果可以实现,但是对于我的情况,它最后会挂起,我不知道还有什么可能会发生。我认为这可能是由于在使用64位共享对象时意外使用32位Python引起的,但在退出时,某些东西不能很好地玩(我认为误用ctypes很容易,但是事实证明它确实是!)。无论如何,至少它会像你期望的那样在一个ncurse窗口中显示输出。

答案 1 :(得分:1)

@metatoaster指示link,其中讨论了将标准输出临时重定向到/dev/null的方法。这可能会显示如何使用dup2,但它本身并不是一个答案。

python的curses接口仅使用initscr,这意味着curses库将其输出写入标准输出。 SWIG'd库将其输出写入标准输出,但这会干扰curses输出。你可以通过

解决问题
  • 将curses输出重定向到/dev/tty
  • 将SWIG'd输出重定向到临时文件,
  • 读取文件,检查要添加到屏幕的更新。

一旦调用initscr,curses库就有自己的输出流副本。如果您可以暂时将实际标准输出指向文件(在初始化curses之前),则打开新标准输出到/dev/tty(对于initscr),然后恢复(global!)输出流那应该有用。