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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.