Navigation Menu

Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Specify Exit Code for exe #874

Open
Greedquest opened this issue May 24, 2022 · 17 comments
Open

Specify Exit Code for exe #874

Greedquest opened this issue May 24, 2022 · 17 comments
Labels
discussion enhancement New feature or request

Comments

@Greedquest
Copy link

Greedquest commented May 24, 2022

Is your feature request related to a problem? Please describe.

As well as stderr, stdout, exe's also specify an exit code - 0 indicating success, non-zero indicating failure. This is a very useful standard to integrate tB into other toolchains.

Describe the solution you'd like

Option 1:

A property for the ExitCode of the app (as opposed to an HResult of a function):

Err.ExitCode = exit_code 'sets exit code

Option 2:

A method to both set the exit code and raise an error that triggers the program to exit with normal error propagation

Sub Main()
    If Not TryDoStuff() Then
	    Err.Exit 10 'this is like Err.Raise, except it also sets the exit code
	End If
    'implicit default exit code of 0
End Sub

This is how Python does it, as it enables with statements to close cleanly, and finally blocks to run by default, no special casing in the language required.

Option 3:

Instead of manually specifying the exit code, make the program exit with Err.Number. That way the last error raised is the exit code of the program.

Sub Main()
     Err.Raise 5
End Sub
 > mytb_win32.exe //calls our compiled tB app
 > echo $LASTEXITCODE
5

Option 3a:
Return the Err.Number as exit_code as a sensible default for uncaught errors, but use one of the other options for manually specifying the exit code.

Option 3b:
As above, but always return 1 by default for uncaught errors. This is what python does:

> python -c "raise RuntimeError()"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
RuntimeError
> $LASTEXITCODE
1   //always 1 regardless of the kind of uncaught exception

Option 4:
We allow Function Main() As Long as the entry point instead of Sub Main() - similar to C's int Main(). The return value is the exit code

Function Main() As ULong
    If Not TryDoStuff() Then
	    Return 10 'this is the exit code
	End If
    'implicit default exit code of 0 since that is what As ULong defaults to
End Function

Describe alternatives you've considered

Private Declare Sub ExitProcess Lib "kernel32" (ByVal uExitCode As Long)

But that is unsafe. It means we can't run any cleanup as the process exits immediately. IDK if tB can even do proper COM cleanup of shared objects.

Python does include os._exit([exit_code]) for immediate exit, but sys.exit([exit_code]) is preferred (this raises an untrappable exception to gradually exit the program but still run finally blocks). The former is only really used for child threads.

As @wqweto points out, FreeBASIC has an optional parameter to the End statement
however from the docs

Usage of this statement does not cleanly close scope. Local variables
will not have their destructors called automatically, because
FreeBASIC does not do stack unwinding. Only the destructors of global
variables will be called in this case.

For this reason, it is discouraged to use End simply to mark the end of a program;
the program will come to an end automatically, and in a cleaner fashion,
when the last line of module-level code has executed.

Additional context

I want to consider how this fits with potential try...catch...else...finally blocks. twinbasic/lang-design#61 Particularly, a way to exit the program while still running finally blocks (and maybe even Class_Terminate, although that doesn't happen with classic VBx errors)

@Greedquest Greedquest changed the title Specify Exit Code for exe in non-gui mode Specify Exit Code for exe May 24, 2022
@wqweto
Copy link

wqweto commented May 24, 2022

FreeBasic has an optional exit code to End statement instead which might be worth considering too.

@Kr00l
Copy link
Collaborator

Kr00l commented May 24, 2022

End statement is not the "clean style". So needing to use that to make an exit code is forcing people to apply this style.
There should be an alternative way which allows to let the app end automatically and naturally.

@Greedquest
Copy link
Author

Greedquest commented May 24, 2022

@wqweto @Kr00l updated with more options.

@Greedquest
Copy link
Author

Greedquest commented May 24, 2022

My thoughts:


Option 1 is most explicit and flexible, allowing you to redefine the exit code multiple times. However this also makes it hard to track the ExitCode through the app if it can be set from anywhere. ExitCode is part of the exe's public API so should be as close as possible to the Entry Point routine.


Option 2 is equally explicit but combines the ideas of exit code + raising an error to force the program to terminate, making it easier to identify from a traceback exactly what set the error code (since errors bubble). It still has the flexibility of redefining the error number by trapping it and re-raising a new exit_code. I think this makes it strictly better than Option 1. However there is this risk:

Private Sub TriggerExit()
	Err.Exit 123
End Sub

Private Sub BadSuppress() 'easy to suppress the exit error
	On Error Resume Next
	TriggerExit
	On Error Goto 0 'clears Err.Number but not Err.ExitCode?
End Sub

Private Sub BadReThrow() 'easy to suppress the exit error
	On Error Goto HandleError
	TriggerExit
HandleError:
	If Err.Number = 5 Then Exit Sub 'ignore this error
	Err.Raise Err.Number, description:= Err.Description & "[unhandled]" 'rethrow last error with better error message
	'BUG: we want Err.Exit not Err.Raise since the latter doesn't set the ExitCode
