imagej / imagej2

Open scientific N-dimensional image processing :microscope: :sparkler:
https://imagej.net/
BSD 2-Clause "Simplified" License
1.17k stars 333 forks source link

Allow returning error codes when running scripts headlessly #61

Open ctrueden opened 10 years ago

ctrueden commented 10 years ago

When executing ImageJ scripts, plugins or macros headlessly, it would be nice if the process could give a non-zero return code in case of error. Currently, ImageJ1 does not do this.

Added by @luxigo:

It would be helpful if Fiji exit code was not zero when an unhandled exception (eg: java.awt.HeadlessException) is raised in headless mode... at least optionally.

ctrueden commented 10 years ago

See also the relevant discussion in #fiji-devel.

carandraug commented 10 years ago

Here's the email I was ready to send to the mailing list:

I have some macros that I'm running in ImageJ in headless mode. After running I was checking the exit status to make sure that no error happened. However, I came to notice that there errors but ImageJ has always been issuing a exit status of zero. How to reproduce

echo "this is bad syntax" > macro.ijm && java -jar .../jars/ij-1.48t.jar -batch ./macro.ijm && echo $?

This always prints "0" but if there was an error, shouldn't the exit status be non-zero? I couldn't find anything on the builtin functions to specify a specific value, the closest I saw was the "exit ()" command which accepts an optional error message [1]

echo "exit ('bad exit');" > macro.ijm && java -jar ../../Applications/Fiji-32bit/jars/ij-1.48t.jar -batch ./macro.ijm && echo $?

Which prints the error message "bad exit", but still returns 0 for exit value.

[1] http://rsbweb.nih.gov/ij/developer/macro/functions.html#exit

ctrueden commented 10 years ago

Pointed out by @dscho: ImageJ does support executing more than one macro at a time this way, so it's a little tricky deciding which return code to give back if the multiple macros had different errors. My first cut at that would probably be something like "return the error code with the greatest magnitude" so that if any macro failed, you get a failure code.

fredricj commented 10 years ago

We have a similar problem with ImageJ not returning anything other than 0 on errors

dscho commented 10 years ago

@fredricj do you really mean scripts? Or macros?

I ask because in scripts you can easily call System.exit(1); yourself. It's a bit harsh, not letting any service dispose cleanly, but since you are running headlessly anyway...

bethac07 commented 4 years ago

I'm not sure if this is the right place to be reporting this, but using sys.exit does not in fact seem to work, at least in Python scripts- inside the FIJI docker, running a script sysexit1.py that's literally just

import sys

print('sys exit 1')
sys.exit(1)

and executing it with Fiji.app/ImageJ-linux64 --ij2 --headless --console --run "Fiji.app/plugins/sysexit1.py"

Gives the following output:

OpenJDK 64-Bit Server VM warning: ignoring option PermSize=128m; support was removed in 8.0
OpenJDK 64-Bit Server VM warning: Using incremental CMS is deprecated and will likely be removed in a future release
sys exit 1
[ERROR] null
Traceback (most recent call last):
  File "Fiji.app/plugins/sysexit1.py", line 4, in <module>
    sys.exit(1)
SystemExit: 1

    at org.python.core.PySystemState.exit(PySystemState.java:1505)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.python.core.PyReflectedFunction.__call__(PyReflectedFunction.java:188)
    at org.python.core.PyReflectedFunction.__call__(PyReflectedFunction.java:206)
    at org.python.core.PyObject.__call__(PyObject.java:497)
    at org.python.core.PyObject.__call__(PyObject.java:501)
    at org.python.core.PyMethod.__call__(PyMethod.java:141)
    at org.python.pycode._pyx0.f$0(Fiji.app/plugins/sysexit1.py:4)
    at org.python.pycode._pyx0.call_function(Fiji.app/plugins/sysexit1.py)
    at org.python.core.PyTableCode.call(PyTableCode.java:171)
    at org.python.core.PyCode.call(PyCode.java:18)
    at org.python.core.Py.runCode(Py.java:1614)
    at org.python.core.__builtin__.eval(__builtin__.java:497)
    at org.python.core.__builtin__.eval(__builtin__.java:501)
    at org.python.util.PythonInterpreter.eval(PythonInterpreter.java:259)
    at org.python.jsr223.PyScriptEngine.eval(PyScriptEngine.java:57)
    at org.python.jsr223.PyScriptEngine.eval(PyScriptEngine.java:31)
    at javax.script.AbstractScriptEngine.eval(AbstractScriptEngine.java:264)
    at org.scijava.script.ScriptModule.run(ScriptModule.java:160)
    at org.scijava.module.ModuleRunner.run(ModuleRunner.java:168)
    at org.scijava.module.ModuleRunner.call(ModuleRunner.java:127)
    at org.scijava.module.ModuleRunner.call(ModuleRunner.java:66)
    at org.scijava.thread.DefaultThreadService$3.call(DefaultThreadService.java:238)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at java.lang.Thread.run(Thread.java:748)
