langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/jdi/JDIExecutionControl.java
changeset 38535 4a25025e0b0d
parent 37751 77e7bb904a13
child 38608 691b607bbcd6
equal deleted inserted replaced
38534:425b30506f80 38535:4a25025e0b0d
       
     1 /*
       
     2  * Copyright (c) 2014, 2015, Oracle and/or its affiliates. All rights reserved.
       
     3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
       
     4  *
       
     5  * This code is free software; you can redistribute it and/or modify it
       
     6  * under the terms of the GNU General Public License version 2 only, as
       
     7  * published by the Free Software Foundation.  Oracle designates this
       
     8  * particular file as subject to the "Classpath" exception as provided
       
     9  * by Oracle in the LICENSE file that accompanied this code.
       
    10  *
       
    11  * This code is distributed in the hope that it will be useful, but WITHOUT
       
    12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
       
    13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
       
    14  * version 2 for more details (a copy is included in the LICENSE file that
       
    15  * accompanied this code).
       
    16  *
       
    17  * You should have received a copy of the GNU General Public License version
       
    18  * 2 along with this work; if not, write to the Free Software Foundation,
       
    19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
       
    20  *
       
    21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
       
    22  * or visit www.oracle.com if you need additional information or have any
       
    23  * questions.
       
    24  */
       
    25 
       
    26 package jdk.internal.jshell.jdi;
       
    27 
       
    28 import static jdk.internal.jshell.remote.RemoteCodes.*;
       
    29 import java.io.DataInputStream;
       
    30 import java.io.InputStream;
       
    31 import java.io.IOException;
       
    32 import java.io.ObjectInputStream;
       
    33 import java.io.ObjectOutputStream;
       
    34 import java.io.PrintStream;
       
    35 import java.net.ServerSocket;
       
    36 import java.net.Socket;
       
    37 import com.sun.jdi.*;
       
    38 import java.io.EOFException;
       
    39 import java.util.Arrays;
       
    40 import java.util.Collection;
       
    41 import java.util.HashMap;
       
    42 import java.util.List;
       
    43 import java.util.Map;
       
    44 import static java.util.stream.Collectors.toList;
       
    45 import jdk.jshell.JShellException;
       
    46 import jdk.jshell.spi.ExecutionControl;
       
    47 import jdk.jshell.spi.ExecutionEnv;
       
    48 import jdk.internal.jshell.jdi.ClassTracker.ClassInfo;
       
    49 import static java.util.stream.Collectors.toMap;
       
    50 import jdk.internal.jshell.debug.InternalDebugControl;
       
    51 import static jdk.internal.jshell.debug.InternalDebugControl.DBG_GEN;
       
    52 
       
    53 /**
       
    54  * Controls the remote execution environment.
       
    55  * Interfaces to the JShell-core by implementing ExecutionControl SPI.
       
    56  * Interfaces to RemoteAgent over a socket and via JDI.
       
    57  * Launches a remote process.
       
    58  */
       
    59 public class JDIExecutionControl implements ExecutionControl {
       
    60 
       
    61     ExecutionEnv execEnv;
       
    62     JDIEnv jdiEnv;
       
    63     private ClassTracker tracker;
       
    64     private JDIEventHandler handler;
       
    65     private Socket socket;
       
    66     private ObjectInputStream remoteIn;
       
    67     private ObjectOutputStream remoteOut;
       
    68     private String remoteVMOptions;
       
    69 
       
    70     /**
       
    71      * Initializes the launching JDI execution engine. Initialize JDI and use it
       
    72      * to launch the remote JVM. Set-up control and result communications socket
       
    73      * to the remote execution environment. This socket also transports the
       
    74      * input/output channels.
       
    75      *
       
    76      * @param execEnv the execution environment provided by the JShell-core
       
    77      * @throws IOException
       
    78      */
       
    79     @Override
       
    80     public void start(ExecutionEnv execEnv) throws IOException {
       
    81         this.execEnv = execEnv;
       
    82         this.jdiEnv = new JDIEnv(this);
       
    83         this.tracker = new ClassTracker(jdiEnv);
       
    84         StringBuilder sb = new StringBuilder();
       
    85         execEnv.extraRemoteVMOptions().stream()
       
    86                 .forEach(s -> {
       
    87                     sb.append(" ");
       
    88                     sb.append(s);
       
    89                 });
       
    90         this.remoteVMOptions = sb.toString();
       
    91         try (ServerSocket listener = new ServerSocket(0)) {
       
    92             // timeout after 60 seconds
       
    93             listener.setSoTimeout(60000);
       
    94             int port = listener.getLocalPort();
       
    95             jdiGo(port);
       
    96             this.socket = listener.accept();
       
    97             // out before in -- match remote creation so we don't hang
       
    98             this.remoteOut = new ObjectOutputStream(socket.getOutputStream());
       
    99             PipeInputStream commandIn = new PipeInputStream();
       
   100             new DemultiplexInput(socket.getInputStream(), commandIn, execEnv.userOut(), execEnv.userErr()).start();
       
   101             this.remoteIn = new ObjectInputStream(commandIn);
       
   102         }
       
   103     }
       
   104 
       
   105     /**
       
   106      * Closes the execution engine. Send an exit command to the remote agent.
       
   107      * Shuts down the JDI connection. Should this close the socket?
       
   108      */
       
   109     @Override
       
   110     public void close() {
       
   111         try {
       
   112             if (remoteOut != null) {
       
   113                 remoteOut.writeInt(CMD_EXIT);
       
   114                 remoteOut.flush();
       
   115             }
       
   116             JDIConnection c = jdiEnv.connection();
       
   117             if (c != null) {
       
   118                 c.disposeVM();
       
   119             }
       
   120         } catch (IOException ex) {
       
   121             debug(DBG_GEN, "Exception on JDI exit: %s\n", ex);
       
   122         }
       
   123     }
       
   124 
       
   125     /**
       
   126      * Loads the list of classes specified. Sends a load command to the remote
       
   127      * agent with pairs of classname/bytes.
       
   128      *
       
   129      * @param classes the names of the wrapper classes to loaded
       
   130      * @return true if all classes loaded successfully
       
   131      */
       
   132     @Override
       
   133     public boolean load(Collection<String> classes) {
       
   134         try {
       
   135             // Create corresponding ClassInfo instances to track the classes.
       
   136             // Each ClassInfo has the current class bytes associated with it.
       
   137             List<ClassInfo> infos = withBytes(classes);
       
   138             // Send a load command to the remote agent.
       
   139             remoteOut.writeInt(CMD_LOAD);
       
   140             remoteOut.writeInt(classes.size());
       
   141             for (ClassInfo ci : infos) {
       
   142                 remoteOut.writeUTF(ci.getClassName());
       
   143                 remoteOut.writeObject(ci.getBytes());
       
   144             }
       
   145             remoteOut.flush();
       
   146             // Retrieve and report results from the remote agent.
       
   147             boolean result = readAndReportResult();
       
   148             // For each class that now has a JDI ReferenceType, mark the bytes
       
   149             // as loaded.
       
   150             infos.stream()
       
   151                     .filter(ci -> ci.getReferenceTypeOrNull() != null)
       
   152                     .forEach(ci -> ci.markLoaded());
       
   153             return result;
       
   154         } catch (IOException ex) {
       
   155             debug(DBG_GEN, "IOException on remote load operation: %s\n", ex);
       
   156             return false;
       
   157         }
       
   158     }
       
   159 
       
   160     /**
       
   161      * Invoke the doit method on the specified class.
       
   162      *
       
   163      * @param classname name of the wrapper class whose doit should be invoked
       
   164      * @return return the result value of the doit
       
   165      * @throws JShellException if a user exception was thrown (EvalException) or
       
   166      * an unresolved reference was encountered (UnresolvedReferenceException)
       
   167      */
       
   168     @Override
       
   169     public String invoke(String classname, String methodname) throws JShellException {
       
   170         try {
       
   171             synchronized (STOP_LOCK) {
       
   172                 userCodeRunning = true;
       
   173             }
       
   174             // Send the invoke command to the remote agent.
       
   175             remoteOut.writeInt(CMD_INVOKE);
       
   176             remoteOut.writeUTF(classname);
       
   177             remoteOut.writeUTF(methodname);
       
   178             remoteOut.flush();
       
   179             // Retrieve and report results from the remote agent.
       
   180             if (readAndReportExecutionResult()) {
       
   181                 String result = remoteIn.readUTF();
       
   182                 return result;
       
   183             }
       
   184         } catch (IOException | RuntimeException ex) {
       
   185             if (!jdiEnv.connection().isRunning()) {
       
   186                 // The JDI connection is no longer live, shutdown.
       
   187                 jdiEnv.shutdown();
       
   188             } else {
       
   189                 debug(DBG_GEN, "Exception on remote invoke: %s\n", ex);
       
   190                 return "Execution failure: " + ex.getMessage();
       
   191             }
       
   192         } finally {
       
   193             synchronized (STOP_LOCK) {
       
   194                 userCodeRunning = false;
       
   195             }
       
   196         }
       
   197         return "";
       
   198     }
       
   199 
       
   200     /**
       
   201      * Retrieves the value of a JShell variable.
       
   202      *
       
   203      * @param classname name of the wrapper class holding the variable
       
   204      * @param varname name of the variable
       
   205      * @return the value as a String
       
   206      */
       
   207     @Override
       
   208     public String varValue(String classname, String varname) {
       
   209         try {
       
   210             // Send the variable-value command to the remote agent.
       
   211             remoteOut.writeInt(CMD_VARVALUE);
       
   212             remoteOut.writeUTF(classname);
       
   213             remoteOut.writeUTF(varname);
       
   214             remoteOut.flush();
       
   215             // Retrieve and report results from the remote agent.
       
   216             if (readAndReportResult()) {
       
   217                 String result = remoteIn.readUTF();
       
   218                 return result;
       
   219             }
       
   220         } catch (EOFException ex) {
       
   221             jdiEnv.shutdown();
       
   222         } catch (IOException ex) {
       
   223             debug(DBG_GEN, "Exception on remote var value: %s\n", ex);
       
   224             return "Execution failure: " + ex.getMessage();
       
   225         }
       
   226         return "";
       
   227     }
       
   228 
       
   229     /**
       
   230      * Adds a path to the remote classpath.
       
   231      *
       
   232      * @param cp the additional path element
       
   233      * @return true if succesful
       
   234      */
       
   235     @Override
       
   236     public boolean addToClasspath(String cp) {
       
   237         try {
       
   238             // Send the classpath addition command to the remote agent.
       
   239             remoteOut.writeInt(CMD_CLASSPATH);
       
   240             remoteOut.writeUTF(cp);
       
   241             remoteOut.flush();
       
   242             // Retrieve and report results from the remote agent.
       
   243             return readAndReportResult();
       
   244         } catch (IOException ex) {
       
   245             throw new InternalError("Classpath addition failed: " + cp, ex);
       
   246         }
       
   247     }
       
   248 
       
   249     /**
       
   250      * Redefine the specified classes. Where 'redefine' is, as in JDI and JVMTI,
       
   251      * an in-place replacement of the classes (preserving class identity) --
       
   252      * that is, existing references to the class do not need to be recompiled.
       
   253      * This implementation uses JDI redefineClasses. It will be unsuccessful if
       
   254      * the signature of the class has changed (see the JDI spec). The
       
   255      * JShell-core is designed to adapt to unsuccessful redefine.
       
   256      *
       
   257      * @param classes the names of the classes to redefine
       
   258      * @return true if all the classes were redefined
       
   259      */
       
   260     @Override
       
   261     public boolean redefine(Collection<String> classes) {
       
   262         try {
       
   263             // Create corresponding ClassInfo instances to track the classes.
       
   264             // Each ClassInfo has the current class bytes associated with it.
       
   265             List<ClassInfo> infos = withBytes(classes);
       
   266             // Convert to the JDI ReferenceType to class bytes map form needed
       
   267             // by JDI.
       
   268             Map<ReferenceType, byte[]> rmp = infos.stream()
       
   269                     .collect(toMap(
       
   270                             ci -> ci.getReferenceTypeOrNull(),
       
   271                             ci -> ci.getBytes()));
       
   272             // Attempt redefine.  Throws exceptions on failure.
       
   273             jdiEnv.vm().redefineClasses(rmp);
       
   274             // Successful: mark the bytes as loaded.
       
   275             infos.stream()
       
   276                     .forEach(ci -> ci.markLoaded());
       
   277             return true;
       
   278         } catch (UnsupportedOperationException ex) {
       
   279             // A form of class transformation not supported by JDI
       
   280             return false;
       
   281         } catch (Exception ex) {
       
   282             debug(DBG_GEN, "Exception on JDI redefine: %s\n", ex);
       
   283             return false;
       
   284         }
       
   285     }
       
   286 
       
   287     /**
       
   288      * Converts a collection of class names into ClassInfo instances associated
       
   289      * with the most recently compiled class bytes.
       
   290      *
       
   291      * @param classes names of the classes
       
   292      * @return a list of corresponding ClassInfo instances
       
   293      */
       
   294     private List<ClassInfo> withBytes(Collection<String> classes) {
       
   295         return classes.stream()
       
   296                 .map(cn -> tracker.classInfo(cn, execEnv.getClassBytes(cn)))
       
   297                 .collect(toList());
       
   298     }
       
   299 
       
   300     /**
       
   301      * Reports the status of the named class. UNKNOWN if not loaded. CURRENT if
       
   302      * the most recent successfully loaded/redefined bytes match the current
       
   303      * compiled bytes.
       
   304      *
       
   305      * @param classname the name of the class to test
       
   306      * @return the status
       
   307      */
       
   308     @Override
       
   309     public ClassStatus getClassStatus(String classname) {
       
   310         ClassInfo ci = tracker.get(classname);
       
   311         if (ci.getReferenceTypeOrNull() == null) {
       
   312             // If the class does not have a JDI ReferenceType it has not been loaded
       
   313             return ClassStatus.UNKNOWN;
       
   314         }
       
   315         // Compare successfully loaded with last compiled bytes.
       
   316         return (Arrays.equals(execEnv.getClassBytes(classname), ci.getLoadedBytes()))
       
   317                 ? ClassStatus.CURRENT
       
   318                 : ClassStatus.NOT_CURRENT;
       
   319     }
       
   320 
       
   321     /**
       
   322      * Reports results from a remote agent command that does not expect
       
   323      * exceptions.
       
   324      *
       
   325      * @return true if successful
       
   326      * @throws IOException if the connection has dropped
       
   327      */
       
   328     private boolean readAndReportResult() throws IOException {
       
   329         int ok = remoteIn.readInt();
       
   330         switch (ok) {
       
   331             case RESULT_SUCCESS:
       
   332                 return true;
       
   333             case RESULT_FAIL: {
       
   334                 String ex = remoteIn.readUTF();
       
   335                 debug(DBG_GEN, "Exception on remote operation: %s\n", ex);
       
   336                 return false;
       
   337             }
       
   338             default: {
       
   339                 debug(DBG_GEN, "Bad remote result code: %s\n", ok);
       
   340                 return false;
       
   341             }
       
   342         }
       
   343     }
       
   344 
       
   345     /**
       
   346      * Reports results from a remote agent command that expects runtime
       
   347      * exceptions.
       
   348      *
       
   349      * @return true if successful
       
   350      * @throws IOException if the connection has dropped
       
   351      * @throws EvalException if a user exception was encountered on invoke
       
   352      * @throws UnresolvedReferenceException if an unresolved reference was
       
   353      * encountered
       
   354      */
       
   355     private boolean readAndReportExecutionResult() throws IOException, JShellException {
       
   356         int ok = remoteIn.readInt();
       
   357         switch (ok) {
       
   358             case RESULT_SUCCESS:
       
   359                 return true;
       
   360             case RESULT_FAIL: {
       
   361                 // An internal error has occurred.
       
   362                 String ex = remoteIn.readUTF();
       
   363                 return false;
       
   364             }
       
   365             case RESULT_EXCEPTION: {
       
   366                 // A user exception was encountered.
       
   367                 String exceptionClassName = remoteIn.readUTF();
       
   368                 String message = remoteIn.readUTF();
       
   369                 StackTraceElement[] elems = readStackTrace();
       
   370                 throw execEnv.createEvalException(message, exceptionClassName, elems);
       
   371             }
       
   372             case RESULT_CORRALLED: {
       
   373                 // An unresolved reference was encountered.
       
   374                 int id = remoteIn.readInt();
       
   375                 StackTraceElement[] elems = readStackTrace();
       
   376                 throw execEnv.createUnresolvedReferenceException(id, elems);
       
   377             }
       
   378             case RESULT_KILLED: {
       
   379                 // Execution was aborted by the stop()
       
   380                 debug(DBG_GEN, "Killed.");
       
   381                 return false;
       
   382             }
       
   383             default: {
       
   384                 debug(DBG_GEN, "Bad remote result code: %s\n", ok);
       
   385                 return false;
       
   386             }
       
   387         }
       
   388     }
       
   389 
       
   390     private StackTraceElement[] readStackTrace() throws IOException {
       
   391         int elemCount = remoteIn.readInt();
       
   392         StackTraceElement[] elems = new StackTraceElement[elemCount];
       
   393         for (int i = 0; i < elemCount; ++i) {
       
   394             String className = remoteIn.readUTF();
       
   395             String methodName = remoteIn.readUTF();
       
   396             String fileName = remoteIn.readUTF();
       
   397             int line = remoteIn.readInt();
       
   398             elems[i] = new StackTraceElement(className, methodName, fileName, line);
       
   399         }
       
   400         return elems;
       
   401     }
       
   402 
       
   403     /**
       
   404      * Launch the remote agent as a JDI connection.
       
   405      *
       
   406      * @param port the socket port for (non-JDI) commands
       
   407      */
       
   408     private void jdiGo(int port) {
       
   409         //MessageOutput.textResources = ResourceBundle.getBundle("impl.TTYResources",
       
   410         //        Locale.getDefault());
       
   411 
       
   412         // Set-up for a fresh launch of a remote agent with any user-specified VM options.
       
   413         String connectorName = "com.sun.jdi.CommandLineLaunch";
       
   414         Map<String, String> argumentName2Value = new HashMap<>();
       
   415         argumentName2Value.put("main", "jdk.internal.jshell.remote.RemoteAgent " + port);
       
   416         argumentName2Value.put("options", remoteVMOptions);
       
   417 
       
   418         boolean launchImmediately = true;
       
   419         int traceFlags = 0;// VirtualMachine.TRACE_SENDS | VirtualMachine.TRACE_EVENTS;
       
   420 
       
   421         // Launch.
       
   422         jdiEnv.init(connectorName, argumentName2Value, launchImmediately, traceFlags);
       
   423 
       
   424         if (jdiEnv.connection().isOpen() && jdiEnv.vm().canBeModified()) {
       
   425             /*
       
   426              * Connection opened on startup. Start event handler
       
   427              * immediately, telling it (through arg 2) to stop on the
       
   428              * VM start event.
       
   429              */
       
   430             handler = new JDIEventHandler(jdiEnv);
       
   431         }
       
   432     }
       
   433 
       
   434     private final Object STOP_LOCK = new Object();
       
   435     private boolean userCodeRunning = false;
       
   436 
       
   437     /**
       
   438      * Interrupt a running invoke.
       
   439      */
       
   440     @Override
       
   441     public void stop() {
       
   442         synchronized (STOP_LOCK) {
       
   443             if (!userCodeRunning) {
       
   444                 return;
       
   445             }
       
   446 
       
   447             VirtualMachine vm = handler.env.vm();
       
   448             vm.suspend();
       
   449             try {
       
   450                 OUTER:
       
   451                 for (ThreadReference thread : vm.allThreads()) {
       
   452                     // could also tag the thread (e.g. using name), to find it easier
       
   453                     for (StackFrame frame : thread.frames()) {
       
   454                         String remoteAgentName = "jdk.internal.jshell.remote.RemoteAgent";
       
   455                         if (remoteAgentName.equals(frame.location().declaringType().name())
       
   456                                 && "commandLoop".equals(frame.location().method().name())) {
       
   457                             ObjectReference thiz = frame.thisObject();
       
   458                             if (((BooleanValue) thiz.getValue(thiz.referenceType().fieldByName("inClientCode"))).value()) {
       
   459                                 thiz.setValue(thiz.referenceType().fieldByName("expectingStop"), vm.mirrorOf(true));
       
   460                                 ObjectReference stopInstance = (ObjectReference) thiz.getValue(thiz.referenceType().fieldByName("stopException"));
       
   461 
       
   462                                 vm.resume();
       
   463                                 debug(DBG_GEN, "Attempting to stop the client code...\n");
       
   464                                 thread.stop(stopInstance);
       
   465                                 thiz.setValue(thiz.referenceType().fieldByName("expectingStop"), vm.mirrorOf(false));
       
   466                             }
       
   467 
       
   468                             break OUTER;
       
   469                         }
       
   470                     }
       
   471                 }
       
   472             } catch (ClassNotLoadedException | IncompatibleThreadStateException | InvalidTypeException ex) {
       
   473                 debug(DBG_GEN, "Exception on remote stop: %s\n", ex);
       
   474             } finally {
       
   475                 vm.resume();
       
   476             }
       
   477         }
       
   478     }
       
   479 
       
   480     void debug(int flags, String format, Object... args) {
       
   481         InternalDebugControl.debug(execEnv.state(), execEnv.userErr(), flags, format, args);
       
   482     }
       
   483 
       
   484     void debug(Exception ex, String where) {
       
   485         InternalDebugControl.debug(execEnv.state(), execEnv.userErr(), ex, where);
       
   486     }
       
   487 
       
   488     private final class DemultiplexInput extends Thread {
       
   489 
       
   490         private final DataInputStream delegate;
       
   491         private final PipeInputStream command;
       
   492         private final PrintStream out;
       
   493         private final PrintStream err;
       
   494 
       
   495         public DemultiplexInput(InputStream input,
       
   496                 PipeInputStream command,
       
   497                 PrintStream out,
       
   498                 PrintStream err) {
       
   499             super("output reader");
       
   500             this.delegate = new DataInputStream(input);
       
   501             this.command = command;
       
   502             this.out = out;
       
   503             this.err = err;
       
   504         }
       
   505 
       
   506         public void run() {
       
   507             try {
       
   508                 while (true) {
       
   509                     int nameLen = delegate.read();
       
   510                     if (nameLen == (-1))
       
   511                         break;
       
   512                     byte[] name = new byte[nameLen];
       
   513                     DemultiplexInput.this.delegate.readFully(name);
       
   514                     int dataLen = delegate.read();
       
   515                     byte[] data = new byte[dataLen];
       
   516                     DemultiplexInput.this.delegate.readFully(data);
       
   517                     switch (new String(name, "UTF-8")) {
       
   518                         case "err":
       
   519                             err.write(data);
       
   520                             break;
       
   521                         case "out":
       
   522                             out.write(data);
       
   523                             break;
       
   524                         case "command":
       
   525                             for (byte b : data) {
       
   526                                 command.write(Byte.toUnsignedInt(b));
       
   527                             }
       
   528                             break;
       
   529                     }
       
   530                 }
       
   531             } catch (IOException ex) {
       
   532                 debug(ex, "Failed reading output");
       
   533             } finally {
       
   534                 command.close();
       
   535             }
       
   536         }
       
   537 
       
   538     }
       
   539 
       
   540     public static final class PipeInputStream extends InputStream {
       
   541         public static final int INITIAL_SIZE = 128;
       
   542 
       
   543         private int[] buffer = new int[INITIAL_SIZE];
       
   544         private int start;
       
   545         private int end;
       
   546         private boolean closed;
       
   547 
       
   548         @Override
       
   549         public synchronized int read() {
       
   550             while (start == end) {
       
   551                 if (closed) {
       
   552                     return -1;
       
   553                 }
       
   554                 try {
       
   555                     wait();
       
   556                 } catch (InterruptedException ex) {
       
   557                     //ignore
       
   558                 }
       
   559             }
       
   560             try {
       
   561                 return buffer[start];
       
   562             } finally {
       
   563                 start = (start + 1) % buffer.length;
       
   564             }
       
   565         }
       
   566 
       
   567         public synchronized void write(int b) {
       
   568             if (closed)
       
   569                 throw new IllegalStateException("Already closed.");
       
   570             int newEnd = (end + 1) % buffer.length;
       
   571             if (newEnd == start) {
       
   572                 //overflow:
       
   573                 int[] newBuffer = new int[buffer.length * 2];
       
   574                 int rightPart = (end > start ? end : buffer.length) - start;
       
   575                 int leftPart = end > start ? 0 : start - 1;
       
   576                 System.arraycopy(buffer, start, newBuffer, 0, rightPart);
       
   577                 System.arraycopy(buffer, 0, newBuffer, rightPart, leftPart);
       
   578                 buffer = newBuffer;
       
   579                 start = 0;
       
   580                 end = rightPart + leftPart;
       
   581                 newEnd = end + 1;
       
   582             }
       
   583             buffer[end] = b;
       
   584             end = newEnd;
       
   585             notifyAll();
       
   586         }
       
   587 
       
   588         @Override
       
   589         public synchronized void close() {
       
   590             closed = true;
       
   591             notifyAll();
       
   592         }
       
   593 
       
   594     }
       
   595 }