Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
New Pipecom Update
#1
Lightbulb 
Pipecom has always been a cross-platform utility for getting the stdout, stderr, and exit code of a shelled program without using temp files. On Windows, it was perfect. On Mac and Linux, there was still a temp file for the stderr handle. This was not ideal and some of y'all complained extensively. Anyways, I decided to do a rewrite of pipecom. I have not tested it on Mac, but it works great on Linux (Crostini). The following code should provide the same functionality y'all enjoyed with Windows for all three operating systems (provided someone can test Mac and verify it works). Happy Shelling.

-Spriggsy
Screenshot of Linux environment test:
   
Edit: I realized that the forum truncated the links in some of the comments. Here is the full file to download: 
.bas   pipecom.bas (Size: 21.21 KB / Downloads: 31)
P.S., I had Gemini do the commenting because I could not be bothered to do so.
Code: (Select All)

'================================================================================
' Pipecom - A Cross-Platform Process Capture Library for QB64
' Version: 2.0 (Native Linux/Mac Implementation)
' Author: Zachary Spriggs
'
' This library provides a single function, pipecom&, to execute a shell
' command and capture its STDOUT, STDERR, and Exit Code on both
' Windows and POSIX (Linux/Mac) systems.
'
' This version uses native POSIX calls on Linux/Mac, removing the
' previous dependency on popen and temporary files for stderr.
'================================================================================
$IncludeOnce
Function pipecom& (cmd As String, stdout As String, stderr As String)
    ' Initialize output strings
    stdout = "": stderr = ""

    '========================================================================
    ' WINDOWS IMPLEMENTATION
    '========================================================================
    $If WINDOWS Then
            ' --- Win32 API Type Definitions ---

            ' https://learn.microsoft.com/en-us/window...attributes
            Type SECURITY_ATTRIBUTES
                As _Unsigned Long nLength
        $If 64BIT Then
                    As String * 4 padding ' Align for 64-bit
        $End If
                As _Offset lpSecurityDescriptor
                As Long bInheritHandle
        $If 64BIT Then
                    As String * 4 padding2 ' Align for 64-bit
        $End If
            End Type

            ' https://learn.microsoft.com/en-us/window...artupinfoa
            Type STARTUPINFO
                As Long cb
        $If 64BIT Then
                    As String * 4 padding
        $End If
                As _Offset lpReserved, lpDesktop, lpTitle
                As _Unsigned Long dwX, dwY, dwXSize, dwYSize, dwXCountChars, dwYCountChars, dwFillAttribute, dwFlags
                As _Unsigned Integer wShowWindow, cbReserved2
        $If 64BIT Then
                    As String * 4 padding2
        $End If
                As _Offset lpReserved2, hStdInput, hStdOutput, hStdError
            End Type

            ' https://learn.microsoft.com/en-us/window...nformation
            Type PROCESS_INFORMATION
                As _Offset hProcess, hThread
                As _Unsigned Long dwProcessId
        $If 64BIT Then
                    As String * 4 padding
        $End If
            End Type

            ' --- Win32 API Constants ---
            Const STARTF_USESTDHANDLES = &H00000100 ' Use hStdInput, hStdOutput, hStdError
            Const CREATE_NO_WINDOW = &H8000000    ' Don't create a console window
            Const INFINITE = 4294967295            ' Wait forever
            Const WAIT_FAILED = &HFFFFFFFF          ' Return value for Wait error

            ' --- Win32 API Function Declarations ---
            Declare CustomType Library
                ' https://learn.microsoft.com/en-us/window...createpipe
                Function CreatePipe& (ByVal hReadPipe As _Offset, Byval hWritePipe As _Offset, Byval lpPipeAttributes As _Offset, Byval nSize As _Unsigned Long)
               
                ' https://learn.microsoft.com/en-us/window...teprocessa
                Function CreateProcess& (ByVal lpApplicationName As _Offset, Byval lpCommandLine As _Offset, Byval lpProcessAttributes As _Offset, Byval lpThreadAttributes As _Offset, Byval bInheritHandles As Long, Byval dwCreationFlags As _Unsigned Long, Byval lpEnvironment As _Offset, Byval lpCurrentDirectory As _Offset, Byval lpStartupInfo As _Offset, Byval lpProcessInformation As _Offset)
               
                ' https://learn.microsoft.com/en-us/window...teprocessw
                Function CreateProcessW& (ByVal lpApplicationName As _Offset, Byval lpCommandLine As _Offset, Byval lpProcessAttributes As _Offset, Byval lpThreadAttributes As _Offset, Byval bInheritHandles As Long, Byval dwCreationFlags As _Unsigned Long, Byval lpEnvironment As _Offset, Byval lpCurrentDirectory As _Offset, Byval lpStartupInfo As _Offset, Byval lpProcessInformation As _Offset)
               
                ' https://learn.microsoft.com/en-us/window...odeprocess
                Function GetExitCodeProcess& (ByVal hProcess As _Offset, Byval lpExitCode As _Offset)
               
                ' https://learn.microsoft.com/en-us/window...losehandle
                Sub HandleClose Alias "CloseHandle" (ByVal hObject As _Offset)
               
                ' https://learn.microsoft.com/en-us/window...i-readfile
                Function ReadFile& (ByVal hFile As _Offset, Byval lpBuffer As _Offset, Byval nNumberOfBytesToRead As _Unsigned Long, Byval lpNumberOfBytesRead As _Offset, Byval lpOverlapped As _Offset)
               
                ' https://learn.microsoft.com/en-us/window...ngleobject
                Function WaitForSingleObject~& (ByVal hHandle As _Offset, Byval dwMilliseconds As _Unsigned Long)
            End Declare

            Dim As Long ok: ok = 1
            Dim As _Offset hStdOutPipeRead, hStdOutPipeWrite, hStdReadPipeError, hStdOutPipeError
           
            ' Set up Security Attributes for inheritable pipe handles
            Dim As SECURITY_ATTRIBUTES sa
            sa.nLength = Len(sa): sa.lpSecurityDescriptor = 0: sa.bInheritHandle = 1

            ' Create the pipe for STDOUT
            If CreatePipe(_Offset(hStdOutPipeRead), _Offset(hStdOutPipeWrite), _Offset(sa), 0) = 0 Then
                pipecom = -1
                Exit Function
            End If

            ' Create the pipe for STDERR
            If CreatePipe(_Offset(hStdReadPipeError), _Offset(hStdOutPipeError), _Offset(sa), 0) = 0 Then
                pipecom = -1
                Exit Function
            End If

            ' Set up STARTUPINFO to redirect the new process's std handles
            Dim As STARTUPINFO si
            si.cb = Len(si)
            si.dwFlags = STARTF_USESTDHANDLES
            si.hStdError = hStdOutPipeError    ' Redirect stderr to our pipe
            si.hStdOutput = hStdOutPipeWrite  ' Redirect stdout to our pipe
            si.hStdInput = 0
           
            Dim As PROCESS_INFORMATION procinfo
            Dim As _Offset lpApplicationName
            Dim As String lpCommandLine
           
            ' Prepend "cmd /c " to execute the command in a shell
            ' and add a null terminator for the C API.
            lpCommandLine = "cmd /c " + cmd + Chr$(0)
           
            Dim As _Offset lpProcessAttributes, lpThreadAttributes
            Dim As Long bInheritHandles: bInheritHandles = 1 ' Must be 1 to inherit pipes
            Dim As _Unsigned Long dwCreationFlags: dwCreationFlags = CREATE_NO_WINDOW
            Dim As _Offset lpEnvironment, lpCurrentDirectory

            ' Create the child process
            ok = CreateProcess(lpApplicationName, _Offset(lpCommandLine), lpProcessAttributes, lpThreadAttributes, bInheritHandles, dwCreationFlags, lpEnvironment, lpCurrentDirectory, _Offset(si), _Offset(procinfo))

            If ok = 0 Then
                pipecom = -1
                Exit Function
            End If

            ' Close the "write" ends of the pipes in the parent process.
            ' The child process now holds the only copies.
            ' This is crucial, or ReadFile will never finish.
            HandleClose hStdOutPipeWrite
            HandleClose hStdOutPipeError

            ' Read loop for STDOUT
            Dim As String buf: buf = Space$(4096 + 1)
            Dim As _Unsigned Long dwRead
            While ReadFile(hStdOutPipeRead, _Offset(buf), 4096, _Offset(dwRead), 0) <> 0 And dwRead > 0
                buf = Mid$(buf, 1, dwRead)
                GoSub RemoveChr13 ' Remove carriage returns
                stdout = stdout + buf
                buf = Space$(4096 + 1)
            Wend

            ' Read loop for STDERR
            While ReadFile(hStdReadPipeError, _Offset(buf), 4096, _Offset(dwRead), 0) <> 0 And dwRead > 0
                buf = Mid$(buf, 1, dwRead)
                GoSub RemoveChr13 ' Remove carriage returns
                stderr = stderr + buf
                buf = Space$(4096 + 1)
            Wend

            ' Wait for the child process to terminate
            Dim As Long exit_code, ex_stat
            If WaitForSingleObject(procinfo.hProcess, INFINITE) <> WAIT_FAILED Then
                ' Get the process's exit code
                If GetExitCodeProcess(procinfo.hProcess, _Offset(exit_code)) Then
                    ex_stat = 1
                End If
            End If

            ' Clean up remaining handles
            HandleClose hStdOutPipeRead
            HandleClose hStdReadPipeError
           
            ' Return the exit code
            If ex_stat = 1 Then
                pipecom = exit_code
            Else
                pipecom = -1
            End If

            Exit Function

            ' Helper routine to strip Chr$(13) for Windows
            RemoveChr13:
            Dim As Long j
            j = InStr(buf, Chr$(13))
            Do While j
                buf = Left$(buf, j - 1) + Mid$(buf, j + 1)
                j = InStr(buf, Chr$(13))
            Loop
            Return

        '========================================================================
        ' POSIX (LINUX / MAC) IMPLEMENTATION
        '========================================================================
    $Else
        ' --- POSIX API Function Declarations ---
        Declare CustomType Library
            ' https://man7.org/linux/man-pages/man2/pipe.2.html
            Function pipe& (fildes As _Integer64)
               
            ' https://man7.org/linux/man-pages/man2/fork.2.html
            Function fork%& ()
               
            ' https://man7.org/linux/man-pages/man2/close.2.html
            Sub __close Alias "close" (ByVal fd As Long)
               
            ' https://man7.org/linux/man-pages/man2/dup2.2.html
            Sub dup2 (ByVal oldfd As Long, ByVal newfd As Long)
               
            ' https://man7.org/linux/man-pages/man3/execl.3.html
            Sub execl (path As String, arg1 As String, arg2 As String, cmd As String, ByVal nul As _Offset)
               
            ' https://man7.org/linux/man-pages/man2/select.2.html
            Function __select& Alias "select" (ByVal nfds As Long, ByVal readfds As _Offset, ByVal writefds As _Offset, ByVal exceptfds As _Offset, ByVal timeout As _Offset)
               
            ' https://man7.org/linux/man-pages/man2/read.2.html
            Function __read%& Alias "read" (ByVal fildes As Long, ByVal buf As _Offset, ByVal nbyte As _Offset)
               
            ' https://man7.org/linux/man-pages/man2/waitpid.2.html
            Sub waitpid (ByVal pid As _Offset, ByVal status As _Offset, ByVal options As Long)
        End Declare
           
        ' https://man7.org/linux/man-pages/man3/wait.3.html
        Declare Library "wait"
            Function WIFEXITED& (ByVal status As Long)
            Function WEXITSTATUS& (ByVal status As Long)
        End Declare

        ' Standard file descriptor numbers
        Const STDOUT_FILENO = 1
        Const STDERR_FILENO = 2

        ' --- fd_set macro replication constants ---
        $If 64BIT Then
            Const NFDBITS = 64
        $Else
                Const NFDBITS = 32
        $End If
        Const FD_SETSIZE = 1024
        Const FD_SET_ARRAY_MAX_INDEX = (FD_SETSIZE / NFDBITS) - 1
           
        ' QB64 doesn't have int[2] arrays as params, so we pack
        ' [read_fd, write_fd] into a single _INTEGER64
        Dim As _Integer64 stdout_pipes, stderr_pipes
        Dim As _Offset pid

        ' Create two pipes: one for stdout, one for stderr
        If pipe(stdout_pipes) = -1 Or pipe(stderr_pipes) = -1 Then
            _LogError "An error with pipe has occurred"
            pipecom = -1
            Exit Function
        End If

        ' Create the child process
        pid = fork
        If pid = -1 Then
            _LogError "An error with fork has occurred"
            pipecom = -1
            Exit Function
        End If

        '========================
        ' CHILD PROCESS
        '========================
        If pid = 0 Then
            ' We are in the child process.
            ' Close the READ ends of the pipes (child only writes)
            __close GetLowLong(stdout_pipes)
            __close GetLowLong(stderr_pipes)

            ' Redirect child's STDOUT to the WRITE end of the stdout pipe
            dup2 GetHighLong(stdout_pipes), STDOUT_FILENO
            ' Redirect child's STDERR to the WRITE end of the stderr pipe
            dup2 GetHighLong(stderr_pipes), STDERR_FILENO

            ' Close the original WRITE end descriptors (now redundant)
            __close GetHighLong(stdout_pipes)
            __close GetHighLong(stderr_pipes)

            ' Execute the command using /bin/sh -c "..."
            ' We add _CHR_NUL for C-string null termination
            execl "/bin/sh" + _CHR_NUL, "sh" + _CHR_NUL, "-c" + _CHR_NUL, cmd + _CHR_NUL, 0
               
            ' If execl returns, an error occurred. Exit with 127.
            System 127
           
            '========================
            ' PARENT PROCESS
            '========================
        Else
            ' We are in the parent process.
            ' Close the WRITE ends of the pipes (parent only reads)
            __close GetHighLong(stdout_pipes)
            __close GetHighLong(stderr_pipes)

            pipecom = -1 ' Default exit code

            ' Find the highest file descriptor number for select()
            Dim As Long max_fd
            If GetLowLong(stdout_pipes) > GetLowLong(stderr_pipes) Then
                max_fd = GetLowLong(stdout_pipes)
            Else
                max_fd = GetLowLong(stderr_pipes)
            End If

            ' This is our file descriptor set for select()
            Dim As _Integer64 read_fds(FD_SET_ARRAY_MAX_INDEX)
               
            ' Main read loop:
            ' We use select() to monitor both pipes at once.
            While 1
                Dim As String read_buf: read_buf = Space$(1024)
                Dim As _Offset bytes
                   
                ' Clear the fd_set
                FD_ZERO read_fds()
                   
                ' Flag to track if any pipes are still open
                Dim As Long fds_open: fds_open = 0

                ' Add STDOUT pipe to set if it's not closed
                ' (We flag closed pipes by setting their FD to -1)
                If GetLowLong(stdout_pipes) <> -1 Then
                    FD_SET GetLowLong(stdout_pipes), read_fds()
                    fds_open = 1
                End If
                   
                ' Add STDERR pipe to set if it's not closed
                If GetLowLong(stderr_pipes) <> -1 Then
                    FD_SET GetLowLong(stderr_pipes), read_fds()
                    fds_open = 1
                End If

                ' If no pipes are left open, exit the read loop
                If fds_open = 0 Then
                    Exit While
                End If

                ' Wait indefinitely until one or more pipes have data
                If __select(max_fd + 1, _Offset(read_fds()), 0, 0, 0) = -1 Then
                    _LogError "An error with __select has occurred"
                    Exit While
                End If

                ' Check if STDOUT pipe has data
                If GetLowLong(stdout_pipes) <> -1 And FD_ISSET(GetLowLong(stdout_pipes), read_fds()) = -1 Then
                    bytes = __read(GetLowLong(stdout_pipes), _Offset(read_buf), Len(read_buf))
                    If bytes > 0 Then
                        ' Append data to stdout string
                        stdout = stdout + Mid$(read_buf, 1, bytes)
                    Else
                        ' 0 bytes means EOF. Close the pipe.
                        __close GetLowLong(stdout_pipes)
                        ' Flag it as closed by setting the FD to -1
                        Dim As Long stdoutlow: stdoutlow = -1
                        stdout_pipes = PackLongsToInteger64(stdoutlow, GetHighLong(stdout_pipes))
                    End If
                End If

                ' Check if STDERR pipe has data
                If GetLowLong(stderr_pipes) <> -1 And FD_ISSET(GetLowLong(stderr_pipes), read_fds()) = -1 Then
                    bytes = __read(GetLowLong(stderr_pipes), _Offset(read_buf), Len(read_buf))
                    If bytes > 0 Then
                        ' Append data to stderr string
                        stderr = stderr + Mid$(read_buf, 1, bytes)
                    Else
                        ' 0 bytes means EOF. Close the pipe.
                        __close GetLowLong(stderr_pipes)
                        ' Flag it as closed by setting the FD to -1
                        Dim As Long stderrlow: stderrlow = -1
                        stderr_pipes = PackLongsToInteger64(stderrlow, GetHighLong(stderr_pipes))
                    End If
                End If
            Wend

            ' Wait for the child process to exit and get its status
            Dim As Long status
            waitpid pid, _Offset(status), 0

            ' Check if the process exited normally
            If WIFEXITED(status) Then
                ' Get the actual exit code
                pipecom = WEXITSTATUS(status)
            Else
                pipecom = -1 ' Process was killed or exited abnormally
            End If
        End If
    $End If
