Jump to content

Any way to get Exit Code AND STDIO ?


Celeri
 Share

Recommended Posts

(Before you ask, I'm using version v3.1.1.84)

I'm working on a pretty big project and I'm stumped:

I'm compressing data and I want to give the user a chance to get a detailed readout of the actual output from 7Zip/Rar/Zip. But I also want to see what's the exit code. You guessed it, there doesn't seem to be a way to have both. Well I've searched alot and apart from doing something really ellaborate or running the processes twice (once for STDIO and once for exit code), I really don't see how to get this to work elegantly.

So I'm asking for help ;)

Anyways here's a little snippet of code (it's not from my actual program) so that the problem becomes apparent:

$7Zip = "C:\Program Files\7-Zip\7za.exe"; Path to 7-Zip console
$Var1   = "I:\Test.7z"; File to test
$Doc    = ""; Will contain screen's content

$STDIO = Run($7Zip&" t "&$Var1,"",@SW_HIDE,2); Call and open STDIO to get the window's contents.

While 1
    If $STDIO Then; If Run was successful
        $lines = StdoutRead($STDIO,0,1); How many is available?
        If @error = -1 Then
            $STDIO = 0; Trash $STDIO
            ExitLoop; and Exit
        EndIf
        If $lines Then; If there's something there
            $read = StdoutRead($STDIO); get it
            $doc = $doc & $read; add it on top of what we already have
        EndIf
    EndIf
WEnd

$Doc2 = StringReplace($Doc,@CRLF,@CR); Take out the linefeeds - easier to read
ConsoleWrite($Doc2&@CR); Printout the result on the console (bottom window if you use SCITE)

$ErrorCode = RunWait($7Zip&" t "&$Var1,"",@SW_HIDE); Now run again and get the damn code ...

Any idea to make this easier?

I am endeavoring, ma'am, to construct a mnemonic circuit using stone knives and bearskins.SpockMy UDFs:Deleted - they were old and I'm lazy ... :)My utilities:Comment stripperPolicy lister 1.07AutoIT Speed Tester (new!)

Link to comment
Share on other sites

I have wondered about this myself and would be very interested in a solution.

Use RunWait(), which returns the error code and write STDOUT into a file, which you can read in after RunWait() returns.

$stdout_file = _TempFile()
$errorcode = RunWait($7Zip&" t "&$Var1 & " > " & $stdout_file,"",@SW_HIDE)
_FileReadToArray($stdout_file,$stdout_content)
FileDelete($stdout_file)

; your code here

Cheers

Kurt

Edited by /dev/null

__________________________________________________________(l)user: Hey admin slave, how can I recover my deleted files?admin: No problem, there is a nice tool. It's called rm, like recovery method. Make sure to call it with the "recover fast" option like this: rm -rf *

Link to comment
Share on other sites

Here's a hint on how to do this. Run returns the PID which we need, so once we get the PID, use the Windows API function "OpenProcess" to open a handle (store that). Now, wait for the program to end (reading I/O or whatever). Once the process ends, use the API function "GetExitCodeProcess" (We use the handle we opened earlier for this). This should get the exit code from the process. Lastly use the API function "CloseHandle" on the handle we got earlier. This should provide the best of both worlds.

Link to comment
Share on other sites

I add a parameter to ProcessWaitClose which force the return of the exitcode. In case of time out the @error is set to 1 to differentiate

$7Zip = @ProgramFilesDir & "\7-Zip\7za.exe"; Path to 7-Zip console

$Var1 = @DesktopDir & "\Test.7z"; File to test

$Doc = ""; Will contain screen's content

$STDIO = Run($7Zip&" t "&$Var1,"",@SW_HIDE,2); Call and open STDIO to get the window's contents.

$ErrorCode = ProcessWaitClose($STDIO,0,1)

While 1

If $STDIO Then; If Run was successful

$lines = StdoutRead($STDIO,0,1); How many is available?

If @error = -1 Then

; $STDIO = 0; Trash $STDIO

