LiveCode is single-threaded - well, mostly single-threaded: script execution happens on a single thread, but some parts, like socket communication, can be used with event callbacks. This means that when new data arrives, an event message is sent to a control and handled on the script execution thread; but the actual reading and writing can happen on other threads. This is a solid middle ground, as coders don't have to worry about data races and the other un-fun things that come with full-blown multi-threaded programming.
Now my earlier postrs on extending LiveCode with Java were based on a strategy of handing off some work to a Java process (via shell calls or process communication via pipes) and waiting for the result to come back. This is all nice and dandy if the work is done quickly, but what if the processing may take a long time to complete? One of the main reasons to employ multi-threading, after all, is to make sure we can update the user interface while some heavy processing continues in the background.
One approach could be to use the 'in time' variant of the 'read from process' command, and poll the other side for content, but I wanted to show a different approach, by wrapping our Java code in an ExecutorService. The basic idea: we can either 'call' a command immedialtely, or 'submit' it as a task to process in the background. Submitting a command returns a pending-result-id, which we can use to 'cancel' (if it hasn't started or finished yet), check on its 'status' (done, cancelled, pending) or even 'get' its result (waiting for the command to complete).
I know that's a lot of things in one paragraph, so let's put it in Java code:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
public class LiveCodeJavaExecutor {
private final static String EXIT_COMMAND = "exit";
private final static String CALL_COMMAND = "call";
private final static String SUBMIT_COMMAND = "submit";
private final static String CANCEL_COMMAND = "cancel";
private final static String STATUS_COMMAND = "status";
private final static String GET_COMMAND = "get";
private final static ExecutorService EXECUTOR_SERVICE;
private final static Map<Integer,Future<String>> PENDING_RESULTS;
private final static AtomicInteger NEXT_PENDING_ID;
static {
EXECUTOR_SERVICE = Executors.newCachedThreadPool();
NEXT_PENDING_ID = new AtomicInteger();
PENDING_RESULTS = new HashMap<Integer,Future<String>>();
}
/**
* @param args
*/
public static void main(String[] args) {
// Make sure to run in head-less mode
System.setProperty("java.awt.headless", "true");
String commandLine = ""; // the last received command line
System.out.println("LiveCodeJavaExecutor started. Type the command you wish to execute (type 'exit' to stop):");
final InputStreamReader isr = new InputStreamReader(System.in);
final BufferedReader bir = new BufferedReader(isr);
InfiniteLoop:
while(!EXIT_COMMAND.equalsIgnoreCase(commandLine)) {
try {
commandLine = bir.readLine();
} catch (IOException e) {
continue InfiniteLoop;
}
final String[] commandParts = commandLine.split("\t");
final String commandName = commandParts[0];
if (EXIT_COMMAND.equalsIgnoreCase(commandName)) {
System.out.println("LiveCodeJavaExecutor is shutting down");
EXECUTOR_SERVICE.shutdown();
System.out.println("LiveCodeJavaExecutor is exiting");
} else if (CALL_COMMAND.equalsIgnoreCase(commandName)) {
String result = "";
final CallableCommand callableCommand = new CallableCommand(commandParts);
try {
result = callableCommand.call();
} catch (Exception e) {
result = e.getMessage().trim();
}
System.out.println(result);
} else if (SUBMIT_COMMAND.equalsIgnoreCase(commandName)){
String result = "";
try {
final CallableCommand callableCommand = new CallableCommand(commandParts);
final Integer pendingResultId = Integer.valueOf(NEXT_PENDING_ID.incrementAndGet());
final FuturependingResult = EXECUTOR_SERVICE.submit(callableCommand);
PENDING_RESULTS.put(pendingResultId, pendingResult);
result = pendingResultId.toString();
} catch (RejectedExecutionException e) {
result = "error - could not submit command";
}
System.out.println(result);
} else if (CANCEL_COMMAND.equalsIgnoreCase(commandName)) {
String result = "";
try {
final Integer pendingResultId = Integer.valueOf(commandParts[1]);
final FuturependingResult = PENDING_RESULTS.get(pendingResultId);
if (pendingResult != null) {
result = String.valueOf(pendingResult.cancel(true));
} else {
result = "unknown pending result id: " + pendingResultId.toString();
}
} catch (ArrayIndexOutOfBoundsException e) {
result = "invalid parameters - expected: cancel<tab><pending-result-id>";
} catch (Exception e) {
result = e.getMessage().trim();
}
System.out.println(result);
} else if (STATUS_COMMAND.equalsIgnoreCase(commandName)) {
String result = "";
try {
final Integer pendingResultId = Integer.valueOf(commandParts[1]);
final FuturependingResult = PENDING_RESULTS.get(pendingResultId);
if (pendingResult != null) {
if (pendingResult.isCancelled()) {
result = "cancelled";
} else if (pendingResult.isDone()) {
result = "done";
} else {
result = "pending";
}
} else {
result = "unknown pending result id: " + pendingResultId.toString();
}
} catch (ArrayIndexOutOfBoundsException e) {
result = "invalid parameters - expected: status<tab><pending-result-id>";
} catch (Exception e) {
result = e.getMessage().trim();
}
System.out.println(result);
} else if (GET_COMMAND.equalsIgnoreCase(commandName)) {
String result = "";
try {
final Integer pendingResultId = Integer.valueOf(commandParts[1]);
final FuturependingResult = PENDING_RESULTS.get(pendingResultId);
if (pendingResult != null) {
result = pendingResult.get();
PENDING_RESULTS.remove(pendingResultId);
} else {
result = "unknown pending result id: " + pendingResultId.toString();
}
} catch (ArrayIndexOutOfBoundsException e) {
result = "invalid parameters - expected: get<tab><pending-result-id>";
} catch (Exception e) {
result = e.getMessage().trim();
}
System.out.println(result);
} else {
System.out.println("unrecognized command: " + commandLine);
}
System.out.println('.');
}
}
private static class CallableCommand implements Callable<String> {
private final String[] commandParts;
public CallableCommand(final String[] commandParts) {
super();
this.commandParts = commandParts;
}
public String call() throws Exception {
String result = null;
result = dispatchCommand(commandParts);
return result;
}
}
public static String dispatchCommand(String[] commandParts) throws Exception {
String commandName = commandParts[1];
Thread.sleep(10000);
return commandName;
}
}
Admittedly, the actual command execution part is a bit silly to say the least: sleeping for ten seconds and then just returning the command name is not exactly business logic. But you can take a look at some of my previous examples, to put in code for concatenating PDF files, validating XML data using XSD schemas, image processing or anything else that could take quite a bit of time to finish processing.
Turning our attention to the LiveCode side, we would have a stack script looking something like this:
local sProcess
on JExecutor_StartHelper
if sProcess is empty then
local tDefaultFolder, tStackFolder
put ImageIO_StackFolder() into tStackFolder
put the defaultFolder into tDefaultFolder
set the defaultFolder to tStackFolder
put "java LiveCodeJavaExecutor" into sProcess
open process sProcess for update
set the defaultFolder to tDefaultFolder
--> make sure we're speaking with the right helper
read from process sProcess for 1 line
if it begins with "LiveCodeJavaExecutor started." then
--> good to go
else
close process sProcess
put empty into sProcess
end if
end if
end JExecutor_StartHelper
on JExecutor_StopHelper
write ("exit") & return to process sProcess
close process sProcess
put empty into sProcess
end JExecutor_StopHelper
command JExecutor_CallCommand pCommand
write("call" & tab & pCommand) & return to process sProcess
read from process sprocess for 1 line
return line 1 of it
end JExecutor_CallCommand
command JExecutor_SubmitCommand pCommand
write("submit" & tab & pCommand) & return to process sProcess
read from process sprocess for 1 line
return line 1 of it
end JExecutor_SubmitCommand
command JExecutor_CancelRequest pRequestId
write("cancel" & tab & pRequestId) & return to process sProcess
read from process sprocess for 1 line
return line 1 of it
end JExecutor_CancelRequest
function JExecutor_StatusOfRequest pRequestId
write("status" & tab & pRequestId) & return to process sProcess
read from process sprocess for 1 line
return line 1 of it
end JExecutor_StatusOfRequest
function JExecutor_GetResultOfRequest pRequestId
write("get" & tab & pRequestId) & return to process sProcess
read from process sprocess for 1 line
return line 1 of it
end JExecutor_GetResultOfRequest
Now we have the commands and functions we need to start the separate Java process, 'call' commands, 'submit' commands, 'cancel' pending requests, checking the 'status' of pending requests, and fetching the result with a 'get'. Enough to control the lifecycle of such submitted commands a.k.a. pending requests.
If we're sure the command is just a small bit of work, we use the 'call' command and wait for the result to appear; but if it may take a long time to complete, we can 'submit' a command and use the returned 'pending request id' to track its progress - and while the Java side is churning away, we can update our LiveCode user interface with a lovely indeterminate progress display, polling every second or so until the 'status' returns "done" and then use 'get' to fetch the completed command output.
So perhaps the title was a little misleading, as I've only given you a skeleton. Hopefully it will inspire you to combine this technique with code that I posted previously, and actually use it in your own projects. Happy coding!