End Function

'============================================================================
' HELPER FUNCTIONS (POSIX-only)
'============================================================================

' This section is skipped on Windows
$If WINDOWS Then
$Else
    ' --- fd_set Macro Replications ---

    Sub FD_ZERO (arr() As _Integer64)
        ' Replicates: FD_ZERO(fd_set *set)
        ' Clears all bits in the set by zeroing the array.
        Dim As Integer i
        For i = 0 To UBound(arr)
            arr(i) = 0
        Next i
    End Sub

    Sub FD_SET (fd As Long, arr() As _Integer64)
        ' Replicates: FD_SET(int fd, fd_set *set)
        ' Sets the specific bit for a file descriptor.
        $If 64BIT Then
            Const NFDBITS = 64
        $Else
                Const NFDBITS = 32
        $End If
        Dim As Long index, bit_position
        Dim As _Integer64 bit_mask

        ' Find which array element holds the bit
        index = fd \ NFDBITS
        ' Find the bit's position within that element
        bit_position = fd Mod NFDBITS
        ' Create a mask for that bit (1 << bit_position)
        bit_mask = 2 ^ bit_position
        ' Set the bit
        arr(index) = arr(index) Or bit_mask
    End Sub

    Function FD_ISSET% (fd As Long, arr() As _Integer64)
        ' Replicates: int FD_ISSET(int fd, fd_set *set)
        ' Checks if a specific bit for a file descriptor is set.
        $If 64BIT Then
            Const NFDBITS = 64
        $Else
                Const NFDBITS = 32
        $End If
        Dim As Long index, bit_position
        Dim As _Integer64 bit_mask

        index = fd \ NFDBITS
        bit_position = fd Mod NFDBITS
        bit_mask = 2 ^ bit_position

        ' Check the bit. Returns -1 (True) or 0 (False).
        If (arr(index) And bit_mask) <> 0 Then
            FD_ISSET = -1
        Else
            FD_ISSET = 0
        End If
    End Function

    ' --- 32/64-bit Packing Helper Functions ---
    ' (Used to store two 32-bit FDs in one 64-bit _INTEGER64)

    Function PackLongsToInteger64&& (lowLong As Long, highLong As Long)
        ' Packs two 32-bit LONGs into one 64-bit _INTEGER64.
        Const LOMASK = &HFFFFFFFF

        Dim As _Integer64 high_shifted, low_masked
        ' Shift high long into the upper 32 bits
        high_shifted = _Cast(_Integer64, highLong) * (2 ^ 32)
        ' Mask low long to 32 bits (to handle sign)
        low_masked = _Cast(_Integer64, lowLong) And LOMASK
        ' Combine them
        PackLongsToInteger64 = high_shifted Or low_masked
    End Function

    Function GetLowLong& (packedValue As _Integer64)
        ' Extracts the low 32-bit LONG (index 0)
        Const LOMASK = &HFFFFFFFF
        GetLowLong = (packedValue And LOMASK)
    End Function

    Function GetHighLong& (packedValue As _Integer64)
        ' Extracts the high 32-bit LONG (index 1)
        GetHighLong = packedValue \ (2 ^ 32) ' Arithmetic shift right
    End Function