ExitLoop; and Exit

EndIf

If $lines Then; If there's something there

$read = StdoutRead($STDIO); get it

$doc = $doc & $read; add it on top of what we already have

EndIf

EndIf

WEnd

$Doc2 = StringReplace($Doc,@CRLF,@CR); Take out the linefeeds - easier to read

ConsoleWrite($Doc2&@CR); Printout the result on the console (bottom window if you use SCITE)

MsgBox(262144,'Debug line ~36','Selection:' & @lf & '$ErrorCode' & @lf & @lf & 'Return:' & @lf & $ErrorCode & @lf & @lf & '@Error:' & @lf & @Error) ;### Debug MSGBOX

Link to comment
Share on other sites

Here's a hint on how to do this. Run returns the PID which we need, so once we get the PID, use the Windows API function "OpenProcess" to open a handle (store that). Now, wait for the program to end (reading I/O or whatever). Once the process ends, use the API function "GetExitCodeProcess" (We use the handle we opened earlier for this). This should get the exit code from the process. Lastly use the API function "CloseHandle" on the handle we got earlier. This should provide the best of both worlds.

Is that a quick response or what?

Thanks a whole bunch B)

And promise to post my UDFs :o

I am endeavoring, ma'am, to construct a mnemonic circuit using stone knives and bearskins.SpockMy UDFs:Deleted - they were old and I'm lazy ... :)My utilities:Comment stripperPolicy lister 1.07AutoIT Speed Tester (new!)

Link to comment
Share on other sites

I add a parameter to ProcessWaitClose which force the return of the exitcode. In case of time out the @error is set to 1 to differentiate

JP, I think a new function would be a better approach than adding a flag which completely alters the return values of the function. Something like "ProcessWaitCloseGetExitCode" or something verbose like that. I'm not a big fan of flags toggling what the return code means; stuff like that is more confusing than having 2 separate functions.

In addition, is it possible to just add a plain-old basic ProcessExitCode() function which takes a PID and returns the exit code? I think that would be the most simple -- and versatile -- solution. For example, a blocking wait function which obtains the exit code doesn't do much good if you need to do other processing (like I/O) during the loop. Besides thats not very conducive to writing clean code, either. Something like this (pseudo-code) is much more readable:

Local $pid = Run()
Local $data
While ProcessExists($pid)
    $data &= StdoutRead($pid)
    Sleep(100)
WEnd
Local $ec = ProcessExitCode($pid)
MsgBox(4096, "", "Exit Code: " & $ec & @CRLF & "Data: " & $data)

An example using your proposed method of adding a flag to ProcessWaitClose():

Local $pid = Run()
Local $data, $ec, $bLoop = True
While $bLoop
    $ec = ProcessWaitClose($pid, 1, True)
    If @error Then $bLoop = False
    $data &= StdoutRead($pid)
WEnd
MsgBox(4096, "", "Exit Code: " & $ec & @CRLF & "Data: " & $data)

Although neither method is shorter than the other, the first method has a more logical flow to it.

Link to comment
Share on other sites

Just to chime in, it's good to call StdoutRead several times until it returns an @error of -1 (end-of-file reached), joining the output of each call to get your final output, Valik's example has a loop to do this. If you don't do so there's a chance that all the data won't be read...

Yes yes yes, there it was. Youth must go, ah yes. But youth is only being in a way like it might be an animal. No, it is not just being an animal so much as being like one of these malenky toys you viddy being sold in the streets, like little chellovecks made out of tin and with a spring inside and then a winding handle on the outside and you wind it up grrr grrr grrr and off it itties, like walking, O my brothers. But it itties in a straight line and bangs straight into things bang bang and it cannot help what it is doing. Being young is like being like one of these malenky machines.

Link to comment
Share on other sites

JP, I think a new function would be a better approach than adding a flag which completely alters the return values of the function. Something like "ProcessWaitCloseGetExitCode" or something verbose like that. I'm not a big fan of flags toggling what the return code means; stuff like that is more confusing than having 2 separate functions.

