Comment puis-je empêcher une bibliothèque partagée en C d'imprimer sur stdout en python?

je travaille avec une lib python qui importe une bibliothèque C partagée qui imprime sur stdout. Je veux une propre production, afin de l'utiliser avec des tuyaux ou pour rediriger dans des fichiers. Les impressions sont faites en dehors de python, dans la bibliothèque partagée.

Au début, mon approche était:

# file: test.py
import os
from ctypes import *
from tempfile import mktemp

libc = CDLL("libc.so.6")

print # That's here on purpose, otherwise hello word is always printed

tempfile = open(mktemp(),'w')
savestdout = os.dup(1)
os.close(1)
if os.dup(tempfile.fileno()) != 1:
    assert False, "couldn't redirect stdout - dup() error"

# let's pretend this is a call to my library
libc.printf("hello worldn")

os.close(1)
os.dup(savestdout)
os.close(savestdout)

Cette première approche est la moitié de travail:

- Pour une raison quelconque, il a besoin d'une déclaration "imprimer" juste avant de déplacer stdout, sinon hello word est toujours imprimé. En conséquence, il sera imprimez une ligne vide au lieu de tous les fuzz que la bibliothèque affiche habituellement.

- Plus gênant, il échoue lors de la redirection vers un fichier:

$python test.py > foo && cat foo

hello world

ma deuxième tentative de python a été inspirée d'un autre fil similaire donné dans les commentaires:

import os
import sys
from ctypes import *
libc = CDLL("libc.so.6")

devnull = open('/dev/null', 'w')
oldstdout = os.dup(sys.stdout.fileno())
os.dup2(devnull.fileno(), 1)

# We still pretend this is a call to my library
libc.printf("hellon")

os.dup2(oldstdout, 1)

celui-ci ne parvient pas non plus à empêcher "hello" d'être imprimé.

comme j'ai senti que c'était un peu bas, j'ai alors décidé d'aller complètement avec des ctypes. Je me suis inspiré de ce programme C, qui ne ne pas imprimer quoi que ce soit:

#include <stdio.h>

int main(int argc, const char *argv[]) {
    char buf[20];
    int saved_stdout = dup(1);
    freopen("/dev/null", "w", stdout);

    printf("hellon"); // not printed

    sprintf(buf, "/dev/fd/%d", saved_stdout);
    freopen(buf, "w", stdout);

    return 0;
}

j'ai construit l'exemple suivant:

from ctypes import *
libc = CDLL("libc.so.6")

saved_stdout = libc.dup(1)
stdout = libc.fdopen(1, "w")
libc.freopen("/dev/null", "w", stdout);

libc.printf("hellon")

libc.freopen("/dev/fd/" + str(saved_stdout), "w", stdout)

ceci imprime "hello", même si je libc.fflush(stdout) juste après le printf. Je commence à penser qu'il pourrait ne pas être possible de faire ce que je veux en python. Ou peut-être que la façon dont j'obtiens un pointeur de dossier à stdout n'est pas juste.

Qu'en pensez-vous?

37
demandé sur user48678 2011-02-22 20:32:35

5 réponses

basé sur la réponse de@Yinon Ehrlich. Cette variante essaie d'éviter les fuites de descripteurs de fichier:

import os
import sys
from contextlib import contextmanager

@contextmanager
def stdout_redirected(to=os.devnull):
    '''
    import os

    with stdout_redirected(to=filename):
        print("from Python")
        os.system("echo non-Python applications are also supported")
    '''
    fd = sys.stdout.fileno()

    ##### assert that Python and C stdio write using the same file descriptor
    ####assert libc.fileno(ctypes.c_void_p.in_dll(libc, "stdout")) == fd == 1

    def _redirect_stdout(to):
        sys.stdout.close() # + implicit flush()
        os.dup2(to.fileno(), fd) # fd writes to 'to' file
        sys.stdout = os.fdopen(fd, 'w') # Python writes to fd

    with os.fdopen(os.dup(fd), 'w') as old_stdout:
        with open(to, 'w') as file:
            _redirect_stdout(to=file)
        try:
            yield # allow code to be run with the redirected stdout
        finally:
            _redirect_stdout(to=old_stdout) # restore stdout.
                                            # buffering and flags such as
                                            # CLOEXEC may be different
18
répondu jfs 2017-05-23 12:03:05

Ouais, vous voulez vraiment utiliser os.dup2 au lieu de os.dup, comme votre seconde idée. Votre code semble un peu rond-point. Ne pas bricolons avec /dev entrées sauf pour /dev/null, c'est inutile. Il est également inutile d'écrire quoi que ce soit ici.