$End If

'============================================================================
' LITE HELPER FUNCTIONS
'============================================================================

Function pipecom_lite$ (cmd As String)
    ' A simple wrapper that returns stderr if it exists,
    ' otherwise returns stdout.
    Dim As Long a
    Dim As String stdout, stderr
    a = pipecom(cmd, stdout, stderr)
    If stderr <> "" Then
        pipecom_lite = stderr
    Else
        pipecom_lite = stdout
    End If
End Function

Sub pipecom_lite (cmd As String)
    ' A "fire-and-forget" version that runs the command
    ' but doesn't return any output.
    Dim As Long a
    Dim As String stdout, stderr
    a = pipecom(cmd, stdout, stderr)
End Sub
The noticing will continue
Reply
#2
Hi Spriggsy,

made some tests today, works fine in the QB64-PE Windows 64-bit version but crashes in the 32-bit version. I was able to follow it until the ReadFile call which reads the STDOUT pipe (line 149). If you place a SLEEP immediatly before that line you can defer the crash until pressing enter. My first thought was to rather define "buf" as fixed string rather than a variable string, but that doesn't work either. I'm not so deep in this API stuff as you are, my best guess is that some how the pipe handles you read from are invalid in the 32-bit version.
Reply
#3
Interesting. I'll have to take a look sometime. The Windows code should not be any different from how it was before. I've only changed the POSIX code. Hmm... I have had multiple code files that don't behave the same way on the latest versions of QB64-PE but worked on older versions. Who knows. Usually, it is the 64 bit that has trouble while the 32 bit works. If I can't figure it out, I ain't too concerned. I don't officially support 32 bit as nothing we use should be 32 bit.
The noticing will continue
Reply
#4
(11-13-2025, 05:59 PM)SpriggsySpriggs Wrote: Interesting. I'll have to take a look sometime. The Windows code should not be any different from how it was before. I've only changed the POSIX code. Hmm... I have had multiple code files that don't behave the same way on the latest versions of QB64-PE but worked on older versions. Who knows. Usually, it is the 64 bit that has trouble while the 32 bit works. If I can't figure it out, I ain't too concerned. I don't officially support 32 bit as nothing we use should be 32 bit.