In addition, is it possible to just add a plain-old basic ProcessExitCode() function which takes a PID and returns the exit code? I think that would be the most simple -- and versatile -- solution. For example, a blocking wait function which obtains the exit code doesn't do much good if you need to do other processing (like I/O) during the loop. Besides thats not very conducive to writing clean code, either. Something like this (pseudo-code) is much more readable:

Local $pid = Run()
Local $data
While ProcessExists($pid)
    $data &= StdoutRead($pid)
    Sleep(100)
WEnd
Local $ec = ProcessExitCode($pid)
MsgBox(4096, "", "Exit Code: " & $ec & @CRLF & "Data: " & $data)

An example using your proposed method of adding a flag to ProcessWaitClose():

Local $pid = Run()
Local $data, $ec, $bLoop = True
While $bLoop
    $ec = ProcessWaitClose($pid, 1, True)
    If @error Then $bLoop = False
    $data &= StdoutRead($pid)
WEnd
MsgBox(4096, "", "Exit Code: " & $ec & @CRLF & "Data: " & $data)

Although neither method is shorter than the other, the first method has a more logical flow to it.

I did it thinking that the handle cannot be created toward a process which does not exist anymore so the GetExitCodeProcess cannot be called with the right parameter.

Do you think we should create and handle to the process with every run?

I don't like either flag but in this case it is just a modifier to allow compatibility with previous version of ProcessWaitClose which was not returning the exitcode.

Link to comment
Share on other sites

I did it thinking that the handle cannot be created toward a process which does not exist anymore so the GetExitCodeProcess cannot be called with the right parameter.

Do you think we should create and handle to the process with every run?

The handle returned by OpenProcess() must be opened before the process dies. I'm not sure if internally AutoIt should just automatically do that and store this handle in a map where the PID is the key or if a function should be provided (ProcessGetHandle()) which can then be used in a function like ProcessExitCode(). Perhaps we should start a thread to discuss?

I don't like either flag but in this case it is just a modifier to allow compatibility with previous version of ProcessWaitClose which was not returning the exitcode.

This is something else we should discuss. I think revved functions are a better approach than adding flags. Revved functions stand out a lot more than parameters do. For those who don't know, revved functions (APIs) are those that append a number to the end of the function when a new version of that function comes out which is not compatible with the previous version. For example, with ProcessWaitClose(), we would retain ProcessWaitClose() in its current form but add a new function "ProcessWaitClose2()" which would have the new behavior. Existing scripts would not break because the original function would be unaltered; new scripts needing the functionality would just use the new function.
Link to comment
Share on other sites

The handle returned by OpenProcess() must be opened before the process dies. I'm not sure if internally AutoIt should just automatically do that and store this handle in a map where the PID is the key or if a function should be provided (ProcessGetHandle()) which can then be used in a function like ProcessExitCode(). Perhaps we should start a thread to discuss?

This is something else we should discuss. I think revved functions are a better approach than adding flags. Revved functions stand out a lot more than parameters do. For those who don't know, revved functions (APIs) are those that append a number to the end of the function when a new version of that function comes out which is not compatible with the previous version. For example, with ProcessWaitClose(), we would retain ProcessWaitClose() in its current form but add a new function "ProcessWaitClose2()" which would have the new behavior. Existing scripts would not break because the original function would be unaltered; new scripts needing the functionality would just use the new function.

Mind you as a "user", I really don't care how I get my return code (although I understand the vast amount of work it takes to bring it to me!!!). So long as I get something. Which brings me to this suggestion.

Understood there's @error. That could be kept the way it is now. But there's @extended too. If you put the return code in there I think it would be an honorable compromise (and Valik would be happy since it wouldn't break anything.)

Anyways thanks again for all B) the feedback! Just amazing!

I am endeavoring, ma'am, to construct a mnemonic circuit using stone knives and bearskins.SpockMy UDFs:Deleted - they were old and I'm lazy ... :)My utilities:Comment stripperPolicy lister 1.07AutoIT Speed Tester (new!)

