Skip to main content

Nogil subprocess for Python with stdout/stderr capturing and stdin writing - without deadlocking!

Project description

Nogil subprocess for Python with stdout/stderr capturing and stdin writing - without deadlocking!

What is it?

cynonblockingsubprocess is a Cython-based wrapper around a C++ class called ShellProcessManager, defined in nonblockingsubprocess.hpp. Its purpose is to start and manage a shell process (e.g., /bin/bash, "C:\\Windows\\System32\\cmd.exe", or any other command-line application), allowing you to:

  • Write commands to the shell’s standard input.
  • Read from the shell’s standard output and standard error without risking deadlocks.
  • Run two background threads to non-blockingly capture stdout and stderr in nogil mode (reducing Python GIL contention).
  • Optionally print stdout and/or stderr in real time while still retaining the captured output.

Features

  • Non-blocking I/O: Spawns separate threads to read stdout and stderr in the background, preventing the parent process from freezing or deadlocking.
  • Configurable Buffers: Control the maximum length of captured stdout and stderr buffers before data is truncated, similar to a ring buffer (or collections.deque).
  • OS Compatibility:
    • On Windows, you can customize process creation flags and environment parameters.
    • On Linux/Unix, the Windows-specific parameters are ignored, but the shell process is still managed the same way.
  • Graceful Shutdown: Call stop_shell() to terminate the subprocess and background threads safely. When using subprocess.Popen, shells might keep open in the background, even after calling .kill() or .terminate(). This should not happen here! The C++ destructor takes care of closing the shell.

Installation and Requirements

pip install cynonblockingsubprocess

  • Cython (to compile the .pyx / .pxd file).
  • A C++ compiler (minimum version 11) compatible with your platform (e.g., MSVC on Windows or g++/clang++ on Linux).
  • The code will compile the first time you import it

Usage example Python

from cynonblockingsubprocess import CySubProc
from time import sleep
from platform import platform

iswindows = "win" in platform().lower()


# shell_command (bytes or str): Path or command to start (e.g., "C:\\Windows\\System32\\cmd.exe" or "/bin/bash").
# buffer_size (int): Size of the internal buffer for reading output (default 4096).
# stdout_max_len (int): Maximum amount of data retained in the stdout buffer (default 4096).
# stderr_max_len (int): Maximum amount of data retained in the stderr buffer (default 4096).
# exit_command (bytes or str): Command used internally to gracefully stop the shell (default b"exit").
# print_stdout (bool): If True, prints all stdout in real time to the console (default False).
# print_stderr (bool): If True, prints all stderr in real time to the console (default False).

tete = CySubProc(
    shell_command="C:\\Windows\\System32\\cmd.exe" if iswindows else "/bin/bash", # cross plattform
    buffer_size=4096,
    stdout_max_len=4096,
    stderr_max_len=4096,
    exit_command=b"exit",
    print_stdout=False,
    print_stderr=False,
)

# Start the shell process with specific creation flags and environment parameters.
# On Posix, all arguments are ignored!

# Detailed information can be found on Microsoft's website https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags

# Parameters
# ----------
# creationFlag : int, optional
#     A custom flag for process creation, by default 0.
# creationFlags : int, optional
#     Additional flags controlling process creation, by default 0x08000000.
# wShowWindow : int, optional
#     Flags controlling how the window is shown (e.g., hidden, normal, minimized),
#     by default 1 (SW_SHOWNORMAL).
# lpReserved : bytes or str, optional
#     Reserved parameter for process creation, by default None.
# lpDesktop : bytes or str, optional
#     The name of the desktop for the process, by default None.
# lpTitle : bytes or str, optional
#     The title for the new console window, by default None.
# dwX : int, optional
#     X-coordinate for the upper-left corner of the window, by default 0.
# dwY : int, optional
#     Y-coordinate for the upper-left corner of the window, by default 0.
# dwXSize : int, optional
#     Width of the window, by default 0.
# dwYSize : int, optional
#     Height of the window, by default 0.
# dwXCountChars : int, optional
#     Screen buffer width in character columns, by default 0.
# dwYCountChars : int, optional
#     Screen buffer height in character rows, by default 0.
# dwFillAttribute : int, optional
#     Initial text and background colors if used in a console, by default 0.
# dwFlags : int, optional
#     Flags that control how the creationFlags are used, by default 0.
# cbReserved2 : int, optional
#     Reserved for C runtime initialization, by default 0.
# lpReserved2 : bytes or str, optional
#     Reserved for C runtime initialization, by default None.

tete.start_shell() # This function must be always called first, if not, you probably will get segmentation faults!


# Write a command or input data to the shell process's stdin.

# Parameters
# ----------
# cmd : bytes or str
#     The command or data to send to the process via stdin.
tete.stdin_write("ls -l") # Writing an existing command 
sleep(1) # Wait a little for the output


# Retrieve the current contents of the shell's standard output as bytes, and clears the C++ vector