Yes I also compared it to old version and it's indeed the same, but that old one also crashed on 32bit.

However, one thing I found already is your TYPE definition for PROCESS_INFORMATION is missing the final field DWORD dwThreadId, this didn't hurt on the 64bit side due to the applied padding, but on the 32bit side w/o the padding the TYPE is simply 4 bytes too short. I corrected that here, also the padding for 64bits is no longer needed then. I was hoping it solves the problem, but it still crashes.

Investigation continues, I'd like to include pipecom here.
Reply
#5
Ok, finally sorted it out, it now works on both 32bit and 64bit Windows. Need to clean out all my testing/debugging garbage befor posting it, but let me have dinner first Tongue Big Grin
Reply
#6
Well, here it is.

In the end, after the correction of the missing TYPE field, it was as easy as changing the DECLARE CUSTOMTYPE LIBRARY to DECLARE DYNAMIC LIBRARY "kernel32".

The hint was in the Wiki:
Quote:SUB procedures using DECLARE CUSTOMTYPE LIBRARY API procedures may error. Try DYNAMIC with the dynamically linked library or shared object file name.

I've no idea why that is how it is, but at least it works now on both 32/64bit Windows, additionally I did the following:
- renamed CreateProcess to CreateProcessA, as the compiler doesn't find the function without the A
- removed CreateProcessW as it isn't used anywhere
- calling CloseHandle directly instead through the HandleClose alias
- replaced all _CHR_NUL occurrences with CHR$(0) to keep pipecom compatible with pre-v4 QB64(PE) versions

