modifications to bsh.util.JConsole for swing threading and to support RMI
"White, John K" <JohnWhite-1VmriVQ4F2F8UrSeD/[email protected]>
| Newsgroups | gmane.comp.java.beanshell.devel |
|---|---|
| Message-ID | <2B5685BB89170947A53B9EDD0CA6276CEA6917@sdomsg00.corp.fairisaac.com> |
hi beanshell, i have been investigating using beanshell in an EJB environment. i do not use the built-in bsh server classes. in our setup, the interpreter exists in the server and the console implementation is remoted (along with remotable i/o streams) from the client to the server. however, i ran into problems when i tried to use bsh.util.JConsole in this system. it would sometimes cause swing to lock up, or display strange error messages like "Console pipe broken...". attached is a modified version of JConsole that alleviates these problems while not changing behavior in a significant way. i've been testing this with bsh 2.0b1 for a few weeks now and it seems stable. below is a lengthy description of the problems and solutions as i implemented them. i am not an expert on EJB, swing, or beanshell, so any corrections are appreciated :) (note: i originally made the changes to bsh 1.3, then merged those changes with the JConsole from bsh 2.0b1.) so, use it if you need it, if you don't... and if i understand the LGPL correctly, we can include the modified JConsole source in our distribution and also make it available on request to anyone to satisfy the license, right? anyway, here is what i hope a nice explanation of the whole thing: There are several related issues here. The first has to do with the fact that we remote the console over RMI to the server. The second is the way that java.io.PipedInputStream and java.io.PipedOutputStream operate. The third has to do with how the bsh.util.JConsole processes key events. The console exists locally to the client. The interpreter exists locally to the server. When the console is created, it realizes its i/o streams with the java.io pipes. After the console is created, we wrap it in a RemoteConsole and send it to the server, which wraps it in a RemoteConsoleWrapper so the server's interpreter sees it only as a Console. The server's interpreter then hooks up its i/o to the console's i/o. (The streams are also remoted, but that is beside the point.) Now, the interpreter starts reading data from the console's output (where user keystrokes go) and the console starts reading input from the interpreter's output (where text for the user goes). The first problem originates in the implementation of java.io.PipedInputStream. Every time a read() is invoked on the pipe, the pipe stores the value of Thread.currentThread(). Because this particular stream is actually an RMI object, the current thread is an ephemeral RMI thread--in other words, it's going to disappear shortly after the RMI method invocation is finished. That would not be a problem, were it not for one other feature: whenever PipedOutputStream.write() is called, it passes the data onto the PipedInputStream's circular buffer by means of PipedInputStream.receive(int b). Before PipedInputStream accepts the data, it first checks if the stored reference to the last thread which called read() is still alive. In the case of an RMI method invocation, the thread could very well be dead if enough time has passed between successive read() invocations. If the thread is dead, an IOException is thrown in receive() and the console dies with a "Cannot output" error message. So, here is what I did. I overrode PipedInputStream.receive() in the console's input to catch the IOException and eat it. That's not perfect, because there are several other reasons that IOException can be thrown in receive(), but I think we can ignore them, as they all manifest in other ways as well. The first reason is if the pipe is not connected. Since the first thing the interpreter does is connect to the pipe, and you can't disconnect pipes, I think we can safely ignore that one. The other reason is if the exception is the result of the interpreter's thread dying. But in that case, the IOException will manifest itself in a different way--by the console's input receiving an IOException the next time it tries to read the interpreter's output, which happens very regularly. Making this change got rid of the "Cannot output" problem. The second problem is that the java.io pipes block when the amount of data added to the fifo exceeds the PipedInputStream's constant internal buffer size. This causes a problem for the gui console because key events are processed on the AWT EventQueue thread. Since key events must be processed in serial in the order in which they were entered, performing the processing on the AWT thread makes sense. However, that causes a problem if the fifo buffer fills up. In that case, the AWT thread blocks until buffer space is available, which means the gui freezes until that time as well. Normally this is not a problem; however if the user enters a long-running foreground command, eg 'calcOneZillionDigitsOfPi();', and then types more commands, the gui will freeze until calcOneZillionDigitsOfPi is finished running and the interpreter has a chance to read more console output. So, rather than write data to the pipe using the AWT thread, I changed JConsole to instead add data to a queue monitored by an internal thread. When a new piece of data is added to the queue, the thread is woken up and it handles the duty of writing data to the output pipe. If it happens to block in the process, that's ok, because it will simply pick up again where it left off once space is available. In the meantime, more input can be added to the queue. The AWT thread does not block in any case, and the queue ensures the input is still processed in the correct order. j.
JConsole.java
(application/octet-stream, 26.9 KB) - not displayed