001: /*
002: * All content copyright (c) 2003-2006 Terracotta, Inc., except as may otherwise be noted in a separate copyright
003: * notice. All rights reserved.
004: */
005: package com.tc.process;
006:
007: import com.tc.util.Assert;
008: import com.tc.util.runtime.Os;
009:
010: import java.io.File;
011: import java.io.IOException;
012: import java.io.InputStream;
013: import java.io.OutputStream;
014: import java.util.ArrayList;
015: import java.util.Arrays;
016: import java.util.Collections;
017: import java.util.HashMap;
018: import java.util.Iterator;
019: import java.util.LinkedList;
020: import java.util.List;
021: import java.util.Map;
022:
023: /**
024: * A child Java process that uses a socket-based ping protocol to make sure that if the parent dies, the child dies a
025: * short time thereafter. Useful for avoiding 'zombie child processes' when writing tests, etc. — otherwise, if
026: * the parent process crashes or otherwise terminates abnormally, you'll get child processes accumulating until all hell
027: * breaks loose on the box.
028: * </p>
029: * <p>
030: * Although it can't actually be related through Java inheritance (because {@link Process}is a class, not an
031: * interface), this class behaves essentially identical to {@link Process}with two differences:
032: * <ul>
033: * <li>You instantiate this class directly, rather than getting an instance from {@link Runtime#exec}.</li>
034: * <li>The process doesn't start until you call {@link #start}.</li>
035: * </ul>
036: */
037: public class LinkedJavaProcess {
038:
039: private File javaHome;
040: private final String mainClassName;
041: private String[] javaArguments;
042: private final String[] arguments;
043: private String[] environment;
044: private File directory;
045: private File javaExecutable;
046:
047: private Process process;
048: private boolean running;
049: private final List copiers = Collections
050: .synchronizedList(new ArrayList());
051:
052: public LinkedJavaProcess(String mainClassName,
053: String[] classArguments) {
054: Assert.assertNotBlank(mainClassName);
055:
056: if (classArguments == null)
057: classArguments = new String[0];
058:
059: this .mainClassName = mainClassName;
060: this .javaArguments = null;
061: this .arguments = classArguments;
062: this .environment = null;
063: this .directory = null;
064: this .javaExecutable = null;
065: this .process = null;
066: this .running = false;
067: }
068:
069: public File getJavaHome() {
070: return javaHome;
071: }
072:
073: public void setJavaHome(File javaHome) {
074: this .javaHome = javaHome;
075: }
076:
077: public LinkedJavaProcess(String mainClassName) {
078: this (mainClassName, null);
079: }
080:
081: public void setJavaExecutable(File javaExecutable) {
082: Assert.assertNotNull(javaExecutable);
083:
084: this .javaExecutable = javaExecutable;
085: }
086:
087: public void setJavaArguments(String[] javaArguments) {
088: this .javaArguments = javaArguments;
089: }
090:
091: public void setEnvironment(String[] environment) {
092: this .environment = environment;
093: }
094:
095: public void setDirectory(File directory) {
096: this .directory = directory;
097: }
098:
099: public synchronized void destroy() {
100: if (!this .running)
101: throw new IllegalStateException(
102: "This LinkedJavaProcess is not running.");
103: this .process.destroy();
104: this .running = false;
105: }
106:
107: private synchronized void setJavaExecutableIfNecessary()
108: throws IOException {
109: if (this .javaExecutable == null) {
110: if (javaHome == null) {
111: javaHome = new File(System.getProperty("java.home"));
112: }
113:
114: File javaBin = new File(javaHome, "bin");
115: File javaPlain = new File(javaBin, "java");
116: File javaExe = new File(javaBin, "java.exe");
117:
118: if (this .javaExecutable == null) {
119: if (javaPlain.exists() && javaPlain.isFile())
120: this .javaExecutable = javaPlain;
121: }
122:
123: if (this .javaExecutable == null) {
124: if (javaExe.exists() && javaExe.isFile())
125: this .javaExecutable = javaExe;
126: }
127:
128: if (this .javaExecutable == null) {
129: // formatting
130: throw new IOException(
131: "Can't find the Java binary; perhaps you need to set it yourself? Tried "
132: + javaPlain.getAbsolutePath() + " and "
133: + javaExe.getAbsolutePath());
134: }
135: }
136: }
137:
138: public synchronized void start() throws IOException {
139: if (this .running)
140: throw new IllegalStateException(
141: "This LinkedJavaProcess is already running.");
142:
143: HeartBeatService.startHeartBeatService();
144:
145: List fullCommandList = new LinkedList();
146: List allJavaArguments = new ArrayList();
147:
148: allJavaArguments.add("-Djava.class.path="
149: + System.getProperty("java.class.path"));
150: allJavaArguments.add("-Dcom.tc.l1.modules.repositories="
151: + System.getProperty("com.tc.l1.modules.repositories"));
152: if (this .javaArguments != null)
153: allJavaArguments.addAll(Arrays.asList(this .javaArguments));
154:
155: setJavaExecutableIfNecessary();
156:
157: int socketPort = HeartBeatService.listenPort();
158:
159: Map env = makeEnvMap(Arrays
160: .asList(this .environment == null ? new String[] {}
161: : this .environment));
162: fixupEnvironment(env);
163:
164: fullCommandList.add(this .javaExecutable.getAbsolutePath());
165: fullCommandList.addAll(allJavaArguments);
166: fullCommandList.add(LinkedJavaProcessStarter.class.getName());
167: fullCommandList.add(Integer.toString(socketPort));
168: fullCommandList.add(this .mainClassName);
169: if (this .arguments != null)
170: fullCommandList.addAll(Arrays.asList(this .arguments));
171:
172: String[] fullCommand = (String[]) fullCommandList
173: .toArray(new String[fullCommandList.size()]);
174:
175: this .process = Runtime.getRuntime().exec(fullCommand,
176: makeEnv(env), this .directory);
177: this .running = true;
178: }
179:
180: private Map makeEnvMap(List list) {
181: Map rv = new HashMap();
182:
183: for (Iterator iter = list.iterator(); iter.hasNext();) {
184: String[] nameValue = ((String) iter.next()).split("=", 2);
185: rv.put(nameValue[0], nameValue[1]);
186: }
187:
188: return rv;
189: }
190:
191: private String[] makeEnv(Map env) {
192: int i = 0;
193: String[] rv = new String[env.size()];
194: for (Iterator iter = env.keySet().iterator(); iter.hasNext(); i++) {
195: String key = (String) iter.next();
196: rv[i] = key + "=" + env.get(key);
197: }
198: return rv;
199: }
200:
201: private static void fixupEnvironment(Map env) {
202: if (Os.isWindows()) {
203: // A bunch of name lookup stuff will fail w/o setting SYSTEMROOT. Also, if
204: // you have apple's rendevous/bonjour
205: // client installed, it needs to be in the PATH such that dnssd.dll will
206: // be found when using DNS
207:
208: if (!env.containsKey("SYSTEMROOT")) {
209: String root = Os.findWindowsSystemRoot();
210: if (root == null) {
211: throw new RuntimeException(
212: "cannot find %SYSTEMROOT% in the environment");
213: }
214: env.put("SYSTEMROOT", root);
215: }
216:
217: String crappleDirs = "C:\\Program Files\\Rendezvous\\"
218: + File.pathSeparator
219: + "C:\\Program Files\\Bonjour\\";
220:
221: if (!env.containsKey("PATH")) {
222: env.put("PATH", crappleDirs);
223: } else {
224: String path = (String) env.get("PATH");
225: path = path + File.pathSeparator + crappleDirs;
226: env.put("PATH", path);
227: }
228: }
229: }
230:
231: /**
232: * Java names these things a bit funny — this is the spawned process's <tt>stdout</tt>.
233: */
234: public synchronized InputStream getInputStream() {
235: if (!this .running)
236: throw new IllegalStateException(
237: "This LinkedJavaProcess is not yet running.");
238: return this .process.getInputStream();
239: }
240:
241: public InputStream STDOUT() {
242: return getInputStream();
243: }
244:
245: public OutputStream STDIN() {
246: return getOutputStream();
247: }
248:
249: public InputStream STDERR() {
250: return getErrorStream();
251: }
252:
253: public void mergeSTDOUT() {
254: mergeSTDOUT(null);
255: }
256:
257: public void mergeSTDOUT(String identifier) {
258: mergeStream(STDOUT(), System.out, identifier);
259: }
260:
261: public void mergeSTDERR() {
262: mergeSTDERR(null);
263: }
264:
265: public void mergeSTDERR(String identifier) {
266: mergeStream(STDERR(), System.err, identifier);
267: }
268:
269: private void mergeStream(InputStream in, OutputStream out,
270: String identifier) {
271: StreamCopier copier = new StreamCopier(in, out, identifier);
272: copiers.add(copier);
273: copier.start();
274: }
275:
276: /**
277: * This is the spawned process's <tt>stderr</tt>.
278: */
279: public synchronized InputStream getErrorStream() {
280: if (!this .running)
281: throw new IllegalStateException(
282: "This LinkedJavaProcess is not yet running.");
283: return this .process.getErrorStream();
284: }
285:
286: /**
287: * Java names these things a bit funny — this is the spawned process's <tt>stdin</tt>.
288: */
289: public synchronized OutputStream getOutputStream() {
290: if (!this .running)
291: throw new IllegalStateException(
292: "This LinkedJavaProcess is not yet running.");
293: return this .process.getOutputStream();
294: }
295:
296: public synchronized int exitValue() {
297: if (this .process == null)
298: throw new IllegalStateException(
299: "This LinkedJavaProcess has not been started.");
300: int out = this .process.exitValue();
301: // Process.exitValue() throws an exception if not yet terminated, so we know
302: // it's terminated now.
303: this .running = false;
304: return out;
305: }
306:
307: public int waitFor() throws InterruptedException {
308: Process theProcess = null;
309:
310: synchronized (this ) {
311: if (!this .running)
312: throw new IllegalStateException(
313: "This LinkedJavaProcess is not running.");
314: theProcess = this .process;
315: Assert.assertNotNull(theProcess);
316: }
317:
318: int exitCode = theProcess.waitFor();
319:
320: for (Iterator i = copiers.iterator(); i.hasNext();) {
321: Thread t = (Thread) i.next();
322: t.join();
323: i.remove();
324: }
325:
326: synchronized (this ) {
327: this .running = false;
328: }
329:
330: return exitCode;
331:
332: }
333:
334: }
|