pipecom.bas
Code: (Select All)
'================================================================================
' Pipecom - A Cross-Platform Process Capture Library for QB64
' Version: 2.0 (Native Linux/Mac Implementation)
' Author: Zachary Spriggs
'
' This library provides a single function, pipecom&, to execute a shell
' command and capture its STDOUT, STDERR, and Exit Code on both
' Windows and POSIX (Linux/Mac) systems.
'
' This version uses native POSIX calls on Linux/Mac, removing the
' previous dependency on popen and temporary files for stderr.
'================================================================================

$INCLUDEONCE

FUNCTION pipecom& (cmd AS STRING, stdout AS STRING, stderr AS STRING)
' Initialize output strings
stdout = "": stderr = ""

'========================================================================
' WINDOWS IMPLEMENTATION
'========================================================================
$IF WINDOWS THEN
' --- Win32 API Type Definitions ---

' https://learn.microsoft.com/en-us/window...attributes
TYPE SECURITY_ATTRIBUTES
AS _UNSIGNED LONG nLength
$IF 64BIT THEN
AS STRING * 4 padding ' Align for 64-bit
$END IF
AS _OFFSET lpSecurityDescriptor
AS LONG bInheritHandle
$IF 64BIT THEN
AS STRING * 4 padding2 ' Align for 64-bit
$END IF
END TYPE

' https://learn.microsoft.com/en-us/window...artupinfoa
TYPE STARTUPINFO
AS LONG cb
$IF 64BIT THEN
AS STRING * 4 padding
$END IF
AS _OFFSET lpReserved, lpDesktop, lpTitle
AS _UNSIGNED LONG dwX, dwY, dwXSize, dwYSize, dwXCountChars, dwYCountChars, dwFillAttribute, dwFlags
AS _UNSIGNED INTEGER wShowWindow, cbReserved2
$IF 64BIT THEN
AS STRING * 4 padding2
$END IF
AS _OFFSET lpReserved2, hStdInput, hStdOutput, hStdError
END TYPE

' https://learn.microsoft.com/en-us/window...nformation
TYPE PROCESS_INFORMATION
AS _OFFSET hProcess, hThread
AS _UNSIGNED LONG dwProcessId, dwThreadId
END TYPE

' --- Win32 API Constants ---
CONST STARTF_USESTDHANDLES = &H00000100 ' Use hStdInput, hStdOutput, hStdError
CONST CREATE_NO_WINDOW = &H8000000 ' Don't create a console window
CONST INFINITE = 4294967295 ' Wait forever
CONST WAIT_FAILED = &HFFFFFFFF ' Return value for Wait error

' --- Win32 API Function Declarations ---
DECLARE DYNAMIC LIBRARY "kernel32"
' https://learn.microsoft.com/en-us/window...createpipe
FUNCTION CreatePipe& (BYVAL hReadPipe AS _OFFSET, BYVAL hWritePipe AS _OFFSET, BYVAL lpPipeAttributes AS _OFFSET, BYVAL nSize AS _UNSIGNED LONG)

' https://learn.microsoft.com/en-us/window...teprocessa
FUNCTION CreateProcessA& (BYVAL lpApplicationName AS _OFFSET, BYVAL lpCommandLine AS _OFFSET, BYVAL lpProcessAttributes AS _OFFSET, BYVAL lpThreadAttributes AS _OFFSET, BYVAL bInheritHandles AS LONG, BYVAL dwCreationFlags AS _UNSIGNED LONG, BYVAL lpEnvironment AS _OFFSET, BYVAL lpCurrentDirectory AS _OFFSET, BYVAL lpStartupInfo AS _OFFSET, BYVAL lpProcessInformation AS _OFFSET)

' https://learn.microsoft.com/en-us/window...odeprocess
FUNCTION GetExitCodeProcess& (BYVAL hProcess AS _OFFSET, BYVAL lpExitCode AS _OFFSET)

' https://learn.microsoft.com/en-us/window...losehandle
SUB CloseHandle (BYVAL hObject AS _OFFSET)

' https://learn.microsoft.com/en-us/window...i-readfile
FUNCTION ReadFile& (BYVAL hFile AS _OFFSET, BYVAL lpBuffer AS _OFFSET, BYVAL nNumberOfBytesToRead AS _UNSIGNED LONG, BYVAL lpNumberOfBytesRead AS _OFFSET, BYVAL lpOverlapped AS _OFFSET)

' https://learn.microsoft.com/en-us/window...ngleobject
FUNCTION WaitForSingleObject~& (BYVAL hHandle AS _OFFSET, BYVAL dwMilliseconds AS _UNSIGNED LONG)
END DECLARE

DIM AS LONG ok: ok = 1
DIM AS _OFFSET hStdOutPipeRead, hStdOutPipeWrite, hStdReadPipeError, hStdOutPipeError

' Set up Security Attributes for inheritable pipe handles
DIM AS SECURITY_ATTRIBUTES sa
sa.nLength = LEN(sa): sa.lpSecurityDescriptor = 0: sa.bInheritHandle = 1

' Create the pipe for STDOUT
IF CreatePipe(_OFFSET(hStdOutPipeRead), _OFFSET(hStdOutPipeWrite), _OFFSET(sa), 0) = 0 THEN
pipecom = -1
EXIT FUNCTION
END IF

' Create the pipe for STDERR
IF CreatePipe(_OFFSET(hStdReadPipeError), _OFFSET(hStdOutPipeError), _OFFSET(sa), 0) = 0 THEN
pipecom = -1
EXIT FUNCTION
END IF

' Set up STARTUPINFO to redirect the new process's std handles
DIM AS STARTUPINFO si
si.cb = LEN(si)
si.dwFlags = STARTF_USESTDHANDLES
si.hStdError = hStdOutPipeError ' Redirect stderr to our pipe
si.hStdOutput = hStdOutPipeWrite ' Redirect stdout to our pipe
si.hStdInput = 0

