package greenfoot.vmcomm;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.IntBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.FileChannel.MapMode;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import bluej.pkgmgr.Project;
import bluej.utility.Debug;
import greenfoot.guifx.GreenfootStage;
import javafx.scene.input.KeyCode;
import threadchecker.OnThread;
import threadchecker.Tag;

import static greenfoot.vmcomm.Command.*;


| VMCommsMain is an abstraction for the inter-VM communications interface ("main VM" side) in |* Greenfoot. It encapsulates a temporary file and memory-mapped buffer. * * @author Davin McCall */ public class VMCommsMain implements Closeable{ // The server-debug VM protocol relies on locking three distinct areas of the file: | |// A - the server VM "put" area // B - the debug VM "put" area // C - a small region not used for data but to ensure synchronisation // // Server Debug // (holds A, C) (holds B) // [command issued/want update] | |// [updates data while B is held] | |// -> release A | |// -> acquire A (commands are available...) | |// -> release B (ensures server can get data) | |// -> acquire B | |// (read image) | |// (read commands) | |// -> release C | |// -> acquire C (Server has acquired B) | |// -> release A | |// -> acquire A | |// -> release B | |// -> acquire B (Server has A and C) | |// -> release C | |// -> acquire C | |// | |// The acquisition order is B-->A, A-->C, and C-->B. This ensures that there can never | |// be deadlock. No process holds all three locks at once and each process always holds at | |// least one lock. | |public static final int DEFAULT_MAPPED_SIZE = 20_000_000; | |public static final int USER_AREA_OFFSET = 0x1000; // offset in 4-byte chunks; 16KB worth. | |public static final int USER_AREA_OFFSET_BYTES = USER_AREA_OFFSET * 4; | |public static final int SERVER_AREA_OFFSET_BYTES = 4; | |public static final int SERVER_AREA_SIZE_BYTES = USER_AREA_OFFSET_BYTES - SERVER_AREA_OFFSET_BYTES; | |public static final int SYNC_AREA_OFFSET_BYTES = 0; | |public static final int SYNC_AREA_SIZE_BYTES = 4; | |private final int fileSize; | |private File shmFile; | |private FileChannel fc; | |private MappedByteBuffer sharedMemoryByte; | |private IntBuffer sharedMemory; | |private FileLock putLock; | |private FileLock syncLock; | |private int lastSeq = 0; | |private final List<Command> pendingCommands = new ArrayList<>(); | |private int setSpeedCommandCount = 0; | |private int lastPaintSeq = -1; | |private int lastConsumedImg = -1; | |private boolean checkingIO = false; | |private boolean haveUpdatedImage = false; | |private boolean haveUpdatedErrorCount = false; | |private long lastExecStartTime; | |private int updatedSimulationSpeed = -1; | |private boolean worldChanged = false; | |private boolean worldPresentAfterChange = false; | |private int[] promptCodepoints = null; | |/** | Because the ask request is sent as a continuous status rather than | a one-off event that we explicitly acknowledge, we keep track of the | last answer we sent so that we know if an ask request is newer than | the last answer or not. That way we don't accidentally ask again | after the answer has been sent. | private int lastAnswer = -1; private int previousStoppedWithErrorCount; private int prevWorldCounter = 0; private int worldCellSize; private final Thread ioThread; private boolean delayLoop; private int askId = -1;
| Constructor for VMCommsMain. Creates a temporary file and maps it into memory. | | @throws IOException if the file could not be created or mapped. | @SuppressWarnings("resource") public VMCommsMain(Project project) throws IOException { fileSize = Integer.parseInt(project.getUnnamedPackage().getLastSavedProperties().getProperty("shm.size", Integer.toString(DEFAULT_MAPPED_SIZE))); shmFile = File.createTempFile("greenfoot", "shm"); shmFile.deleteOnExit(); fc = new RandomAccessFile(shmFile, "rw").getChannel(); sharedMemoryByte = fc.map(MapMode.READ_WRITE, 0, fileSize); sharedMemory = sharedMemoryByte.asIntBuffer(); putLock = fc.lock(SERVER_AREA_OFFSET_BYTES, SERVER_AREA_SIZE_BYTES, false); syncLock = fc.lock(SYNC_AREA_OFFSET_BYTES, SYNC_AREA_SIZE_BYTES, false); ioThread = new Thread() { @OnThread(Tag.Worker) public void run() { while (checkIO()) { } } }; ioThread.start(); }
| Close the communications channel, and release resources. | @Override @OnThread(value = Tag.FXPlatform, ignoreParent = true) public void close() { try { fc.close(); } catch (IOException ioe) { } shmFile = null; fc = null; sharedMemoryByte = null; sharedMemory = null; }
| Get the file channel for this communication channel. | public FileChannel getChannel() { return fc; }
| Get the shared memory buffer for this communication channel. | public MappedByteBuffer getSharedBuffer() { return sharedMemoryByte; }
| Get the name of the file used for this communication channel. | public File getSharedFile() { return shmFile; }
| Get the size of the file used for this communication channel. | public int getSharedFileSize() { return fileSize; }
| Write commands into the shared memory buffer. | private synchronized void writeCommands(List<Command> pendingCommands) { int pendingCountPos = sharedMemory.position(); sharedMemory.put(pendingCommands.size()); int numIssued = 0; for (Command pendingCommand : pendingCommands) { int totalLength = pendingCommand.extraInfo.length + 2; if (sharedMemory.position() + totalLength > USER_AREA_OFFSET) { sharedMemory.put(pendingCountPos, numIssued); if (numIssued == 0) { throw new RuntimeException("Single command exceeds buffer size"); } return; } sharedMemory.put(pendingCommand.commandSequence); sharedMemory.put(pendingCommand.extraInfo.length + 1); sharedMemory.put(pendingCommand.commandType); sharedMemory.put(pendingCommand.extraInfo); numIssued++; } }
| Get the world cell size, if it is known. | | @return The cell size in pixels, or 0 if there is no world. | public int getWorldCellSize() { return worldCellSize; }
| Check for input / send output, and apply received data to the stage. | @OnThread(Tag.FXPlatform) public synchronized void checkIO(GreenfootStage stage) { if (checkingIO) { return; } checkingIO = true; boolean shouldDraw = !worldChanged || worldPresentAfterChange; if (worldChanged) { stage.worldChanged(worldPresentAfterChange); worldChanged = false; } if (haveUpdatedImage && shouldDraw) { IntBuffer copy = sharedMemory.asReadOnlyBuffer(); copy.position(USER_AREA_OFFSET + 2); int width = copy.get(); int height = copy.get(); stage.receivedWorldImage(width, height, copy); haveUpdatedImage = false; lastConsumedImg = lastPaintSeq; } if (haveUpdatedErrorCount) { stage.bringTerminalToFront(); haveUpdatedErrorCount = false; } if (updatedSimulationSpeed != -1) { stage.notifySimulationSpeed(updatedSimulationSpeed); updatedSimulationSpeed = -1; } if (promptCodepoints != null && askId > lastAnswer) { stage.receivedAsk(promptCodepoints); promptCodepoints = null; } stage.setLastUserExecutionStartTime(lastExecStartTime, delayLoop); checkingIO = false; notifyAll(); }
| Check for input / send output | @return true If we should continue processing, false if not. | @OnThread(Tag.Worker) private boolean checkIO() { FileChannel sharedMemoryLock = this.fc; sharedMemory.position(1); sharedMemory.put(-lastSeq); sharedMemory.put(lastConsumedImg); writeCommands(pendingCommands); FileLock fileLock = null; try { putLock.release(); fileLock = sharedMemoryLock.lock(USER_AREA_OFFSET_BYTES, fileSize - USER_AREA_OFFSET_BYTES, false); syncLock.release(); int seq = sharedMemory.get(USER_AREA_OFFSET); if (seq > lastSeq) { lastSeq = seq; synchronized (this) { sharedMemory.position(USER_AREA_OFFSET + 1); int paintSeq = sharedMemory.get(); int width = sharedMemory.get(); int height = sharedMemory.get(); if (width != 0 && height != 0 && paintSeq != lastPaintSeq) { lastPaintSeq = paintSeq; haveUpdatedImage = true; } sharedMemory.position(sharedMemory.position() + width * height); int lastAckCommand = sharedMemory.get(); if (lastAckCommand != -1) { for (Iterator<Command> iterator = pendingCommands.iterator(); iterator.hasNext(); ) { Command pendingCommand = iterator.next(); if (pendingCommand.commandSequence <= lastAckCommand) { if (pendingCommand.commandType == COMMAND_SET_SPEED) { setSpeedCommandCount = setSpeedCommandCount - 1; } iterator.remove(); } } } int latestStoppedWithErrorCount = sharedMemory.get(); if (latestStoppedWithErrorCount != previousStoppedWithErrorCount) { previousStoppedWithErrorCount = latestStoppedWithErrorCount; haveUpdatedErrorCount = true; } int highTime = sharedMemory.get(); int lowTime = sharedMemory.get(); lastExecStartTime = (((long)highTime) << 32) | ((long)lowTime & 0xFFFFFFFFL); int simSpeed = sharedMemory.get(); if (setSpeedCommandCount == 0) { updatedSimulationSpeed = simSpeed; } int worldCounter = sharedMemory.get(); if (worldCounter != prevWorldCounter) { worldChanged = true; worldPresentAfterChange = worldCounter != 0; prevWorldCounter = worldCounter; } worldCellSize = sharedMemory.get(); int askId = sharedMemory.get(); if (askId > 0 && askId > lastAnswer) { this.askId = askId; int askLength = sharedMemory.get(); promptCodepoints = new int[askLength]; sharedMemory.get(promptCodepoints); } int delayLoopStatus = sharedMemory.get(); if (delayLoopStatus == 1) { delayLoop = true; } else { delayLoop = false; } } } } catch (IOException ex) { Debug.reportError(ex); } catch (IllegalArgumentException ex) { } finally { try { putLock = fc.lock(SERVER_AREA_OFFSET_BYTES, SERVER_AREA_SIZE_BYTES, false); if (fileLock != null) { fileLock.release(); } syncLock = fc.lock(SYNC_AREA_OFFSET_BYTES, SYNC_AREA_SIZE_BYTES, false); } catch (IOException ex) { Debug.reportError(ex); } } synchronized (this) { try { wait(); } catch (InterruptedException ie) { } return shmFile != null; } }
| Send an "instantiate world" command. |*/ public synchronized void instantiateWorld(String className) { pendingCommands.add(new Command(COMMAND_INSTANTIATE_WORLD, className.codePoints().toArray())); } | |/** | Send a "discard world" command. |*/ public synchronized void discardWorld() { pendingCommands.add(new Command(COMMAND_DISCARD_WORLD)); } /** * Send an answer (after receving an "ask" request). */ public synchronized void sendAnswer(String answer) { Command answerCommand = new Command(COMMAND_ANSWERED, answer.codePoints().toArray()); pendingCommands.add(answerCommand); | |// Remember that we've now answered: | |lastAnswer = answerCommand.commandSequence; | |} | |/** | Send an updated property value. | @param key The property name | @param value The property value | public synchronized void sendProperty(String key, String value) { int[] keyCodepoints = key.codePoints().toArray(); int[] valueCodepoints = value == null ? new int[0] : value.codePoints().toArray(); int[] combined = new int[1 + keyCodepoints.length + 1 + valueCodepoints.length]; combined[0] = keyCodepoints.length; System.arraycopy(keyCodepoints, 0, combined, 1, keyCodepoints.length); combined[1 + keyCodepoints.length] = value == null ? -1 : valueCodepoints.length; System.arraycopy(valueCodepoints, 0, combined, 2 + keyCodepoints.length, valueCodepoints.length); pendingCommands.add(new Command(COMMAND_PROPERTY_CHANGED, combined)); }
| Send an "act" command. |*/ public synchronized void act() { pendingCommands.add(new Command(COMMAND_ACT)); } /** * Send a "run simulation" command. */ public synchronized void runSimulation() { pendingCommands.add(new Command(COMMAND_RUN)); } /** * Send a "pause simulation" command. */ public synchronized void pauseSimulation() { pendingCommands.add(new Command(COMMAND_PAUSE)); } /** * Continue a mouse drag, identified by the given id. Note that drags are initiated by | a pick request executed via a separate mechanism | | @see greenfoot.core.PickActorHelper | public synchronized void continueDrag(int dragId, int x, int y) { pendingCommands.add(new Command(COMMAND_CONTINUE_DRAG, dragId, x, y)); }
| End a drag, identified by the given id. | public synchronized void endDrag(int dragId) { pendingCommands.add(new Command(COMMAND_END_DRAG, dragId)); }
| Send a key event. | | @param eventType The event type | @param keyCode The key code, from KeyEvent.getCode() | @param keyText The key text, from KeyEvent.getText() | public synchronized void sendKeyEvent(int eventType, KeyCode keyCode, String keyText) { int[] textCodePoints = keyText.codePoints().toArray(); int[] data = new int[textCodePoints.length + 1]; data[0] = keyCode.ordinal(); System.arraycopy(textCodePoints, 0, data, 1, textCodePoints.length); pendingCommands.add(new Command(eventType, data)); }
| Send a mouse event. | | @param eventType The event type | @param x The mouse x-coordinate (in pixels) | @param y The mouse y-coordinate (in pixels) | @param button The button pressed (for button events) | @param clickCount The click count (for click events) | public synchronized void sendMouseEvent(int eventType, int x, int y, int button, int clickCount) { pendingCommands.add(new Command(eventType, x, y, button, clickCount)); }
| Set the simulation speed to a specified value | | @param speed The speed value | public synchronized void setSimulationSpeed(int speed) { pendingCommands.add(new Command(COMMAND_SET_SPEED, speed)); setSpeedCommandCount = setSpeedCommandCount + 1; }
| The debug VM has terminated. We re-use the same shared memory file, | so we must reset our state ready for a new debug VM. | public void vmTerminated() { lastSeq = 0; pendingCommands.clear(); setSpeedCommandCount = 0; lastAnswer = -1; previousStoppedWithErrorCount = 0; prevWorldCounter = 0; sharedMemoryByte.position(0); sharedMemoryByte.put(new byte[fileSize], 0, fileSize); }
| The world display has gained or lost focus | @param focused true if the world display gained focus, false if it lost focus | public synchronized void worldFocusChanged(boolean focused) { pendingCommands.add(new Command(focused ? COMMAND_WORLD_FOCUS_GAINED : COMMAND_WORLD_FOCUS_LOST)); } }

.   VMCommsMain
.   run
.   close
.   getChannel
.   getSharedBuffer
.   getSharedFile
.   getSharedFileSize
.   writeCommands
.   getWorldCellSize
.   checkIO
.   checkIO
.   sendProperty
.   continueDrag
.   endDrag
.   sendKeyEvent
.   sendMouseEvent
.   setSimulationSpeed
.   vmTerminated
.   worldFocusChanged




573 neLoCode + 97 LoComm