End Sub

Sub Main()
    BadSuppress
    BadReThrow
End Sub

... Although that can be circumvented with this pattern of only ever exiting from the top level of the program:

'The Main _Function_ takes the command line args and returns the exit code
Function Main(args... As Variant) As Long
    On Error Goto ErrHappened
    'do stuff
    Return 0
	
ErrHappened:
    Return exit_code
End Function

Sub Main() 'entry point
   Err.Exit Main(args...) 'throws an error and sets the exit code
End Sub

Python makes it a lot harder to suppress or redefine the SystemExit exception in this way by making it pretty much untrappable without being explicit. However python's exception system is much more powerful than VBx's


Option 3 is less explicit and flexible than the other 2, but I think most idiomatic; it doesn't add anything new to the language, and I think VBx devs are used to seeing a single error number representing the outcome of their app. This gets my vote

Option 3 I think is dumb - uncaught errors should be indicated with traceback to stderr/ a message box.

However Option 3b of returning a 1 indicating an uncaught error is sensible IMO. I was very confused when my tB app had an error but returned 0 because I hadn't said otherwise! It should return 1 to make sure the caller doesn't just plough on unaware of the error.

Option 3a may result in number collisions; e.g. I say my program will return exit code 5 to indicate "file not found", but then I get an uncaught "Invalid Procedure Call" bubble up which also sets the exit-code to 5, that would be bad.


Option 4 I think is too implementation details-y level for VBA programmers, and introduces 2 kinds of Main entry point methods which is confusing (what if I define both?)


Looking at tB's latest draft of design principles, I'd say

  • Options 1, 2 & 4 trump on Transparency Rocks since the exit code is set explicitly
  • Options 3 wins on Consistency matters since it is most idiomatic to use VBA error numbers as the exit code
  • Options 3 & 4 do well on Readable code as both set the exit code only as the program exits from Sub Main, rather than at the bottom of a bubbling up error (with option 2), or somewhere random with option 1.
  • Option 4 loses on One way is better than several ways by creating a second kind of entry point, it also loses points on Solving problems is the point by being a little too C++/C ish feeling.

Therefore I feel option 3 is best immediately, option 2 may become viable if tB develops some more powerful error/ exception control structures and types, as well as proper stack trace support to make the source of the Exit signal easy to find. It should be used in combination with a default exit code for unhandled errors (option 3b).

@wqweto
Copy link

wqweto commented May 24, 2022

What does VB.Net use? Copy this :))

Main function is most natural IMO. Main sub vs Main function is a moot argument (not sure if allowed in TB) because one can have two Main subs overloaded too (if not mistaken)

@EduardoVB
Copy link

I don't think that someone could want to design the program to exit with the code of an unhandled error.
I'm also inclined to option 2.

@Kr00l
Copy link
Collaborator

Kr00l commented May 25, 2022

I like more the Err.ExitCode idea and solution IMO. The good part of that idea is that it's like a variable you can always refer to.

@Greedquest
Copy link
Author

Greedquest commented May 25, 2022

@wqweto vb.net offers option 1 or option 4: i.e. a variable Environment.ExitCode that can be set and overwritten at any time, and also Sub Main() -> Function Main() As Integer.

In fact Main has 4 possible overloads:

Sub Main()

Sub Main(ByVal cmdArgs() As String)

Function Main() As Integer

Function Main(ByVal cmdArgs() As String) As Integer

I don't know what would happen if you overload those with further arguments in .Net. Possibly a compiler error. Possibly you would just get a sub called Main but without the special behaviour of being the entry point. Certainly it would be a behaviour for a new user to learn rather than being obvious.

Personally I'm not a fan of tB having an implicit entry point, I would prefer to be able to annotate any Sub as the entry point explicitly. But I understand this is a default to be consistent with VB6.


That aside, I don't find the syntax that natural as you say. In VBx, we have a function to read the command line args - Command$ rather than taking them as a parameter to Main(ByVal command As String). I think the language-consistent approach therefore, would be to set the exit code using a method too, rather than as a slightly magic optional function return value. There is a risk IMO of making the Main entry point have some incomprehensible boiler plate that beginners don't understand, and I don't think providing multiple overloads (only some of which actually work as entry points) makes that simpler for them either. One way is better than many ways. 99% of people don't care about CLI exit code. That's why I lean more towards python's pattern with sys.argv and sys.exit as optional things you can use as and when needed:

import sys


def main(args=None):  # take optional args as Parm for unit testing
    if args is None:
        args = sys.argv()  # read from command line
    # continue


sys.exit(main())

But maybe that's just a consequence of python not having overloads... I don't know, I like the simplicity and lack of magic.


On the subject of one vs many ways, vb.net also allows a third option. Environment.Exit is the same as the unsafe option I immediately discounted:

Private Declare Sub ExitProcess Lib "kernel32" (ByVal uExitCode As Long)

