Search code examples
powershellopenedgeprogress-4gl

Wrap Powershell in Progress class


I'm trying to implement a class wrapper around Powershell. It almost works, except that it hangs when it cannot read any more output from the STREAM. So it manages to read all output, but after theres no more, it just hangs in IMPORT statement:

BLOCK-LEVEL ON ERROR UNDO, THROW.

    CLASS Powershell:

    DEF PRIVATE STREAM stPowershell.
    
    CONSTRUCTOR PUBLIC Powershell():
        INPUT-OUTPUT STREAM stPowershell THROUGH VALUE("powershell").
        THIS-OBJECT:ReadOutput().
    END.
    
    DESTRUCTOR Powershell():
        INPUT-OUTPUT STREAM stPowershell CLOSE.
    END.
    
    METHOD PUBLIC CHAR Input(i_cInput AS CHAR):
        IF i_cInput = ? THEN UNDO, THROW NEW Progress.Lang.AppError(SUBST("&1: 'i_cInput' is 'UNKNOWN'!", PROGRAM-NAME(1))).
        
        PUT STREAM stPowershell UNFORMATTED i_cInput SKIP.
        
        RETURN THIS-OBJECT:ReadOutput().
    END.
    
    METHOD PROTECTED CHAR ReadOutput():
    
        DEF VAR cOutputs AS CHAR NO-UNDO.
        DEF VAR cOutput  AS CHAR NO-UNDO.
        DEF VAR lFirst   AS LOGICAL NO-UNDO INIT TRUE.
        
        REPEAT:
            IF lFirst THEN lFirst = FALSE.
                      ELSE cOutputs = cOutputs + "~n".
            
            IMPORT STREAM stPowershell UNFORMATTED cOutput NO-ERROR.
            cOutputs = cOutputs + cOutput.
        END.
        
        RETURN cOutputs.
    END.
    
END.

Usage:

DEF VAR oPowershell AS CLASS Powershell NO-UNDO.
DEF VAR cOutput     AS CHAR NO-UNDO.

oPowershell = NEW Powershell().
cOutput = oPowershell:Input("$num = 12").
cOutput = oPowershell:Input("(New-Object -ComObject Wscript.Shell).Popup($num, 0, 'Done', 0x0)").
cOutput = oPowershell:Input("Write-Output 'test output'").

Solution

  • I managed to get this version to work, but I wouldn't call it a perfect solution.

    To avoid the output read hanging issue, I made the Powershell output EOF character after each command. This way all synchronous execution will always end in EOF character in the Powershell output. We will read the output until we reach the EOF and stop there.

    Possible problems can arise if we're running asynchronous commands, or the output contains EOF characters.

    I think the best solution would require us to use platform specific APIs (for example WIN32 API). We need a way to read all existing output. But I think this version will work for a lot of normal cases.

    BLOCK-LEVEL ON ERROR UNDO, THROW.
    
    CLASS Powershell:
        
        &SCOPED-DEFINE POWERSHELL_EOF "[char]26"
        &SCOPED-DEFINE PROGRESS_EOF   26
        
        DEF PRIVATE STREAM stPowershell.
        
        CONSTRUCTOR PUBLIC Powershell():
            
            /* Start new 'Powershell' */
            INPUT-OUTPUT STREAM stPowershell THROUGH VALUE("powershell") NO-ECHO.
            
            /* Disable prompt */
            PUT STREAM stPowershell UNFORMATTED "$global:Prompt = $null" SKIP.
            
            /* Flush startup output */
            THIS-OBJECT:Read().
        END.
        
        DESTRUCTOR Powershell():
            INPUT-OUTPUT STREAM stPowershell CLOSE.
        END.
        
        METHOD PUBLIC CHAR Write(i_cInput AS CHAR):
            IF i_cInput = ? THEN UNDO, THROW NEW Progress.Lang.AppError(SUBST("&1: 'i_cInput' is 'UNKNOWN'!", PROGRAM-NAME(1))).
            
            /* Write input */
            PUT STREAM stPowershell UNFORMATTED i_cInput SKIP.
            
            /* Read output */
            RETURN THIS-OBJECT:Read().
        END.
        
        METHOD PROTECTED CHAR Read():
            
            DEF VAR cOutputs AS CHAR NO-UNDO.
            
            /* Output 'EOF' manually */
            PUT STREAM stPowershell UNFORMATTED {&POWERSHELL_EOF} SKIP.
            
            /* Read all output until 'EOF' */
            REPEAT:
                READKEY STREAM stPowershell PAUSE 0.
                
                /* If we reached the 'EOF', stop reading */
                IF LASTKEY = {&PROGRESS_EOF} THEN DO:
                    LEAVE.
                END.
                
                cOutputs = cOutputs + CHR(LASTKEY).
            END.
            
            RETURN cOutputs.
        END.
        
    END.