How can the terminal output of executables run by Python functions be silenced in a general way?
Asked Answered
C

4

4

I want to suppress all of the terminal output produced by a function that runs executables.

I have attempted to suppress the output of a Python function by using a context manager that temporarily redefines stdout and stderr each time the function is called. This suppresses terminal output produced by print calls in the function, but it doesn't seem to work when the function calls executables that produce terminal output.

So, how could the output of executables called by Python functions be suppressed?

My code is below. I have included an example function that calls ls to try to illustrate the kind of terminal output I want to suppress (though the function I'm dealing with is different).

#!/usr/bin/env python

import os
import subprocess
import sys

def main():

    print("hello")

    with silence():
        print("there")

    print("world")

    with silence():
        engage_command(command = "ls")

class silence(object):

    def __init__(
        self,
        stdout = None,
        stderr = None
        ):
        if stdout == None and stderr == None:
            devnull = open(os.devnull, "w")
            stdout = devnull
            stderr = devnull
        self._stdout = stdout or sys.stdout
        self._stderr = stderr or sys.stderr

    def __enter__(
        self
        ):
        self.old_stdout = sys.stdout
        self.old_stderr = sys.stderr
        self.old_stdout.flush()
        self.old_stderr.flush()
        sys.stdout = self._stdout
        sys.stderr = self._stderr

    def __exit__(
        self,
        exc_type,
        exc_value,
        traceback
        ):
        self._stdout.flush()
        self._stderr.flush()
        sys.stdout = self.old_stdout
        sys.stderr = self.old_stderr

def engage_command(
    command = None
    ):
    process = subprocess.Popen(
        [command],
        shell      = True,
        executable = "/bin/bash")
    process.wait()
    output, errors = process.communicate()
    return output

if __name__ == "__main__":
    main()

In my particular case, I'm trying to run the following function (instead of the ls function above):

with propyte.silence():
    stream = pyaudio.PyAudio().open(
        format   = pyaudio.PyAudio().get_format_from_width(1),
        channels = 1, 
        rate     = bitrate, 
        output   = True
    )

When run, this produces output like the following:

ALSA lib pcm_dsnoop.c:606:(snd_pcm_dsnoop_open) unable to open slave
ALSA lib pcm_dmix.c:1029:(snd_pcm_dmix_open) unable to open slave
ALSA lib pcm.c:2266:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.rear
ALSA lib pcm.c:2266:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.center_lfe
ALSA lib pcm.c:2266:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.side
ALSA lib pcm_dmix.c:1029:(snd_pcm_dmix_open) unable to open slave
Cannot connect to server socket err = No such file or directory
Cannot connect to server request channel
jack server is not running or cannot be started
JackShmReadWritePtr::~JackShmReadWritePtr - Init not done for 4294967295, skipping unlock
JackShmReadWritePtr::~JackShmReadWritePtr - Init not done for 4294967295, skipping unlock

I want to suppress that output.


EDIT: testing a solution provided by @Matthias

#!/usr/bin/env python

import contextlib
import os
import subprocess
import sys

def main():

    print("hello")

    with silence():
        print("there")

    print("world")

    with silence():
        engage_command(command = "ls")

@contextlib.contextmanager
def silence():
    devnull = os.open(os.devnull, os.O_WRONLY)
    old_stderr = os.dup(2)
    sys.stderr.flush()
    os.dup2(devnull, 2)
    os.close(devnull)
    try:
        yield
    finally:
        os.dup2(old_stderr, 2)
        os.close(old_stderr)

def engage_command(
    command = None
    ):
    process = subprocess.Popen(
        [command],
        shell      = True,
        executable = "/bin/bash")
    process.wait()
    output, errors = process.communicate()
    return output

if __name__ == "__main__":
    main()

I have not been successful in suppressing the terminal output from the print or the ls and I'm not sure why.

Corin answered 30/4, 2016 at 14:27 Comment(0)
H
6

You could switch from PyAudio to the sounddevice module, which already takes care of silencing the terminal output (see #12). This is how it is done there (using CFFI):

from cffi import FFI
import os

ffi = FFI()
ffi.cdef("""
/* from stdio.h */
FILE* fopen(const char* path, const char* mode);
int fclose(FILE* fp);
FILE* stderr;  /* GNU C library */
FILE* __stderrp;  /* Mac OS X */
""")
try:
    stdio = ffi.dlopen(None)
    devnull = stdio.fopen(os.devnull.encode(), b'w')
except OSError:
    return
try:
    stdio.stderr = devnull
except KeyError:
    try:
        stdio.__stderrp = devnull
    except KeyError:
        stdio.fclose(devnull)

If you want a pure Python solution, you can try this context manager:

import contextlib
import os
import sys

@contextlib.contextmanager
def ignore_stderr():
    devnull = os.open(os.devnull, os.O_WRONLY)
    old_stderr = os.dup(2)
    sys.stderr.flush()
    os.dup2(devnull, 2)
    os.close(devnull)
    try:
        yield
    finally:
        os.dup2(old_stderr, 2)
        os.close(old_stderr)

This is a very helpful blog post about the topic: http://eli.thegreenplace.net/2015/redirecting-all-kinds-of-stdout-in-python/.


UPDATE:

The context manager above silences the standard error output (stderr), which is used for the annoying messages from PortAudio mentioned in the original question. To get rid of the standard output (stdout), as in your updated question, you'll have to replace sys.stderr with sys.stdout and the file descriptor 2 with the number 1:

@contextlib.contextmanager
def ignore_stdout():
    devnull = os.open(os.devnull, os.O_WRONLY)
    old_stdout = os.dup(1)
    sys.stdout.flush()
    os.dup2(devnull, 1)
    os.close(devnull)
    try:
        yield
    finally:
        os.dup2(old_stdout, 1)
        os.close(old_stdout)
Homeland answered 1/5, 2016 at 11:57 Comment(2)
Thank you very much for your detailed solution. I'll investigate the sounddevice module as suggested and will examine its solution for silencing PortAudio. For the pure, general approach to silencing the terminal completely (whether that be for Python output or executables launched by Python) that you suggested, I have not been successful in getting it to suppress the terminal output. I've put my code in an edit to the question. Would you have any idea about how to change it so that the terminal output of the print and the ls is suppressed?Corin
Ah, that's it. There was a need to handle stdout. Thanks again for your clear and detailed solution.Corin
H
6

For the ALSA errors in particular, you can use ALSA's snd_lib_error_set_handler function as described for example in this question.

For one of my projects, I created a module called mute_alsa, which looks like this:

import ctypes

ERROR_HANDLER_FUNC = ctypes.CFUNCTYPE(None, ctypes.c_char_p, ctypes.c_int,
                                      ctypes.c_char_p, ctypes.c_int,
                                      ctypes.c_char_p)


def py_error_handler(filename, line, function, err, fmt):
    pass

c_error_handler = ERROR_HANDLER_FUNC(py_error_handler)

try:
    asound = ctypes.cdll.LoadLibrary('libasound.so.2')
    asound.snd_lib_error_set_handler(c_error_handler)
except OSError:
    pass

And you use it simply by putting the following into your code:

import mute_alsa

In a more general sense, you can prevent anything from printing to stderr by simply closing the associated file descriptor. For example, if we try to rm a file that doesn't exist, we get an error message printed to stderr:

>>> import sys
>>> import os
>>> os.system('rm /doesnotexist')
rm: cannot remove ‘/doesnotexist’: Is a directory
256

If we close the stderr file descriptor:

>>> os.close(sys.stderr.fileno())

We no longer see any error messages:

>>> os.system('rm /doesnotexist')

The downside to this approach is that it silences all messages to stderr, which means you will no longer see legitimate error messages. This may lead to surprising failure modes (the program exited without printing any errors!).

Homing answered 30/4, 2016 at 17:41 Comment(2)
Hey, thank you very much for your detailed solution (and for the preemptive ALSA mute suggestion). When I put os.close(sys.stdout.fileno()) and os.close(sys.stderr.fileno()) in the enter or init functions of the context manager above, the terminal output doesn't seem to get suppressed. Can you see what I'm missing?Corin
Is there a way of getting the varg args part on the python handler? The handler receives the 'fmt' string, but not the params sent to the original ALSA error handler. I don't want to mute the errors, just log them once for each occurrence.Humfrid
H
6

You could switch from PyAudio to the sounddevice module, which already takes care of silencing the terminal output (see #12). This is how it is done there (using CFFI):

from cffi import FFI
import os

ffi = FFI()
ffi.cdef("""
/* from stdio.h */
FILE* fopen(const char* path, const char* mode);
int fclose(FILE* fp);
FILE* stderr;  /* GNU C library */
FILE* __stderrp;  /* Mac OS X */
""")
try:
    stdio = ffi.dlopen(None)
    devnull = stdio.fopen(os.devnull.encode(), b'w')
except OSError:
    return
try:
    stdio.stderr = devnull
except KeyError:
    try:
        stdio.__stderrp = devnull
    except KeyError:
        stdio.fclose(devnull)

If you want a pure Python solution, you can try this context manager:

import contextlib
import os
import sys

@contextlib.contextmanager
def ignore_stderr():
    devnull = os.open(os.devnull, os.O_WRONLY)
    old_stderr = os.dup(2)
    sys.stderr.flush()
    os.dup2(devnull, 2)
    os.close(devnull)
    try:
        yield
    finally:
        os.dup2(old_stderr, 2)
        os.close(old_stderr)

This is a very helpful blog post about the topic: http://eli.thegreenplace.net/2015/redirecting-all-kinds-of-stdout-in-python/.


UPDATE:

The context manager above silences the standard error output (stderr), which is used for the annoying messages from PortAudio mentioned in the original question. To get rid of the standard output (stdout), as in your updated question, you'll have to replace sys.stderr with sys.stdout and the file descriptor 2 with the number 1:

@contextlib.contextmanager
def ignore_stdout():
    devnull = os.open(os.devnull, os.O_WRONLY)
    old_stdout = os.dup(1)
    sys.stdout.flush()
    os.dup2(devnull, 1)
    os.close(devnull)
    try:
        yield
    finally:
        os.dup2(old_stdout, 1)
        os.close(old_stdout)
Homeland answered 1/5, 2016 at 11:57 Comment(2)
Thank you very much for your detailed solution. I'll investigate the sounddevice module as suggested and will examine its solution for silencing PortAudio. For the pure, general approach to silencing the terminal completely (whether that be for Python output or executables launched by Python) that you suggested, I have not been successful in getting it to suppress the terminal output. I've put my code in an edit to the question. Would you have any idea about how to change it so that the terminal output of the print and the ls is suppressed?Corin
Ah, that's it. There was a need to handle stdout. Thanks again for your clear and detailed solution.Corin
F
3

Just add the following code:

import sounddevice

If you need to install:

python3 -m pip install sounddevice
Filling answered 4/4, 2022 at 17:40 Comment(2)
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.Aria
Pretty clear to me if you understand Python.Hamm
C
0

The easiest way to silence these executables without silencing your terminal is to import the sounddevice module.

pip -m install sounddevice

If you want to know more details about the sounddevice model read the documentation: https://python-sounddevice.readthedocs.io/en/0.4.6/index.html

Summary of (sounddevice) Module:

It "provides bindings for the PortAudio library and a few convenience functions to play and record NumPy arrays containing audio signals" (python-sounddevice, version 0.4.6, 2019).

My Working Code:

import pyaudio
import wave
import sounddevice

FRAMES_PER_BUFFER = 3200
FORMAT = pyaudio.paInt16
CHANNELS = 1
RATE = 16000

p = pyaudio.PyAudio()

stream = p.open(
        format=FORMAT,
        channels=CHANNELS,
        rate=RATE,
        input=True,
        frames_per_buffer=FRAMES_PER_BUFFER
)

print("start recording")

Output Before Import:

ALSA lib pcm.c:2664:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.rear
ALSA lib pcm.c:2664:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.center_lfe
ALSA lib pcm.c:2664:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.side

@Rafhael P. Zrenner Thanks!!!! Because of your quick solution, I can continue working on my AI assistant using the pyttsx3 and SpeechRecognition modules.

Conjoint answered 6/4, 2023 at 20:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.