le truc est de sauver le stdout fdes à l'aide de dup, puis passez à fdopen pour la sys.stdout objet Python. En attendant, ouvrez un fdes à /dev/null et utiliser dup2 pour remplacer l'existant stdout fdes. Alors fermez les vieux fdes à /dev/null. L'appel à dup2 est nécessaire parce que nous ne pouvons pas dire open qui fdes nous voulons qu'il revienne, dup2 est vraiment la seule façon de le faire.

Edit: et si vous redirigez vers un fichier, alors stdout n'est pas doté d'un tampon de ligne, vous devez le vider. Vous pouvez le faire à partir de Python et il interopèrera avec C correctement. Bien sûr, si vous appelez cette fonction avant d'écrire quoi que ce soit stdout, alors ce n'est pas question.

Voici un exemple que je viens de testé qui fonctionne sur mon système.

import zook
import os
import sys

def redirect_stdout():
    print "Redirecting stdout"
    sys.stdout.flush() # <--- important when redirecting to files
    newstdout = os.dup(1)
    devnull = os.open(os.devnull, os.O_WRONLY)
    os.dup2(devnull, 1)
    os.close(devnull)
    sys.stdout = os.fdopen(newstdout, 'w')

zook.myfunc()
redirect_stdout()
zook.myfunc()
print "But python can still print to stdout..."

le module" zook " est une bibliothèque très simple en C.

#include <Python.h>
#include <stdio.h>

static PyObject *
myfunc(PyObject *self, PyObject *args)
{
    puts("myfunc called");
    Py_INCREF(Py_None);
    return Py_None;
}

static PyMethodDef zookMethods[] = {
    {"myfunc",  myfunc, METH_VARARGS, "Print a string."},
    {NULL, NULL, 0, NULL}
};

PyMODINIT_FUNC
initzook(void)
{
    (void)Py_InitModule("zook", zookMethods);
}

Et la sortie?

$ python2.5 test.py
myfunc called
Redirecting stdout
But python can still print to stdout...

et la redirection vers des fichiers?

$ python2.5 test.py > test.txt
$ cat test.txt
myfunc called
Redirecting stdout
But python can still print to stdout...
15
répondu Dietrich Epp 2013-02-10 10:55:18

Combinant à la fois des réponses - https://stackoverflow.com/a/5103455/1820106 & https://stackoverflow.com/a/4178672/1820106 gestionnaire de contexte qui bloque imprimer sur la sortie standard stdout seulement de son champ d'application (le code dans la première réponse, bloqué toute sortie externe, la dernière réponse raté le sys.la sortie standard stdout.flush() à la fin):

class HideOutput(object):
    '''
    A context manager that block stdout for its scope, usage:

    with HideOutput():
        os.system('ls -l')
    '''

    def __init__(self, *args, **kw):
        sys.stdout.flush()
        self._origstdout = sys.stdout
        self._oldstdout_fno = os.dup(sys.stdout.fileno())
        self._devnull = os.open(os.devnull, os.O_WRONLY)

    def __enter__(self):
        self._newstdout = os.dup(1)
        os.dup2(self._devnull, 1)
        os.close(self._devnull)
        sys.stdout = os.fdopen(self._newstdout, 'w')

    def __exit__(self, exc_type, exc_val, exc_tb):
        sys.stdout = self._origstdout
        sys.stdout.flush()
        os.dup2(self._oldstdout_fno, 1)
10
répondu Yinon Ehrlich 2017-05-23 12:18:19

voici ce que j'ai finalement fait. J'espère que cela peut être utile pour d'autres personnes (cela fonctionne sur ma station linux).

je vous présente fièrement le libshutup, conçu pour faire taire les bibliothèques externes.

1) copier le fichier suivant

// file: shutup.c
#include <stdio.h>
#include <unistd.h>

static char buf[20];
static int saved_stdout;

void stdout_off() {
    saved_stdout = dup(1);
    freopen("/dev/null", "w", stdout);
}

void stdout_on() {
    sprintf(buf, "/dev/fd/%d", saved_stdout);
    freopen(buf, "w", stdout);
}

2) compilez-le comme une bibliothèque partagée

gcc -Wall -shared shutup.c -fPIC -o libshutup.so

3) Utilisez-le dans votre code comme ceci

from ctypes import *
shutup = CDLL("libshutup.so")

shutup.stdout_off()

# Let's pretend this printf comes from the external lib
libc = CDLL("libc.so.6")
libc.printf("hello\n")

shutup.stdout_on()
4
répondu user48678 2011-02-24 10:17:52

ne seriez-vous pas capable de faire la même chose qu'en Python? Vous importeriez sys et point sys.stdout et sys.stderr à quelque chose qui n'est pas le système par défaut.stdout et sys.stderr? Je le fais tout le temps dans quelques applications où je engloutissent la sortie à partir d'une bibliothèque.

-2
répondu Jeremy Whitlock 2011-02-23 00:17:04