DIM AS PROCESS_INFORMATION procinfo
DIM AS _OFFSET lpApplicationName
DIM AS STRING lpCommandLine

' Prepend "cmd /c " to execute the command in a shell
' and add a null terminator for the C API.
lpCommandLine = "cmd /c " + cmd + CHR$(0)

DIM AS _OFFSET lpProcessAttributes, lpThreadAttributes
DIM AS LONG bInheritHandles: bInheritHandles = 1 ' Must be 1 to inherit pipes
DIM AS _UNSIGNED LONG dwCreationFlags: dwCreationFlags = CREATE_NO_WINDOW
DIM AS _OFFSET lpEnvironment, lpCurrentDirectory

' Create the child process
ok = CreateProcessA(lpApplicationName, _OFFSET(lpCommandLine), lpProcessAttributes, lpThreadAttributes, bInheritHandles, dwCreationFlags, lpEnvironment, lpCurrentDirectory, _OFFSET(si), _OFFSET(procinfo))

IF ok = 0 THEN
pipecom = -1
EXIT FUNCTION
END IF

' Close the "write" ends of the pipes in the parent process.
' The child process now holds the only copies.
' This is crucial, or ReadFile will never finish.
CloseHandle hStdOutPipeWrite
CloseHandle hStdOutPipeError

' Read loop for STDOUT
DIM AS STRING buf: buf = SPACE$(4096 + 1)
DIM AS _UNSIGNED LONG dwRead
WHILE ReadFile(hStdOutPipeRead, _OFFSET(buf), 4096, _OFFSET(dwRead), 0) <> 0 AND dwRead > 0
buf = MID$(buf, 1, dwRead)
GOSUB RemoveChr13 ' Remove carriage returns
stdout = stdout + buf
buf = SPACE$(4096 + 1)
WEND

' Read loop for STDERR
WHILE ReadFile(hStdReadPipeError, _OFFSET(buf), 4096, _OFFSET(dwRead), 0) <> 0 AND dwRead > 0
buf = MID$(buf, 1, dwRead)
GOSUB RemoveChr13 ' Remove carriage returns
stderr = stderr + buf
buf = SPACE$(4096 + 1)
WEND

' Wait for the child process to terminate
DIM AS LONG exit_code, ex_stat
IF WaitForSingleObject(procinfo.hProcess, INFINITE) <> WAIT_FAILED THEN
' Get the process's exit code
IF GetExitCodeProcess(procinfo.hProcess, _OFFSET(exit_code)) THEN
ex_stat = 1
END IF
END IF

' Clean up remaining handles
CloseHandle hStdOutPipeRead
CloseHandle hStdReadPipeError

' Return the exit code
IF ex_stat = 1 THEN
pipecom = exit_code
ELSE
pipecom = -1
END IF

EXIT FUNCTION

' Helper routine to strip Chr$(13) for Windows
RemoveChr13:
DIM AS LONG j
j = INSTR(buf, CHR$(13))
DO WHILE j
buf = LEFT$(buf, j - 1) + MID$(buf, j + 1)
j = INSTR(buf, CHR$(13))
LOOP
RETURN

'========================================================================
' POSIX (LINUX / MAC) IMPLEMENTATION
'========================================================================
$ELSE
' --- POSIX API Function Declarations ---
DECLARE CUSTOMTYPE LIBRARY
' https://man7.org/linux/man-pages/man2/pipe.2.html
FUNCTION pipe& (fildes AS _INTEGER64)

' https://man7.org/linux/man-pages/man2/fork.2.html
FUNCTION fork%& ()

' https://man7.org/linux/man-pages/man2/close.2.html
SUB __close ALIAS "close" (BYVAL fd AS LONG)

' https://man7.org/linux/man-pages/man2/dup2.2.html
SUB dup2 (BYVAL oldfd AS LONG, BYVAL newfd AS LONG)

' https://man7.org/linux/man-pages/man3/execl.3.html
SUB execl (path AS STRING, arg1 AS STRING, arg2 AS STRING, cmd AS STRING, BYVAL nul AS _OFFSET)

' https://man7.org/linux/man-pages/man2/select.2.html
FUNCTION __select& ALIAS "select" (BYVAL nfds AS LONG, BYVAL readfds AS _OFFSET, BYVAL writefds AS _OFFSET, BYVAL exceptfds AS _OFFSET, BYVAL timeout AS _OFFSET)

' https://man7.org/linux/man-pages/man2/read.2.html
FUNCTION __read%& ALIAS "read" (BYVAL fildes AS LONG, BYVAL buf AS _OFFSET, BYVAL nbyte AS _OFFSET)

' https://man7.org/linux/man-pages/man2/waitpid.2.html
SUB waitpid (BYVAL pid AS _OFFSET, BYVAL status AS _OFFSET, BYVAL options AS LONG)
END DECLARE

' https://man7.org/linux/man-pages/man3/wait.3.html
DECLARE LIBRARY "wait"
FUNCTION WIFEXITED& (BYVAL status AS LONG)
FUNCTION WEXITSTATUS& (BYVAL status AS LONG)
END DECLARE

' Standard file descriptor numbers
CONST STDOUT_FILENO = 1
CONST STDERR_FILENO = 2

' --- fd_set macro replication constants ---
$IF 64BIT THEN
CONST NFDBITS = 64
$ELSE
CONST NFDBITS = 32
$END IF
CONST FD_SETSIZE = 1024
CONST FD_SET_ARRAY_MAX_INDEX = (FD_SETSIZE / NFDBITS) - 1

' QB64 doesn't have int[2] arrays as params, so we pack
' [read_fd, write_fd] into a single _INTEGER64
DIM AS _INTEGER64 stdout_pipes, stderr_pipes
DIM AS _OFFSET pid

' Create two pipes: one for stdout, one for stderr
IF pipe(stdout_pipes) = -1 OR pipe(stderr_pipes) = -1 THEN
_LOGERROR "An error with pipe has occurred"
pipecom = -1
EXIT FUNCTION
END IF