fiji@518285d69ae9:/opt/fiji$ echo $?
0
carandraug commented 4 years ago

I confirm that you can use sys.exit(1) without docker and with any script outside the scripts directory. The issue is that it requires the script to call sys.exit so you effectively need to wrap the entire script in a try/catch block. Note that entire even includes importing because you never know where the error that causes the script to fail may come from:

import something 
# if the import fails, then imagej still exits with error code 0.

try:
    something.do_it()
except:
    sys.exit(1)
$ Fiji.app/ImageJ-linux64 --ij2 --headless --console --run "./fail-python.py" 
[...]
[ERROR] Traceback (most recent call last):
  File "/home/carandraug/fail-python.py", line 1, in <module>
    import something
ImportError: No module named something
[...]
$ echo $?
0

And this even assuming that there's no syntax error or your script.

bethac07 commented 4 years ago

No, even if my script hits an except loop and exits there, FIJI still returns an exit code of 0.

import sys

print('sys exit 1')
try:
    import pathlib
except:
    print('in the except')
    sys.exit(1)
fiji@518285d69ae9:/opt/fiji$ Fiji.app/ImageJ-linux64 --ij2 --headless --console --run "Fiji.app/plugins/sysexit1.py"
OpenJDK 64-Bit Server VM warning: ignoring option PermSize=128m; support was removed in 8.0
OpenJDK 64-Bit Server VM warning: Using incremental CMS is deprecated and will likely be removed in a future release
sys exit 1
in the except
[ERROR] null
Traceback (most recent call last):
  File "Fiji.app/plugins/sysexit1.py", line 8, in <module>
    sys.exit(1)
SystemExit: 1

    at org.python.core.PySystemState.exit(PySystemState.java:1505)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.python.core.PyReflectedFunction.__call__(PyReflectedFunction.java:188)
    at org.python.core.PyReflectedFunction.__call__(PyReflectedFunction.java:206)
    at org.python.core.PyObject.__call__(PyObject.java:497)
    at org.python.core.PyObject.__call__(PyObject.java:501)
    at org.python.core.PyMethod.__call__(PyMethod.java:141)
    at org.python.pycode._pyx0.f$0(Fiji.app/plugins/sysexit1.py:8)
    at org.python.pycode._pyx0.call_function(Fiji.app/plugins/sysexit1.py)
    at org.python.core.PyTableCode.call(PyTableCode.java:171)
    at org.python.core.PyCode.call(PyCode.java:18)
    at org.python.core.Py.runCode(Py.java:1614)
    at org.python.core.__builtin__.eval(__builtin__.java:497)
    at org.python.core.__builtin__.eval(__builtin__.java:501)
    at org.python.util.PythonInterpreter.eval(PythonInterpreter.java:259)
    at org.python.jsr223.PyScriptEngine.eval(PyScriptEngine.java:57)
    at org.python.jsr223.PyScriptEngine.eval(PyScriptEngine.java:31)
    at javax.script.AbstractScriptEngine.eval(AbstractScriptEngine.java:264)
    at org.scijava.script.ScriptModule.run(ScriptModule.java:160)
    at org.scijava.module.ModuleRunner.run(ModuleRunner.java:168)
    at org.scijava.module.ModuleRunner.call(ModuleRunner.java:127)
    at org.scijava.module.ModuleRunner.call(ModuleRunner.java:66)
    at org.scijava.thread.DefaultThreadService$3.call(DefaultThreadService.java:238)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at java.lang.Thread.run(Thread.java:748)
fiji@518285d69ae9:/opt/fiji$ echo $?
0
carandraug commented 4 years ago

No, even if my script hits an except loop and exits there, FIJI still returns an exit code of 0.

You're right, I tested it wrong. Using sys.exit does not work to cause ImageJ to exist with another error code.

krokicki commented 3 years ago

We are also running into this issue. A workaround we use is to parse the STDOUT stream and detect keywords like "Exception" and "Macro Error", but it's a lot of extra housekeeping code anytime you want to run Fiji, and it's just a heuristic. It often has false positives because there are many benign exceptions that happen in headless mode.

To allow running ImageJ in batch mode robustly, if any user macro throws an exception and doesn't complete, the return code should be non-zero.