Link to comment
Share on other sites

Celeri, this isn't really about breaking anything, it's about just plain-ole usability. Modifying ProcessWaitClose() just allows us to write our own version of RunWait() and thats about all it logically provides. Although it would technically work in a polling environment, it doesn't make sense to invoke a function that normally blocks to poll for the return code of a process. Neither of those two methods of use actually fit the name "ProcessWaitClose" so the name would be a misnomer in certain usage cases.

Link to comment
Share on other sites

Celeri, this isn't really about breaking anything, it's about just plain-ole usability. Modifying ProcessWaitClose() just allows us to write our own version of RunWait() and thats about all it logically provides. Although it would technically work in a polling environment, it doesn't make sense to invoke a function that normally blocks to poll for the return code of a process. Neither of those two methods of use actually fit the name "ProcessWaitClose" so the name would be a misnomer in certain usage cases.

Ok thanks for the clarification :o

If I understand the dilemma properly than usually @error and return code are expected to happen as soon as the command(s) return. However Run(params...) only gets an exit code well after coming back ... forcing the question: how do you get the exit code and @error value after a few functions and programs have run?

It would take a function like ProcessGetExitCode($Pid) ... but judging from what I have seen it isn't as simple as that.

BTW I hope I'm not imposing but I think I got to code what you proposed earlier (using DLL calls to get the error code). Now I've never done any DLL calls but for some reason I got lucky and the following code actually works :graduated: (I got VERY lucky). Anyone care to show me the error of my ways? B)

#include<array.au3>
$g = chr(34); Quotes
$a = $g&"G:\zip.exe"&$g; Path to Info-ZIP with Quotes
$b = $g&"H:\Address Book.zip"&$g; Path to zipfile with Quotes
$c = $a&" -T "&$b; Complete command line

$Zip_test = Run($c,"",@SW_HIDE,2); Call and run Info-ZIP.
; BTW never saw any difference with _RunDOS ...

$dwProcessId     = $Zip_test; 
$lpExitCode = 0; Should contain error code after DLL Call?
$PROCESS_QUERY_INFORMATION = 1024; Borrowed from a post by Larry
; --> http://www.autoitscript.com/forum/index.php?showtopic=13808&hl=OpenProcess#

$Var1 = DllCall('kernel32.dll','hwnd','OpenProcess','int',$PROCESS_QUERY_INFORMATION,'int',0,'int',$dwProcessId)
sleep(2000); Ok just wait 2 seconds till ZIP.EXE is finished (too lazy to get screen output)
$Var2 = DllCall("kernel32","hwnd","GetExitCodeProcess","int",$Var1[0],"int_ptr",$lpExitCode); Here we get the code
; Incidentally, the error code (8) comes up in $Var2 but $lpExitCode remains unchanged???
               ; Error code 8 is correct in this case - For some reason InfoZip chokes on this file :(
$Var3 = DllCall("kernel32","hwnd","CloseHandle","int",$Var1[0]); Supposed to close something.
_ArrayDisplay($Var1,"DLLCall: $Var1"); Show first
_ArrayDisplay($Var2,"DLLCall: $Var2"); Show second
_ArrayDisplay($Var3,"DLLCall: $Var3"); And last
ConsoleWrite($Zip_test&@TAB&$lpExitCode&@CR); For the sake of completeness, here are the remaining variables.

; If correct, the answer is found in $Var2[2]

I am endeavoring, ma'am, to construct a mnemonic circuit using stone knives and bearskins.SpockMy UDFs:Deleted - they were old and I'm lazy ... :)My utilities:Comment stripperPolicy lister 1.07AutoIT Speed Tester (new!)

Link to comment
Share on other sites

I think thats correct (I just scanned over it).