# Returns
# -------
# bytes
#     The raw bytes from the shell's stdout.
print(tete.get_stdout().decode()) # there will be something here


# Retrieve the current contents of the shell's standard error as bytes, and clears the C++ vector.

# Returns
# -------
# bytes
#     The raw bytes from the shell's stderr.
print(tete.get_stderr().decode()) # will be empty
tete.stdin_write("lxs xx-lxxxx") # command does not exist
sleep(1)
print(tete.get_stdout())  # will be empty
print(tete.get_stderr()) # there will be something here
del tete # closes the shell automatically! If you want, you can also call proc.stop_shell()

Usage example C++ (stack)

#include "nonblockingsubprocess.hpp"

int main(int argc, char *argv[])
{
    while (true)
    {
#ifdef _WIN32
        std::string shellcmd = "C:\\Windows\\System32\\cmd.exe";
#else
        std::string shellcmd = "/bin/bash";
#endif
        // arguments: std::string shell_command, size_t buffer_size = 4096, size_t stdout_max_len = 4096, size_t stderr_max_len = 4096, std::string exit_command = "exit", int print_stdout = 1, int print_stderr = 1
        ShellProcessManager proc{shellcmd, 4096, 4096, 4096, "exit", 1, 1};
        bool resultproc = proc.start_shell(); //optional arguments for Windows: DWORD creationFlag = 0, DWORD creationFlags = CREATE_NO_WINDOW, WORD wShowWindow = SW_NORMAL, LPSTR lpReserved = nullptr, LPSTR lpDesktop = nullptr, LPSTR lpTitle = nullptr, DWORD dwX = 0, DWORD dwY = 0, DWORD dwXSize = 0, DWORD dwYSize = 0, DWORD dwXCountChars = 0, DWORD dwYCountChars = 0, DWORD dwFillAttribute = 0, DWORD dwFlags = 0, WORD cbReserved2 = 0, LPBYTE lpReserved2 = nullptr
        std::cout << "resultproc: " << resultproc << std::endl;
        proc.stdin_write("ls -l");
        sleepcp(100);
        auto val = proc.get_stdout();
        std::cout << "stdout: " << val << std::endl;
        sleepcp(100);
        proc.stdin_write("ls -l");
        sleepcp(100);
        auto val2 = proc.get_stdout();
        std::cout << "stderr: " << val << std::endl;
        proc.stop_shell(); // optional: automatically called by the destructor
        sleepcp(1000);
    }
}

Usage example C++ (heap)

#include "nonblockingsubprocess.hpp"

int main(int argc, char *argv[])
{
    while (true)
    {
#ifdef _WIN32
        std::string shellcmd = "C:\\Windows\\System32\\cmd.exe";
#else
        std::string shellcmd = "/bin/bash";
#endif

        ShellProcessManager *proc = new ShellProcessManager{shellcmd, 4096, 4096, 4096, "exit", 1, 1};
        bool resultproc = proc->start_shell();
        std::cout << "resultproc: " << resultproc << std::endl;
        proc->stdin_write("ls -l");
        sleepcp(100);
        auto val = proc->get_stdout();
        std::cout << "v1111111111: " << val << std::endl;
        sleepcp(100);
        proc->stdin_write("ls -l");
        sleepcp(100);
        auto val2 = proc->get_stdout();
        std::cout << "v2222222222: " << val << std::endl;
        proc->stop_shell();
        sleepcp(1000);
    }
    delete proc
    std::cin.get();
}

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cynonblockingsubprocess-0.10.tar.gz (17.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cynonblockingsubprocess-0.10-py3-none-any.whl (19.0 kB view details)

Uploaded Python 3

File details

Details for the file cynonblockingsubprocess-0.10.tar.gz.

File metadata

  • Download URL: cynonblockingsubprocess-0.10.tar.gz
  • Upload date:
  • Size: 17.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.2 CPython/3.11.7

File hashes

Hashes for cynonblockingsubprocess-0.10.tar.gz
Algorithm Hash digest
SHA256 1b0d3f6dc7729f540cbc896c365d9c17d888bb775de7f26dd76870c7ec2e61a3
MD5 17317a0108f516eaa5fac80c9636a6bc
BLAKE2b-256 6886b003e8255906aebdcc8ce2026e2bad36fb32585eb0df3c9527253f84bfa6

See more details on using hashes here.

File details

Details for the file cynonblockingsubprocess-0.10-py3-none-any.whl.

File metadata

File hashes

Hashes for cynonblockingsubprocess-0.10-py3-none-any.whl
Algorithm Hash digest
SHA256 ed267a7743b2e6018b3ade013bc147ac3e13b184eca0e1c621d2596f7e835cbe
MD5 1862acf2ef1c8cb418bf768ee7ddadea
BLAKE2b-256 588233a817d465fa1eaccf2af6af2eaaf8db39e810858cff0e6ab1d63221fdc9

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page