' Create the child process
pid = fork
IF pid = -1 THEN
_LOGERROR "An error with fork has occurred"
pipecom = -1
EXIT FUNCTION
END IF

'========================
' CHILD PROCESS
'========================
IF pid = 0 THEN
' We are in the child process.
' Close the READ ends of the pipes (child only writes)
__close GetLowLong(stdout_pipes)
__close GetLowLong(stderr_pipes)

' Redirect child's STDOUT to the WRITE end of the stdout pipe
dup2 GetHighLong(stdout_pipes), STDOUT_FILENO
' Redirect child's STDERR to the WRITE end of the stderr pipe
dup2 GetHighLong(stderr_pipes), STDERR_FILENO

' Close the original WRITE end descriptors (now redundant)
__close GetHighLong(stdout_pipes)
__close GetHighLong(stderr_pipes)

' Execute the command using /bin/sh -c "..."
' We add CHR$(0) for C-string null termination
execl "/bin/sh" + CHR$(0), "sh" + CHR$(0), "-c" + CHR$(0), cmd + CHR$(0), 0

' If execl returns, an error occurred. Exit with 127.
SYSTEM 127

'========================
' PARENT PROCESS
'========================
ELSE
' We are in the parent process.
' Close the WRITE ends of the pipes (parent only reads)
__close GetHighLong(stdout_pipes)
__close GetHighLong(stderr_pipes)

pipecom = -1 ' Default exit code

' Find the highest file descriptor number for select()
DIM AS LONG max_fd
IF GetLowLong(stdout_pipes) > GetLowLong(stderr_pipes) THEN
max_fd = GetLowLong(stdout_pipes)
ELSE
max_fd = GetLowLong(stderr_pipes)
END IF

' This is our file descriptor set for select()
DIM AS _INTEGER64 read_fds(FD_SET_ARRAY_MAX_INDEX)

' Main read loop:
' We use select() to monitor both pipes at once.
WHILE 1
DIM AS STRING read_buf: read_buf = SPACE$(1024)
DIM AS _OFFSET bytes

' Clear the fd_set
FD_ZERO read_fds()

' Flag to track if any pipes are still open
DIM AS LONG fds_open: fds_open = 0

' Add STDOUT pipe to set if it's not closed
' (We flag closed pipes by setting their FD to -1)
IF GetLowLong(stdout_pipes) <> -1 THEN
FD_SET GetLowLong(stdout_pipes), read_fds()
fds_open = 1
END IF

' Add STDERR pipe to set if it's not closed
IF GetLowLong(stderr_pipes) <> -1 THEN
FD_SET GetLowLong(stderr_pipes), read_fds()
fds_open = 1
END IF

' If no pipes are left open, exit the read loop
IF fds_open = 0 THEN
EXIT WHILE
END IF

' Wait indefinitely until one or more pipes have data
IF __select(max_fd + 1, _OFFSET(read_fds()), 0, 0, 0) = -1 THEN
_LOGERROR "An error with __select has occurred"
EXIT WHILE
END IF

' Check if STDOUT pipe has data
IF GetLowLong(stdout_pipes) <> -1 AND FD_ISSET(GetLowLong(stdout_pipes), read_fds()) = -1 THEN
bytes = __read(GetLowLong(stdout_pipes), _OFFSET(read_buf), LEN(read_buf))
IF bytes > 0 THEN
' Append data to stdout string
stdout = stdout + MID$(read_buf, 1, bytes)
ELSE
' 0 bytes means EOF. Close the pipe.
__close GetLowLong(stdout_pipes)
' Flag it as closed by setting the FD to -1
DIM AS LONG stdoutlow: stdoutlow = -1
stdout_pipes = PackLongsToInteger64(stdoutlow, GetHighLong(stdout_pipes))
END IF
END IF

' Check if STDERR pipe has data
IF GetLowLong(stderr_pipes) <> -1 AND FD_ISSET(GetLowLong(stderr_pipes), read_fds()) = -1 THEN
bytes = __read(GetLowLong(stderr_pipes), _OFFSET(read_buf), LEN(read_buf))
IF bytes > 0 THEN
' Append data to stderr string
stderr = stderr + MID$(read_buf, 1, bytes)
ELSE
' 0 bytes means EOF. Close the pipe.
__close GetLowLong(stderr_pipes)
' Flag it as closed by setting the FD to -1
DIM AS LONG stderrlow: stderrlow = -1
stderr_pipes = PackLongsToInteger64(stderrlow, GetHighLong(stderr_pipes))
END IF
END IF
WEND

' Wait for the child process to exit and get its status
DIM AS LONG status
waitpid pid, _OFFSET(status), 0

' Check if the process exited normally
IF WIFEXITED(status) THEN
' Get the actual exit code
pipecom = WEXITSTATUS(status)
ELSE
pipecom = -1 ' Process was killed or exited abnormally
END IF
END IF
$END IF
END FUNCTION

'============================================================================
' HELPER FUNCTIONS (POSIX-only)
'============================================================================

' This section is skipped on Windows
$IF WINDOWS THEN
$ELSE
' --- fd_set Macro Replications ---

SUB FD_ZERO (arr() AS _INTEGER64)
' Replicates: FD_ZERO(fd_set *set)
' Clears all bits in the set by zeroing the array.
DIM AS INTEGER i
FOR i = 0 TO UBOUND(arr)
arr(i) = 0
NEXT i
END SUB

SUB FD_SET (fd AS LONG, arr() AS _INTEGER64)
' Replicates: FD_SET(int fd, fd_set *set)
' Sets the specific bit for a file descriptor.
$IF 64BIT THEN
CONST NFDBITS = 64
$ELSE
CONST NFDBITS = 32
$END IF
DIM AS LONG index, bit_position
DIM AS _INTEGER64 bit_mask

' Find which array element holds the bit
index = fd \ NFDBITS
' Find the bit's position within that element
bit_position = fd MOD NFDBITS
' Create a mask for that bit (1 << bit_position)
bit_mask = 2 ^ bit_position
' Set the bit
arr(index) = arr(index) OR bit_mask
END SUB

