001: /*BEGIN_COPYRIGHT_BLOCK
002: *
003: * Copyright (c) 2001-2007, JavaPLT group at Rice University (javaplt@rice.edu)
004: * All rights reserved.
005: *
006: * Redistribution and use in source and binary forms, with or without
007: * modification, are permitted provided that the following conditions are met:
008: * * Redistributions of source code must retain the above copyright
009: * notice, this list of conditions and the following disclaimer.
010: * * Redistributions in binary form must reproduce the above copyright
011: * notice, this list of conditions and the following disclaimer in the
012: * documentation and/or other materials provided with the distribution.
013: * * Neither the names of DrJava, the JavaPLT group, Rice University, nor the
014: * names of its contributors may be used to endorse or promote products
015: * derived from this software without specific prior written permission.
016: *
017: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
018: * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
019: * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
020: * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
021: * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
022: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
023: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
024: * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
025: * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
026: * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
027: * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
028: *
029: * This software is Open Source Initiative approved Open Source Software.
030: * Open Source Initative Approved is a trademark of the Open Source Initiative.
031: *
032: * This file is part of DrJava. Download the current version of this project
033: * from http://www.drjava.org/ or http://sourceforge.net/projects/drjava/
034: *
035: * END_COPYRIGHT_BLOCK*/
036:
037: package edu.rice.cs.util.newjvm;
038:
039: import edu.rice.cs.util.Log;
040: import edu.rice.cs.util.UnexpectedException;
041: import edu.rice.cs.drjava.config.FileOption;
042: import edu.rice.cs.plt.concurrent.ConcurrentUtil;
043:
044: import java.rmi.*;
045: import java.rmi.server.*;
046: import java.io.*;
047: import java.util.Arrays;
048: import java.util.LinkedList;
049: import java.util.Properties;
050: import java.util.Map;
051:
052: /** An abstract class implementing the logic to invoke and control, via RMI, a second Java virtual
053: * machine. This class is used by subclassing it. (See package documentation for more details.)
054: * This class runs in both the master and the slave JVMs.
055: * @version $Id: AbstractMasterJVM.java 4255 2007-08-28 19:17:37Z mgricken $
056: */
057: public abstract class AbstractMasterJVM
058: /*<SlaveType extends SlaveRemote>*/extends UnicastRemoteObject
059: implements MasterRemote/*<SlaveType>*/{
060:
061: public static final Log _log = new Log("MasterSlave.txt", false);
062:
063: /** Name for the thread that waits for the slave to exit. */
064: protected volatile String _waitForQuitThreadName = "Wait for SlaveJVM Exit Thread";
065:
066: // /** Name for the thread that exports the MasterJVM to RMI. */
067: // protected volatile String _exportMasterThreadName = "Export MasterJVM Thread";
068:
069: /** Lock for accessing the critical state of this AbstractMasterJVM including _monitorThread. */
070: protected final Object _masterJVMLock = new Object();
071:
072: private static final String RUNNER = SlaveJVMRunner.class.getName();
073:
074: /** The slave JVM remote stub if it's connected; null if not connected. */
075: protected volatile SlaveRemote _slave;
076:
077: /** Is slave JVM in the process of starting up? INVARIANT: _startupInProgess => _slave == null. */
078: private volatile boolean _startupInProgress = false;
079:
080: /** This flag is set when a quit request is issued before the slave has finished starting up.
081: * In that case, immediately after starting up, we quit it. INVARIANT: _quitOnStartUp => _startupInProgress
082: */
083: private volatile boolean _quitOnStartup = false;
084:
085: // /** Lock used in exporting this object to a file and loading it in the slaveJVM; protects stub variables. */
086: // final static Object Lock = new Object();
087:
088: /** The current remote stub for this main JVM object. This field is null except between the time the slave
089: * JVM is first invoked and the time the slave registers itself.
090: */
091: private volatile MasterRemote _masterStub = null;
092:
093: /** The file containing the serialized remote stub. This field is null except between the time the slave
094: * JVM is first invoked and the time the slave registers itself.
095: */
096: private volatile File _masterStubFile;
097:
098: /** The fully-qualified name of the slave JVM class. */
099: private final String _slaveClassName;
100:
101: /** The thread monitoring the Slave JVM, waiting for it to terminate. This feature inhibits the creation
102: * of more than one Slave JVM corresponding to "this"
103: */
104: private volatile Thread _monitorThread;
105:
106: // /** The lock used to protect _monitorThread. */
107: // private final Object _monitorLock = new Object();
108:
109: /** Sets up the master JVM object, but does not actually invoke the slave JVM.
110: * @param slaveClassName The fully-qualified class name of the class to start up in the second JVM. This
111: * class must implement the interface specified by this class's type parameter, which must be a subclass
112: * of {@link SlaveRemote}.
113: */
114: protected AbstractMasterJVM(String slaveClassName)
115: throws RemoteException {
116: super ();
117: _slaveClassName = slaveClassName;
118: _slave = null;
119: _monitorThread = null;
120:
121: _log.log(this + " CREATED");
122:
123: // Make sure RMI doesn't use an IP address that might change
124: System.setProperty("java.rmi.server.hostname", "127.0.0.1");
125: }
126:
127: /** Callback for when the slave JVM has connected, and the bidirectional communications link has been
128: * established. During this call, {@link #getSlave} is guaranteed to not return null.
129: */
130: protected abstract void handleSlaveConnected();
131:
132: /** Callback for when the slave JVM has quit. During this call, {@link #getSlave} is guaranteed to return null.
133: * @param status The exit code returned by the slave JVM.
134: */
135: protected abstract void handleSlaveQuit(int status);
136:
137: /** Invokes slave JVM without any JVM arguments.
138: * @throws IllegalStateException if slave JVM already connected or startUp is in progress.
139: */
140: protected final void invokeSlave() throws IOException,
141: RemoteException {
142: invokeSlave(new String[0], FileOption.NULL_FILE);
143: }
144:
145: /** Invokes slave JVM, using the system classpath.
146: * @param jvmArgs Array of arguments to pass to the JVM on startUp
147: * @throws IllegalStateException if slave JVM already connected or startUp is in progress.
148: */
149: protected final void invokeSlave(String[] jvmArgs, File workDir)
150: throws IOException, RemoteException {
151: invokeSlave(jvmArgs, System.getProperty("java.class.path"),
152: workDir);
153: }
154:
155: /** Creates and invokes slave JVM.
156: * @param jvmArgs Array of arguments to pass to the JVM on startUp
157: * @param cp Classpath to use when starting the JVM
158: * @throws IllegalStateException if slave JVM already connected or startUp is in progress.
159: */
160: protected final void invokeSlave(final String[] jvmArgs,
161: final String cp, final File workDir) throws IOException,
162: RemoteException {
163:
164: synchronized (_masterJVMLock) { // synchronization prelude only lets one thread at a time execute the sequel
165:
166: try {
167: while (_startupInProgress || _monitorThread != null)
168: _masterJVMLock.wait();
169: } catch (InterruptedException e) {
170: throw new UnexpectedException(e);
171: }
172: _startupInProgress = true;
173: }
174:
175: _log.log(this + ".invokeSlave(...) called");
176:
177: /******************************************************************************************************
178: * First, we we export ourselves to a file, if it has not already been done on a previous invocation. *
179: *****************************************************************************************************/
180:
181: if (_masterStub == null) {
182: try {
183: _masterStub = (MasterRemote) toStub(this );
184: } catch (RemoteException re) {
185: javax.swing.JOptionPane.showMessageDialog(null,
186: edu.rice.cs.util.StringOps.getStackTrace(re));
187: _log.log(this + " threw " + re);
188: throw new UnexpectedException(re); // should never happen
189: }
190: _log.log(this + " EXPORTed Master JVM");
191:
192: _masterStubFile = File.createTempFile("DrJava-remote-stub",
193: ".tmp");
194: _masterStubFile.deleteOnExit();
195:
196: // serialize stub to _masterStubFile
197: FileOutputStream fstream = new FileOutputStream(
198: _masterStubFile);
199: ObjectOutputStream ostream = new ObjectOutputStream(fstream);
200: ostream.writeObject(_masterStub);
201: ostream.flush();
202: fstream.close();
203: ostream.close();
204: }
205:
206: final String[] args = new String[] {
207: _masterStubFile.getAbsolutePath(), _slaveClassName };
208:
209: LinkedList<String> fullJVMArgs = new LinkedList<String>(Arrays
210: .asList(jvmArgs));
211: Properties propagate = ConcurrentUtil.getProperties("plt.",
212: "drjava.", "edu.rice.cs.");
213: if (propagate.containsKey("plt.debug.log")
214: || propagate.containsKey("plt.error.log")
215: || propagate.containsKey("plt.log.factory")) {
216: propagate.put("plt.log.working.dir", System.getProperty(
217: "user.dir", ""));
218: }
219: for (Map.Entry<Object, Object> entry : propagate.entrySet()) {
220: fullJVMArgs.addFirst("-D" + entry.getKey() + "="
221: + entry.getValue());
222: }
223: final String[] jvmArgsArray = fullJVMArgs
224: .toArray(new String[0]);
225:
226: // Start a thread to create the slave JVM and wait for it to die. When it dies, delegate what to do (restart?)
227: // to subclass
228: _monitorThread = new Thread(_waitForQuitThreadName) {
229: public void run() {
230: try { /* Create the slave JVM. */
231:
232: _log.log(AbstractMasterJVM.this
233: + " is STARTING a Slave JVM with args "
234: + Arrays.asList(args));
235:
236: final Process process = ExecJVM.runJVM(RUNNER,
237: args, cp, jvmArgsArray, workDir);
238: _log.log(AbstractMasterJVM.this
239: + " CREATED Slave JVM process " + process
240: + " with " + asString());
241:
242: int status = process.waitFor();
243: // System.err.println(process + " DIED under control of " + asString() + " with status " + status);
244: synchronized (_masterJVMLock) {
245: if (_startupInProgress) {
246: // System.err.println("Process " + process + " died while starting up");
247: /* If we get here, the process died without registering. One possible cause is the intermittent funky 3 minute
248: * pause in readObject in RUNNER. Other possible causes are errors in the classpath or the absence of a
249: * debug port. Proper behavior in this case is unclear, so we'll let our subclasses decide. */
250: slaveQuitDuringStartup(status);
251: }
252: if (_slave != null) { // Slave JVM quit spontaneously
253: _slave = null;
254: }
255: _monitorThread = null;
256: _masterJVMLock.notifyAll(); // signal that Slave JVM died to any thread waiting for _monitorThread == null
257: }
258:
259: // _log.log(asString() + " calling handleSlaveQuit(" + status + ")");
260: handleSlaveQuit(status);
261: } catch (NoSuchObjectException e) {
262: throw new UnexpectedException(e);
263: } catch (InterruptedException e) {
264: throw new UnexpectedException(e);
265: } catch (IOException e) {
266: throw new UnexpectedException(e);
267: }
268: }
269:
270: private String asString() {
271: return "MonitorThread@"
272: + Integer.toHexString(hashCode());
273: }
274: };
275: // _log.log(this + " is starting a slave monitor thread to detect when the Slave JVM dies");
276: _monitorThread.start();
277: }
278:
279: /** Waits until no slave JVM is running under control of "this" */
280: public void waitSlaveDone() {
281: try {
282: synchronized (_masterJVMLock) {
283: while (_monitorThread != null)
284: _masterJVMLock.wait();
285: }
286: } catch (InterruptedException e) {
287: throw new UnexpectedException(e);
288: }
289: }
290:
291: /** Action to take if the slave JVM quits before registering. Assumes _masterJVMLock is held.
292: * @param status Status code of the JVM
293: */
294: protected void slaveQuitDuringStartup(int status) {
295: // Reset Master JVM state (in case invokeSlave is called again on this object)
296: _startupInProgress = false;
297: _quitOnStartup = false;
298: _monitorThread = null;
299: }
300:
301: /** Called if the slave JVM dies before it is able to register.
302: * @param cause The Throwable which caused the slave to die.
303: */
304: public abstract void errorStartingSlave(Throwable cause)
305: throws RemoteException;
306:
307: /** No-op to prove that the master is still alive. */
308: public void checkStillAlive() {
309: }
310:
311: /* Records the identity and status of the Slave JVM in the Master JVM */
312: public void registerSlave(SlaveRemote slave) throws RemoteException {
313: _log.log(this + " registering Slave " + slave);
314:
315: boolean quitSlavePending; // flag used to move quitSlave() call out of synchronized block
316:
317: synchronized (_masterJVMLock) {
318: _slave = slave;
319: _startupInProgress = false;
320:
321: _log.log(this + " calling handleSlaveConnected()");
322:
323: handleSlaveConnected();
324:
325: quitSlavePending = _quitOnStartup;
326: if (_quitOnStartup) {
327: // quitSlave was called before the slave registered, so we now act on the deferred quit request.
328: _quitOnStartup = false;
329: }
330: }
331: if (quitSlavePending) {
332: _log
333: .log(this
334: + " Executing deferred quitSlave() that was called during startUp");
335: quitSlave(); // not synchronized; _slave may be null when this code executes
336: }
337: }
338:
339: /** Withdraws RMI exports for this. */
340: public void dispose() throws RemoteException {
341: _log.log(this + ".dispose() called; slaveRemote is " + _slave);
342: if (_startupInProgress)
343: _log
344: .log(this
345: + ".dispose() is KILLing startUp in process; dying slave reference does not yet exist");
346: SlaveRemote dyingSlave;
347: synchronized (_masterJVMLock) {
348: _masterStub = null;
349: if (_monitorThread != null)
350: _monitorThread = null;
351: dyingSlave = _slave; // save value of _slave in case it is not null
352: _slave = null;
353:
354: // Withdraw RMI exports
355: // Slave in process of starting will die because master is inaccessible.
356: _log.log(this + ".dispose() UNEXPORTing " + this );
357: UnicastRemoteObject.unexportObject(this , true);
358: }
359: if (dyingSlave != null) {
360: _log.log(this + ".dispose() QUITing " + dyingSlave);
361: dyingSlave.quit(); // unsynchronized; may hasten the death of dyingSlave
362: }
363: }
364:
365: /** Quits slave JVM. On exit, _slave == null. _quitOnStartup may be true
366: * @throws IllegalStateException if no slave JVM is connected
367: */
368: protected final void quitSlave() throws RemoteException {
369: SlaveRemote dyingSlave;
370: synchronized (_masterJVMLock) {
371: if (isStartupInProgress()) {
372: /* There is a slave to be quit, but _slave == null, so we cannot contact it yet. Instead we set _quitOnStartup
373: * and tell the slave to quit when it registers in registerSlave. */
374: _quitOnStartup = true;
375: return;
376: } else if (_slave == null) {
377: _log
378: .log(this
379: + " called quitSlave() when no slave was running");
380: return;
381: } else {
382: dyingSlave = _slave;
383: _slave = null;
384: }
385: }
386: dyingSlave.quit(); // remote operation is not synchronized!
387: }
388:
389: /** Returns slave remote instance, or null if not connected. */
390: protected final SlaveRemote getSlave() {
391: return _slave;
392: }
393:
394: /** Returns true if the slave is in the process of starting. */
395: protected boolean isStartupInProgress() {
396: return _startupInProgress;
397: }
398: }
|