/* * License: source-license.txt * If this code is used independently, copy the license here. */ package wombat.scheme; import java.awt.BorderLayout; import java.io.*; import java.net.URISyntaxException; import java.util.*; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import javax.swing.*; import wombat.scheme.util.InteropAPI; import wombat.util.OS; /** * Class to wrap Petite bindings. */ public class Petite { static final boolean DEBUG_DISPLAY = false; static final boolean DEBUG_INTEROP = false; static final boolean DEBUG_LISTENER = false; static int DEBUG_LISTENER_COUNT = 0; // Keep track of which state the Petite bindings are in. private enum PetiteState { Startup, // getting everything running Interop, // communicating between Scheme and Java Command, // reading a command from the user } PetiteState State = PetiteState.Startup; boolean HalfPrompt = false; // Remember if this is the first line responding to a new command. boolean FirstResponse = false; // Choose the different prompt characters. static final char Prompt1 = '|'; static final char Prompt2 = '`'; static final char Interop = '!'; // Listen to events from Petite machine. List<PetiteListener> Listeners = new ArrayList<PetiteListener>(); // Communicate to and from the Petite process. Writer ToPetite; Reader FromPetite; Process NativeProcess; Thread FromPetiteThread; // Buffers for commands from the Petite process. StringBuffer Buffer = new StringBuffer(); StringBuffer InteropBuffer = new StringBuffer(); // The root is either this directory or a nested 'lib' directory. static File[] searchDirs; static { try { searchDirs = new File[] { new File("").getCanonicalFile(), new File(new File("").getCanonicalFile(), "lib").getCanonicalFile(), new File(Petite.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath()).getCanonicalFile(), new File(new File(Petite.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath()),"lib").getCanonicalFile(), new File(Petite.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath()).getParentFile().getCanonicalFile(), new File(new File(Petite.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath()).getParentFile(), "lib").getCanonicalFile(), }; } catch (IOException ex) { } catch (URISyntaxException e) { } }; /** * Create a new Petite binding. * * @throws IOException If we fail to access the Petite process. * @throws URISyntaxException If we have problems getting the path from a JAR file. */ public Petite() throws IOException, URISyntaxException { // Add a debug listener if requested. if (DEBUG_LISTENER) { final int myCount = DEBUG_LISTENER_COUNT++; Listeners.add(new PetiteListener() { @Override public void onStop() { System.out.println("** (" + myCount + ") onStop"); } @Override public void onReady() { System.out.println("** (" + myCount + ") onReady"); } @Override public void onOutput(String output) { System.out.print("** (" + myCount + ") onOutput:\n** | "); System.out.println(output.replace("\n", "\n** | ")); } @Override public void onError(Exception ex) { System.err.println("** (" + myCount + ") onError:"); ex.printStackTrace(); } @Override public void onReset() { System.out.println("** (" + myCount + ") onReset"); } }); } // Find a Petite installation. File pdir = findPetiteDirectory(); // If that fails, look for a version that's still archived. if (pdir == null) { findPetiteArchive(); pdir = findPetiteDirectory(); } // If *that* fails, we're screwed. if (pdir == null) throw new IOException("Unable to find Petite directory."); // Choose the binary and boot file. String petiteBinary = null; String petiteBoot = null; if (OS.IsWindows) { petiteBinary = "petite.exe"; petiteBoot = "petite.boot"; } else { if (OS.Is64Bit) { petiteBinary = "petite64"; petiteBoot = "petite64.boot"; } else { petiteBinary = "petite"; petiteBoot = "petite.boot"; } } // Debugging information. System.out.println("Directory: " + pdir.getCanonicalPath() + ", Binary: " + petiteBinary + ", Boot: " + petiteBoot); // Create the process builder. ProcessBuilder pb = new ProcessBuilder( new File(pdir, petiteBinary).getCanonicalPath(), "-b", new File(pdir, petiteBoot).getCanonicalPath() ); pb.directory(pdir.getParentFile().getParentFile()); pb.redirectErrorStream(true); // Start the process. NativeProcess = pb.start(); // Set up the print writer. ToPetite = new PrintWriter(NativeProcess.getOutputStream()); FromPetite = new InputStreamReader(NativeProcess.getInputStream()); // Immediately send the command to reset the prompt to set everything up the first time. reset(); // Create a listener thread. FromPetiteThread = new Thread("Read from Petite") { @SuppressWarnings("unused") public void run() { final JTextArea text = new JTextArea(); if (DEBUG_DISPLAY) { final JFrame frame = new JFrame(); frame.setSize(400, 400); frame.setLayout(new BorderLayout()); frame.add(text); frame.setVisible(true); } char c; try { while (true) { // Read from the buffer. c = (char) FromPetite.read(); // Display debug. if (DEBUG_DISPLAY && c != (char) 65535) { text.setText(text.getText() + c); } // Ignore end of file characters. if (c == (char) 65535) { } // Potential start of a prompt. else if (c == Prompt1) { HalfPrompt = true; } // First prompt after startup. else if (HalfPrompt && c == Prompt2 && State == PetiteState.Startup) { Buffer.delete(0, Buffer.length()); HalfPrompt = false; State = PetiteState.Command; synchronized (Listeners) { for (PetiteListener pl : Listeners) pl.onReady(); } } // Prompt while running, means the process is ready for more. else if (HalfPrompt && c == Prompt2 && State == PetiteState.Command) { String output = Buffer.toString(); Buffer.delete(0, Buffer.length()); HalfPrompt = false; State = PetiteState.Command; synchronized (Listeners) { for (PetiteListener pl : Listeners) { if (FirstResponse && output.startsWith(" ")) { pl.onOutput(output.substring(1)); } else { pl.onOutput(output); } pl.onReady(); } FirstResponse = false; } } // Interop mode. else if (HalfPrompt && c == Interop) { if (State == PetiteState.Interop) { String[] parts = InteropBuffer.toString().split(" ", 2); String key = parts[0]; String val = (parts.length > 1 ? parts[1] : null); if (DEBUG_INTEROP) System.out.println("calling interop: " + key + " with " + val); // debug String result = InteropAPI.interop(key, val); if (DEBUG_INTEROP) System.out.println("interop returns: " + (result.length() > 10 ? result .subSequence(0, 10) + "..." : result)); // debug if (result != null) { ToPetite.write(result + " "); ToPetite.flush(); } if (DEBUG_INTEROP) System.out.println("exiting interop"); InteropBuffer.delete(0, InteropBuffer.length()); HalfPrompt = false; State = PetiteState.Command; } else { if (DEBUG_INTEROP) System.out.println("entering interop"); // debug HalfPrompt = false; State = PetiteState.Interop; } } // Thought it was a prompt, but we were wrong. // Remember to store the first half of the prompt. else if (HalfPrompt) { HalfPrompt = false; if (State == PetiteState.Interop) { InteropBuffer.append(Prompt1); InteropBuffer.append(c); } else { Buffer.append(Prompt1); Buffer.append(c); } } // Go ahead and force output on newlines (if not in interop) else if (State == PetiteState.Command && c == '\n') { Buffer.append(c); String output = Buffer.toString(); Buffer.delete(0, Buffer.length()); synchronized (Listeners) { for (PetiteListener pl : Listeners) { if (FirstResponse && output.startsWith(" ")) pl.onOutput(output.substring(1)); else pl.onOutput(output); } FirstResponse = false; } } // Normal case, no new characters. else { if (State == PetiteState.Interop) { InteropBuffer.append(c); } else { Buffer.append(c); } } } } catch (IOException e) { synchronized (Listeners) { for (PetiteListener pl : Listeners) { pl.onError(e); } } } } }; FromPetiteThread.setDaemon(true); FromPetiteThread.start(); // Show down the thread when done. Runtime.getRuntime().addShutdownHook(new Thread("Petite Shutdown") { public void run() { NativeProcess.destroy(); } }); } /** * Find the Petite directory.. * @return The directory or null if one could not be found. */ private File findPetiteDirectory() { // Find the correct Petite directory. File pdir = null; for (File dir : searchDirs) { if (dir != null && dir.exists() && dir.isDirectory()) { for (String path : dir.list()) { if (path.startsWith("petite") && path.endsWith(OS.IsWindows ? "win" : OS.IsOSX ? "osx" : OS.IsLinux ? "linux" : "unknown")) { pdir = new File(dir, path); break; } } } if (pdir != null) break; } return pdir; } /** * Find and unzip the proper Petite archive if it hasn't already been. */ private void findPetiteArchive() { try { for (File dir : searchDirs) { if (dir != null && dir.exists() && dir.isDirectory()) { for (String path : dir.list()) { if (path.startsWith("petite") && path.endsWith((OS.IsWindows ? "win" : OS.IsOSX ? "osx" : OS.IsLinux ? "linux" : "unknown") + ".zip")) { ZipFile zip = new ZipFile(new File(dir, path)); @SuppressWarnings("unchecked") Enumeration<ZipEntry> entries = (Enumeration<ZipEntry>) zip.entries(); while (entries.hasMoreElements()) { ZipEntry entry = entries.nextElement(); if (entry.isDirectory()) { new File(dir, entry.getName()).getCanonicalFile().mkdirs(); } else { new File(dir, entry.getName()).getCanonicalFile().getParentFile().mkdirs(); File targetFile = new File(dir, entry.getName()); InputStream zipin = zip.getInputStream(entry); OutputStream zipout = new BufferedOutputStream(new FileOutputStream(targetFile)); byte[] buffer = new byte[1024]; int len; while((len = zipin.read(buffer)) >= 0) zipout.write(buffer, 0, len); zipin.close(); zipout.close(); if (targetFile.getName().toLowerCase().startsWith("petite")) { targetFile.setExecutable(true); } } } zip.close(); return; } } } } } catch(IOException ex) { System.err.println("Unable to open petite archive: " + ex.getMessage()); } } /** * Reset Petite's environment. */ public void reset() { // Actually clear the environment sendCommand("(interaction-environment (copy-environment (scheme-environment) #t))"); // So that gensyms look at least semi-sane (it's not like anyone will need them) sendCommand("(print-gensym #f)"); // To test infinite loops sendCommand("(define (omega) ((lambda (x) (x x)) (lambda (x) (x x))))"); // Reset the library directories. sendCommand("(library-directories '((\"lib\" . \"lib\") (\".\" . \".\") (\"..\" . \"..\") (\"dist\" . \"dist\") (\"dist/lib\" . \"dist/lib\")))"); // Fix error message that give define/lambda names sendCommand("(import (wombat define))"); // Make sure that the prompt is set as we want it // Set this last so all of the startup commands have time to run sendCommand("(waiter-prompt-string \"|`\")"); // Tell the listeners what we did. synchronized (Listeners) { for (PetiteListener pl : Listeners) pl.onReset(); } } /** * Stop the running process. * * @throws IOException If we cannot connect. * @throws URISyntaxException Botched file from JAR. */ public void stop() { // Shut down connection. NativeProcess.destroy(); try { NativeProcess.waitFor(); } catch (InterruptedException e) { } // Tell any listeners that we got it. synchronized (Listeners) { for (PetiteListener pl : Listeners) pl.onStop(); } } /** * Listen for state changes in the Petite binding. * @param pl A listener */ public void addPetiteListener(final PetiteListener pl) { Listeners.add(pl); } /** * Stop a certain Petite listener. * @param pl The listener that we are watching. */ public void removePetiteListener(final PetiteListener pl) { SwingUtilities.invokeLater(new Runnable() { public void run() { Listeners.remove(pl); } }); } /** * Send a command to the Petite process. * * @param cmd The command to send. */ public void sendCommand(String cmd) { try { // Swap out lambda character for string cmd = cmd.replace("\u03BB", "lambda"); // Note that this is the first process. FirstResponse = true; // Send it, make sure there's a newline. // Use flush to force it to actually run. ToPetite.write(cmd); if (!cmd.endsWith("\n")) ToPetite.write("\n"); ToPetite.flush(); } catch (Exception e) { e.printStackTrace(); } } }