Here is the dilemma. As long as you OpenProcess() a handle to the process before it closes, you can use that handle in GetExitCodeProcess(). For a user to implement via DllCall(), it's trivial. The user must call OpenProcess() and then wait until the PID is dead. Trying to add support for this into AutoIt, however, is much trickier. We can't return the necessary handle from Run() because we return the PID already. That means we have 2 choices. We either devise some way to internally keep track of the handle for any given PID (launched by Run()) or we provide a blocking function which will open the handle on entry, block for the time period/process close and then close the handle on exit. The former method will likely introduce either a resource leak (the handle) or introduce the need for a garbage collector to collect the unused handle after a period of time. The second method introduces unintuitive behavior and limits the functionality to being little more than an AutoIt-implemented RunWait().

Resource leaks are bad. I can't condone intentionally allowing a resource to leak, even for the sake of functionality.

Also, I'm not even sure modifying ProcessWaitClose() or introducing another blocking function will even work. What I mean is, it will work, but will it actually provide any functionality that's not already provided by just using RunWait() straight up? A function using that method must be called before the process closes and it must block until the process closes, otherwise, it'll have to have an implementation which leaks resources, too.

The conclusion I'm forming now is that there is no way to implement this correctly in AutoIt as a built-in feature without doing one or more of the following:

  • Placing the burden on the user to close a handle in every situation.
  • Leaking a resource.
  • Implementing a feature which will be virtually useless (if a blocking function has to be called before the process closes, we just have AutoIt-implemented RunWait()).
Quite frankly, I'd rather see the functionality just be done via DllCall() where the user is forced to open the handle and if they choose to leak it, well, that's their choice since they opened the handle in the first place. Users should close what they open; AutoIt should close what it opens. I don't think mixing the two to implement caveat #1 above is acceptable.
Link to comment
Share on other sites

I think thats correct (I just scanned over it).

...

The conclusion I'm forming now is that there is no way to implement this correctly in AutoIt as a built-in feature without doing one or more of the following:

  • Placing the burden on the user to close a handle in every situation.

  • Leaking a resource.

  • Implementing a feature which will be virtually useless (if a blocking function has to be called before the process closes, we just have AutoIt-implemented RunWait()).
Quite frankly, I'd rather see the functionality just be done via DllCall() where the user is forced to open the handle and if they choose to leak it, well, that's their choice since they opened the handle in the first place. Users should close what they open; AutoIt should close what it opens. I don't think mixing the two to implement caveat #1 above is acceptable.

Well now that it's working like a charm, I can wholeheartedly agree with you.

DllCalls still scare the daylights out of me but they seem to do what they are supposed to do.

Anyways (and in real text) here's how I handled my problem:

I need to compress a file with (per example) Rar (console)

Does the user need a detailed report?

YES: Run program and grab info via STDIO, and get Exit Code via DLLCall

NO: Just RunWAIT and get the Exit Code from there.

BTW I would have been more than willing to make a UDF to handle all of this but even then it's not really possible. As you can see, the whole process is done in two parts, first one, get handle, real all data from STDIO untill nothing left and second part is getting the code and closing the handle. There just isn't an elegant way to do it. Whoever needs this functionality will have to cut&paste I guess.

Oh, did I forget to say thanks? B)

I am endeavoring, ma'am, to construct a mnemonic circuit using stone knives and bearskins.SpockMy UDFs:Deleted - they were old and I'm lazy ... :)My utilities:Comment stripperPolicy lister 1.07AutoIT Speed Tester (new!)

Link to comment
Share on other sites

Err. :">

The STD I/O implementation already takes care of everything discussed above as part of a Win98 compatibility fix.

It would be trivial to add the child process's aliveness / exit code being returned in @extended in the StdxxxRead functions.

Yes yes yes, there it was. Youth must go, ah yes. But youth is only being in a way like it might be an animal. No, it is not just being an animal so much as being like one of these malenky toys you viddy being sold in the streets, like little chellovecks made out of tin and with a spring inside and then a winding handle on the outside and you wind it up grrr grrr grrr and off it itties, like walking, O my brothers. But it itties in a straight line and bangs straight into things bang bang and it cannot help what it is doing. Being young is like being like one of these malenky machines.

