Killing java processes started from Port.open

Hi all, my current work involves interacting with MS Project files. The binary file format is pretty tedious to interact with. The best third party library I have found for reading the files is written in Java.

I have written a small test Java project that I have interoperating with a test Elixir project following guidance from this post https://www.theerlangelist.com/article/outside_elixir from @sasajuric.

The only problem is that the java program doesn’t die when the owning BEAM process dies or BEAM is terminated. This is discussed here: http://erlang.org/pipermail/erlang-questions/2016-November/090910.html, and it sounds like it all relies on the program invoked by Port.open being “well-behaved” and shutting down if the stdin channel is killed.

I have trawled the Java docs and aside from realising how convoluted the process is for writing anything useful in Java (compared with Elixir), I haven’t found anything to help.

So the questions are:

  1. has anyone managed to get a well-behaved interop between Java and Elixir using Ports.
  2. If so, what does your main System.in reading code look like / how did you solve the problem of shutting down the java program without explicit termination commands from the Elixir side
  3. If not, what are the preferred options for Java interop. I am aware of JInterface but wanted to keep operational complexity down as far as possible and also minimise the amount of Java code.

I know this is probably a Java problem rather than an Elixir problem, but the Ports style interop for long-running processes through stdin/stdout does seem to be unique to Erlang/Elixir.

Edit: Only tries on Windows so far. Will year on Linux tomorrow.

Thanks!

1 Like

Yes, if you want proper cleanup, the external program has to be well-behaved. I’ve briefly touched on that in the article, in the “program termination” section. So basically, you need to make the Java program terminate if you receive EOF on stdin. No idea how this can be done in Java, but I’d be surprised if this wasn’t possible :slight_smile:

If you don’t control the external program, you could use erlport, which will automatically terminate the external program if the owner beam process (or the entire beam node) stops.

4 Likes

Besides of the already mentioned erlport there are a couple of other projects available, like porcelain which relies on a wrapper written in Golang, which has to be installed seperately, or rambo which explicitely not only manages the lifecycle of the program, but also is able to wrap those programs that do only print their own reply after stdin got closed. Its wrapper is written in Rust, and as far as I remember you need to have rust installed to compile the library.

1 Like

Thanks - I’m watching for EOF in my code and that doesn’t appear to be triggering. I’ll double-check and go to ErlPort if I can’t get it working.

Thanks @NobbZ - I have no problem receiving the inputs from the external process, it’s just shutting it down cleanly afterwards that’s the problem.

I’ll double-check receives of EOF first, then I’ll try on Linux to see if there is some weird Windows effect, then I’ll see if ErlPorts does a more aggressive shutdown, then I’ll take a look at the other options you presented.

Thanks to you both for your suggestions. I’ll let you know how I go.

OK, problem exists between chair & keyboard. My main loop was badly behaved. I stripped it back to the most basic stdin read loop and it terminates ok. Time to start building it up again and see where it fails.

For reference, here’s a minimal Java program that works nicely over ports, terminating when the process or BEAM terminates (including brutally killing from Windows task manager):

package com.thing;
import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException {
        int ch;
        while(true) {
            ch = System.in.read();
            if (ch == -1) {break;} // This is the EOF signal in Java
            if (ch == 'q') {break;}
            if (ch == 't') {System.out.println("tasks");}
            System.out.println(" got " + ch + " - " + (char)ch);
        }
    }
}
3 Likes

Creator of Rambo here. Don’t use libraries if you’re writing your own port. Libraries are for working around external programs that cannot be changed.

Your minimal program is a good start, just need to read (block on) standard input on another thread so your MS Project job can run.

If you don’t care about JVM startup time, just run your port with System.cmd on demand. Otherwise if your Java process is long-lived, you’ll need to consider the communication protocol between your Java and Erlang processes.

5 Likes

Thanks for the mention. To clarify, Rambo only requires the Rust compiler if you are not using Linux, macOS or Windows. Bundled binaries should cover most users.

2 Likes

Thanks @jayjun - yes, the java process will be long lived. I’ve worked up a more extended example that uses Erlang ETF to transmit the payload back and forth. The Ericsson JInterface library has all the parsing and packaging routines. There are a couple of gotchas that I’ll write up (eg the OtpOutputStream doesn’t write the first byte - 131 - for some reason so you have to do that by hand), but it seems pretty straightforward otherwise. The Java side can respond synchronously to each call in my case which simplifies things on that side.

1 Like

Cool, just be careful of the classic pipe buffer deadlock if you’re not continuously reading while writing. And you cannot terminate early if the port is closed or if the Erlang node dies.

1 Like

Thanks @jayjun - that sounds like some hard-won experience right there!

Hi @mindok, might you by any chance have an easy to share example that solves these issues? Thanks a lot!

Hi @why, not easy to share at the moment. I’ll dm you…

Hi @why,

You can take a look here for some sample code:

1 Like

Thanks a ton @mindok :slight_smile:

Very much appreciate it!