FUNCTION FD_ISSET% (fd AS LONG, arr() AS _INTEGER64)
' Replicates: int FD_ISSET(int fd, fd_set *set)
' Checks if a specific bit for a file descriptor is set.
$IF 64BIT THEN
CONST NFDBITS = 64
$ELSE
CONST NFDBITS = 32
$END IF
DIM AS LONG index, bit_position
DIM AS _INTEGER64 bit_mask

index = fd \ NFDBITS
bit_position = fd MOD NFDBITS
bit_mask = 2 ^ bit_position

' Check the bit. Returns -1 (True) or 0 (False).
IF (arr(index) AND bit_mask) <> 0 THEN
FD_ISSET = -1
ELSE
FD_ISSET = 0
END IF
END FUNCTION

' --- 32/64-bit Packing Helper Functions ---
' (Used to store two 32-bit FDs in one 64-bit _INTEGER64)

FUNCTION PackLongsToInteger64&& (lowLong AS LONG, highLong AS LONG)
' Packs two 32-bit LONGs into one 64-bit _INTEGER64.
CONST LOMASK = &HFFFFFFFF

DIM AS _INTEGER64 high_shifted, low_masked
' Shift high long into the upper 32 bits
high_shifted = _CAST(_INTEGER64, highLong) * (2 ^ 32)
' Mask low long to 32 bits (to handle sign)
low_masked = _CAST(_INTEGER64, lowLong) AND LOMASK
' Combine them
PackLongsToInteger64 = high_shifted OR low_masked
END FUNCTION

FUNCTION GetLowLong& (packedValue AS _INTEGER64)
' Extracts the low 32-bit LONG (index 0)
CONST LOMASK = &HFFFFFFFF
GetLowLong = (packedValue AND LOMASK)
END FUNCTION

FUNCTION GetHighLong& (packedValue AS _INTEGER64)
' Extracts the high 32-bit LONG (index 1)
GetHighLong = packedValue \ (2 ^ 32) ' Arithmetic shift right
END FUNCTION

$END IF

'============================================================================
' LITE HELPER FUNCTIONS
'============================================================================

FUNCTION pipecom_lite$ (cmd AS STRING)
' A simple wrapper that returns stderr if it exists,
' otherwise returns stdout.
DIM AS LONG a
DIM AS STRING stdout, stderr
a = pipecom(cmd, stdout, stderr)
IF stderr <> "" THEN
pipecom_lite = stderr
ELSE
pipecom_lite = stdout
END IF
END FUNCTION

SUB pipecom_lite (cmd AS STRING)
' A "fire-and-forget" version that runs the command
' but doesn't return any output.
DIM AS LONG a
DIM AS STRING stdout, stderr
a = pipecom(cmd, stdout, stderr)
END SUB
Reply
#7
The way I understand your pipecom library (*please correct any error I may make here*)  Is that it executes a shell command then stores either STDOUT or STDERR in string variables  and Then returns after the command is finished !

A function I would like to see ? ?  Is one that acts a bit more like _FILES$.      I am thinking of shell process's that might take some significant time to execute and I want my ***Main program*** to monitor that process's output As it executes !.   (I am specifically in my projects using ffmpeg & ImageMagick to do conversions,  some of those commands can take several seconds and in the case of using ffmpeg to extract Video frames may actually take several minutes to complete !)

I'm currently using a kludge for monitoring ffmpeg frame extractions.    I use a SHELL _DONTWAIT than start counting files in the frames output directory.    If that count stays Static for 3 seconds then I assume ffmpeg is done !.      So far that is working.    But I'm concerned that it may not work properly if someone ever runs my app on a slow machine with a Video with VERY LARGE frames !

That works for frame extractions but I also have a couple places in my code where ffmpeg just converts a video and outputs another video !    Counting Files doesn't work in this context and at present I just print an appropriate status message with Please Wait and use a regular Shell command.    It would be a nice touch for my software if I could do something like Linux does with Software updates and enable a terminal sub-window in my Main screen showing ffmpeg's output as it proceeds !

   I don't expect miracles but I was just thinking that such a function would be useful to have if that was an option that pipecom could encompass !
Reply
#8
I do not intend to add this behavior to pipecom. Also, I do not think _FILES$ uses a DONTWAIT. I think it is using direntry or something to that effect. If I decide to do anything with this, it won't be with pipecom but with a separate library. Also, it looks like ffmpeg has a "-progress" flag. Look into it. It's supposed to let you track the progress using either a file or local host.
The noticing will continue
Reply
#9
(12-10-2025, 06:06 PM)SpriggsySpriggs Wrote: I do not intend to add this behavior to pipecom. Also, I do not think _FILES$ uses a DONTWAIT. I think it is using direntry or something to that effect. If I decide to do anything with this, it won't be with pipecom but with a separate library. Also, it looks like ffmpeg has a "-progress" flag. Look into it. It's supposed to let you track the progress using either a file or local host.
   Thanks for the input.   I'll look into that ffmpeg -progress flag   ( ffmpeg has an overwhelming number of options !)

I was more talking about how _FILES$ behaves when called vs implementation.     Each successive call gives you a the next entry.   I was thinking some sort of  " pipeShell " command that does something like a _DONTWAIT and returns each line of output from the Child process when called successively !     If I was clearer on the implementation details I might give it a stab myself,   at the moment I don't have that level of knowledge !
Reply
#10
(12-10-2025, 06:06 PM)SpriggsySpriggs Wrote: I do not intend to add this behavior to pipecom. Also, I do not think _FILES$ uses a DONTWAIT. I think it is using direntry or something to that effect. If I decide to do anything with this, it won't be with pipecom but with a separate library. Also, it looks like ffmpeg has a "-progress" flag. Look into it. It's supposed to let you track the progress using either a file or local host.
    Thank you very much for the tip on the ffmpeg -progress flag.   It was exactly the ticket for what I was doing !
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  PIPECOM of SpriggsySpriggs krovit 7 1,006 08-26-2025, 12:10 PM
Last Post: SpriggsySpriggs

Forum Jump:


Users browsing this thread: 1 Guest(s)