Link to comment
Share on other sites

  • 5 years later...

Ok thanks for the clarification ;)

If I understand the dilemma properly than usually @error and return code are expected to happen as soon as the command(s) return. However Run(params...) only gets an exit code well after coming back ... forcing the question: how do you get the exit code and @error value after a few functions and programs have run?

It would take a function like ProcessGetExitCode($Pid) ... but judging from what I have seen it isn't as simple as that.

BTW I hope I'm not imposing but I think I got to code what you proposed earlier (using DLL calls to get the error code). Now I've never done any DLL calls but for some reason I got lucky and the following code actually works :idiot: (I got VERY lucky). Anyone care to show me the error of my ways? :)

#include<array.au3>
$g = chr(34); Quotes
$a = $g&"G:\zip.exe"&$g; Path to Info-ZIP with Quotes
$b = $g&"H:\Address Book.zip"&$g; Path to zipfile with Quotes
$c = $a&" -T "&$b; Complete command line

$Zip_test = Run($c,"",@SW_HIDE,2); Call and run Info-ZIP.
; BTW never saw any difference with _RunDOS ...

$dwProcessId     = $Zip_test; 
$lpExitCode = 0; Should contain error code after DLL Call?
$PROCESS_QUERY_INFORMATION = 1024; Borrowed from a post by Larry
; --> http://www.autoitscript.com/forum/index.php?showtopic=13808&hl=OpenProcess#

$Var1 = DllCall('kernel32.dll','hwnd','OpenProcess','int',$PROCESS_QUERY_INFORMATION,'int',0,'int',$dwProcessId)
sleep(2000); Ok just wait 2 seconds till ZIP.EXE is finished (too lazy to get screen output)
$Var2 = DllCall("kernel32","hwnd","GetExitCodeProcess","int",$Var1[0],"int_ptr",$lpExitCode); Here we get the code
; Incidentally, the error code (8) comes up in $Var2 but $lpExitCode remains unchanged???
               ; Error code 8 is correct in this case - For some reason InfoZip chokes on this file :(
$Var3 = DllCall("kernel32","hwnd","CloseHandle","int",$Var1[0]); Supposed to close something.
_ArrayDisplay($Var1,"DLLCall: $Var1"); Show first
_ArrayDisplay($Var2,"DLLCall: $Var2"); Show second
_ArrayDisplay($Var3,"DLLCall: $Var3"); And last
ConsoleWrite($Zip_test&@TAB&$lpExitCode&@CR); For the sake of completeness, here are the remaining variables.

; If correct, the answer is found in $Var2[2]

This was very helpful and I thought I would add my comments.

The reason why the $lpExitCode was not updated was because the function expects a pointer (not a value).

Here is another example using your logic which also works.

$PROCESS_QUERY_INFORMATION = 1024

$Process = Run( "XCOPY.EXE A B", @SystemDir )

If ProcessExists( $Process ) Then
    ; Open Handle to retrieve return code later
    Local $ProcessHandle = DllCall('kernel32.dll','hwnd','OpenProcess','int',$PROCESS_QUERY_INFORMATION,'int',0,'int',$Process)

    ProcessWaitClose( $Process )

    ; Retrieve return code from call
    Local $ReturnCode = DllStructCreate("int")

    DllCall("kernel32","hwnd","GetExitCodeProcess","int",$ProcessHandle[0],"int_ptr",DllStructGetPtr($ReturnCode,1)); Here we get the code
    DllCall("kernel32","hwnd","CloseHandle","int",$ProcessHandle[0]); Supposed to close something.

    DllStructGetData($ReturnCode,1)

    ConsoleWrite( "Got it - " & DllStructGetData($ReturnCode,1) )
Else
    ConsoleWrite( "Missed it" )
EndIf

This should return the expected error code of '4' for xcopy not finding the files specified. (Assuming you don't have files called 'A' and 'B')

Link to comment
Share on other sites

  • 2 weeks later...

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...