Ok,
Here’s an ugly PoC chunk of java code. I have extracted out the bits that are sensitive so it almost certainly won’t work as-is, but hopefully it gives you some pointers. You will need to install the Ericsson otp java package (see the import statement at the top).
There were a couple of gotchas:
- Marshalling data types - strings, doubles, dates etc all need to be explicitly marshalled between built-in Java datatypes and otp ready data types. I wrote quite a few helper functions to do this.
- The Ericsson package fails to emit a required byte to the stream of bytes sent back to the port, so the
sendResponse
method has the required hack to make it work. - Note the warnings from @jayjun in Killing java processes started from Port.open - #10 by jayjun re: port buffer deadlocks. I haven’t handled that potential issue as it was good enough for our PoC as-is
Don’t pick on my java code - I hadn’t written any since 2005 before this.
package com.mindok;
import java.io.*;
import java.nio.ByteBuffer;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import com.ericsson.otp.erlang.*;
public class Main {
static SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd' 'HH:mm");
public static void main(String[] args) throws Exception {
OtpErlangObject payload = null;
while(true) {
try {
payload = receivePayload();
} catch (Exception e) {
sendResponse(toOtpString("Receive " + e.getStackTrace()));
break;
}
if (payload == null) {
sendResponse(toOtpString("No payload received - exiting"));
break;
}
OtpErlangObject response = null;
try {
response = processPayload(payload);
} catch (Exception e) {
sendResponse(toOtpString("Process " + e.getMessage()));
Object[] ste = e.getStackTrace();
for (int i=0; i < 5; ++i) {
sendResponse(toOtpString("Process " + ste[i]));
}
break;
}
if (response == null) {
sendResponse(toOtpString("No response received - exiting"));
break;
}
try {
sendResponse(response);
} catch (Exception e) {
sendResponse(toOtpString("Send " + e.getStackTrace()));
break;
}
}
}
private static void sendResponse(OtpErlangObject response) throws IOException {
OtpOutputStream otpOutputStream = new OtpOutputStream(response);
byte[] output = otpOutputStream.toByteArray();
System.out.write(encodeLength(output.length + 1)); // Add one for the header byte added below
System.out.write((byte) 131); // for some reason the encoding doesn't include the very thing that identifies it as Erlang ETF
System.out.write(output);
}
private static byte[] encodeLength(int length) {
ByteBuffer bb = ByteBuffer.allocate(4);
bb.putInt(length);
return bb.array();
}
private static OtpErlangObject processPayload(OtpErlangObject payload) {
OtpErlangTuple tuple = (OtpErlangTuple)payload;
OtpErlangAtom command = (OtpErlangAtom)tuple.elementAt(0);
OtpErlangObject commandParameters = tuple.elementAt(1);
switch (command.atomValue()) {
case "open_file" :
return packageOutput("open_file", openFile(commandParameters));
case "something_else" :
// *** etc
default:
return payload;
}
}
private static OtpErlangObject openFile(OtpErlangObject commandParameters) {
String outcome = "ok";
String msg = "Successfully read ";
String fileName = "";
try {
fileName = new String(((OtpErlangBinary)commandParameters).binaryValue());
msg = "Successfully read " + fileName;
} catch (Exception e) {
outcome = "error";
msg = "Error reading file: " + e.getMessage();
}
if (outcome != "error") {
try {
// *** Do something with file that was opened
} catch (Exception e) {
outcome = "error";
msg = "Error reading file " + fileName + " - " + e.getMessage();
}
}
OtpErlangObject[] tupleContents = {toOtpAtom(outcome), toOtp(msg)};
return new OtpErlangTuple(tupleContents);
}
private static OtpErlangObject packageOutput(String cmd, OtpErlangObject payload) {
OtpErlangObject cmdAtom = toOtpAtom(cmd);
OtpErlangObject[] tupleContents = {cmdAtom, payload};
return new OtpErlangTuple(tupleContents);
}
private static int fromOtpInt(OtpErlangObject otpInt) {
try {
return ((OtpErlangLong)otpInt).intValue();
} catch (OtpErlangRangeException e) {
return -1;
}
}
private static OtpErlangObject toOtp(Object o) {
if (o == null) {
return new OtpErlangAtom("nil");
} else {
if (o instanceof Integer) { return toOtpInt((Integer)o);}
if (o instanceof String) { return toOtpString((String)o);}
if (o instanceof Date) { return toOtpDate((Date)o);}
if (o instanceof Double) { return toOtpDouble((Double)o);}
}
return new OtpErlangAtom("nil");
}
private static OtpErlangObject toOtpDouble(Double dbl) {
return new OtpErlangDouble(dbl);
}
private static OtpErlangString toOtpString(String str) {
return new OtpErlangString(str);
}
private static OtpErlangString toOtpDate(Date date) {
return toOtpString(dateFormatter.format(date));
}
private static OtpErlangLong toOtpInt(Integer integer) {
return new OtpErlangLong(integer);
}
private static OtpErlangAtom toOtpAtom(String str) {
return new OtpErlangAtom(str);
}
private static OtpErlangObject receivePayload() throws Exception {
byte[] msgLen = new byte[4];
int readByteCount = System.in.read(msgLen);
if (readByteCount != msgLen.length) {throw new Exception("Didn't get message length");}
ByteBuffer bb = ByteBuffer.wrap(msgLen);
int encodedLength = bb.getInt();
byte[] rawPayload = new byte[encodedLength];
readByteCount = System.in.read(rawPayload);
if (readByteCount != encodedLength) {throw new Exception("Payload didn't match message length");}
OtpInputStream otpInputStream = new OtpInputStream(rawPayload);
return otpInputStream.read_any();
}
}