Search code examples
basherlangsherlang-ports

Shell script behaves strangely when called via an Erlang port


When calling shell scripts from Erlang, I generally need their exit status (0 or something else), so I run them using this function:

%% in module util
os_cmd_exitstatus(Action, Cmd) ->
    ?debug("~ts starting... Shell command: ~ts", [Action, Cmd]),
    try erlang:open_port({spawn, Cmd}, [exit_status, stderr_to_stdout]) of
        Port -> 
            os_cmd_exitstatus_loop(Action, Port)
    catch
        _:Reason ->
            case Reason of
                badarg ->
                    Message = "Bad input arguments";
                system_limit ->
                    Message = "All available ports in the Erlang emulator are in use";
                _ ->
                    Message = file:format_error(Reason)
            end,
            ?error("~ts: shell command error: ~ts", [Action, Message]),
            error
    end.

os_cmd_exitstatus_loop(Action, Port) ->
    receive
        {Port, {data, Data}} ->
            ?debug("~ts... Shell output: ~ts", [Action, Data]),
            os_cmd_exitstatus_loop(Action, Port);
        {Port, {exit_status, 0}} ->
            ?info("~ts finished successfully", [Action]),
            ok;
        {Port, {exit_status, Status}} ->
            ?error("~ts failed with exit status ~p", [Action, Status]),
            error;
        {'EXIT', Port, Reason} ->
            ?error("~ts failed with port exit: reason ~ts", 
                         [Action, file:format_error(Reason)]),
            error
    end.

This worked fine, until I used this to start a script which forks off a program and exits:

#!/bin/sh

FILENAME=$1

eog $FILENAME &

exit 0

(In the actual usecase, there are quite a few more arguments, and some massaging before they are passed to the program). When run from the terminal, it shows the image and exits immediately, as expected.

But running from Erlang, it doesn't. In the log file I see that it starts fine:

22/Mar/2011 13:38:30.518  Debug: Starting player starting... Shell command: /home/aromanov/workspace/gmcontroller/scripts.dummy/image/show-image.sh /home/aromanov/workspace/media/images/9e89471e-eb0b-43f8-8c12-97bbe598e7f7.png

and the eog window appears. But I don't get

22/Mar/2011 13:47:14.709  Info: Starting player finished successfully

until killing the eog process (with kill or just closing the window), which isn't suitable for my requirements. Why the difference in behavior? Is there a way to fix it?


Solution

  • Normally if you run a command in background with & in a shell script and the shell script terminates before the command, then the command gets orphaned. It might be that erlang trys to prevent orphaned processes in open_port and waits for eog to terminate. Normally if you want to run something in background during a shell script you should put in a wait at the end of the script to wait for your background processes to terminate. But this is exactly what youd don't want to do.

    You might try the following in your shell script:

    #!/bin/sh
    
    FILENAME=$1
    
    daemon eog $FILENAME
    
    # exit 0 not needed: daemon returns 0 if everything is ok
    

    If your operating system has a daemon command. I checked in FreeBSD and it has one: daemon(8)

    This is not a command available on all Unix alike systems, however there might be a different command doing the same thing in your operating system.

    The daemon utility detaches itself from the controlling terminal and executes the program specified by its arguments.

    I'm not sure if this solves your problem, but I suspect that eog somehow stays attached to stdin/stdou as a kind of controling terminal. Worth a try anyway.

    This should also solve the possible problem that job control is on erroneously which could also cause the problem. Since daemon does exit normally your shell can't try to wait for the background job on exit because there is none in the shells view.

    Having said all this: why not just keep the port open in Erlang while eog runs?

    Start it with:

    #!/bin/sh
    
    FILENAME=$1
    
    exec eog $FILENAME
    

    Calling it with exec doesn't fork it bu replaces the shell process with eog. The exit status you'll see in Erlang will then be the status of eog when it terminates. Also you have the possibility to close the port and terminate eog from Erlang if you want to do so.