And requires security permission to run unmanaged code. tB will always have the ability to call this method with dll declares, and I admit, don't really know the use case, something to do with multithreading. I don't think it should be the default because it exits without any cleanup. Maybe a wrapper can be added to the standard tB library when cross-platform becomes important.

@Greedquest
Copy link
Author

Greedquest commented May 25, 2022

@EduardoVB Devil's advocate here (I also quite like option 2) - do you think it's sensible to use error propagation to terminate the program when VBx lacks particularly strong exception handling:

  • No tracebacks to show where the error originated (may be added with vbWatchdog)
  • No exception objects meaning this Err.Exit error is indistinguishable from another error with the same error number
  • No exception control flow structures (may be added with try...finally, +this error should just bubble up and not be caught by anyone ideally so proper control flow may not even be necessary)

I'm just worried about the situation where the exit_code is set but then the Exit error is suppressed and then nobody remembers to reset the exit_code to 0, meaning the program terminates with non-zero exit code unintentionally. Maybe this can be done automatically, similar to how Err.Number gets cleared every so often.

This is even more of a concern with option 1 IMO @Kr00l, how do you ensure users keep track of the current exit_code status throughout the application?


I don't think that someone could want to design the program to exit with the code of an unhandled error.

Now I think about it properly, I guess my problem is currently tB pops a useless "unhandled error" message box, rather than a traceback or even just the Err.Number. If tB included that then the exit code wouldn't also need to convey that information.

That said, I think it's bad behaviour that at the moment even when there is an unhandled error , the exit code is set to 0. Python always sets it to 1 if there is an uncaught exception:

> python -c "raise RuntimeError()"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
RuntimeError
> $LASTEXITCODE
1

Let's make a modification to Option 3: tB should by default return a non-zero exit code (let's say 1) if there was an unhandled error (as well as give a more detailed traceback or Err.Number, Err.Description to stderr/ as a messagebox).

Update

Added 3a and 3b as new options in original post, to be used in combination with the other options. My leaning is Option 2 + Option 3b

@Kr00l
Copy link
Collaborator

Kr00l commented May 25, 2022

@Greedquest it's like with everything. It can be mis-used ..

So, normally one would apply the exit_code on the last line of Sub Main.

Sub Main()
Dim exit_code As Long
' do stuff
Err.ExitCode = exit_code
End Sub

Of course if someone uses Err.ExitCode in the wild and all over the place it can be difficult to track. But that's up to the developer..

I suggest using the Err object for that, because that doesn't pollute the namespace and is in line with other goodies, e.g. Err.LastDllError, Err.ReturnHResult, Err.LastHResult which are similar in a way that it doesn't relate exactly to the current error track.

@Greedquest
Copy link
Author

Greedquest commented May 25, 2022

@Kr00l I know, I'm just trying to think how to minimise misuse - i.e. is there a syntax that makes it harder to do so, rather than just enforcing by convention (using the pattern you describe). For example, Option 4, returning the exit code from the main function, it is unambiguous that the final point it is set is inside that function. Option 2, you can follow the error traceback to find the point that it was set (if we get a traceback). Using Err.ExitCode is like having a global variable, you can only enforce sensible practices by convention, they are not built-in by design.

@Greedquest
Copy link
Author

I'm gonna be quiet for a bit and open up the floor for discussion, I feel like I've said more than enough already! Don't want to monopolise the conversation 😅

@EduardoVB
Copy link

EduardoVB commented May 25, 2022

If tB is not going to unload forms and terminate objects (and do other cleaning tasks to end the program normally), you can already exit with whatever code that you want by using the ExitProcess API.

Do not use it in the IDE because it will close the IDE.

Private Declare Sub ExitProcess Lib "kernel32.dll" (ByVal uExitCode As Long)

Private Sub Command1_Click()
    ExitProcess 5
End Sub

PS: I see it is already mentioned in the first post.

@bclothier
Copy link
Collaborator

TBH, I don't like the idea of Err.Exit or Err.ExitCode --- this leads to this possible construct:

Public Sub Main()
On Error GoTo Doh

  Derp
  Exit Sub

Doh:
  If Err.ExitCode = 1 Then
    Err.ExitCode =2
  End If
End Sub

Private Sub Derp()
  Err.ExitCode = 1 'Or Err.Exit 1
End Sub

Which feels totally wrong. Because it'd be a global variable or a method called from somewhere, it feels messy.

The option of returning a code from the main sub, IMO is more sensible because we can then control how the exitcode is assigned then potentially re-interpreted as it bubbles up through the callstack.

I'm good with unhandled error returning as an exitcode, too. Not sure what is the VB6's behavior in this case if there's any back-compat issue.

@EduardoVB
Copy link

Not sure what is the VB6's behavior in this case if there's any back-compat issue.

IFAIK VB6 doesn't return any kind of exit codes.

@mansellan
Copy link

Option 4 could simplified by having the compiler disallow overloads of Main - you can have a sub or a function as int, but not both.

@EduardoVB
Copy link

Humm, I thought that the option of Function Main was Option 2.
Then option 4 is my choice.

Or whatever option is the one that makes Main to return the exit code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discussion enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

6 participants