001    package org.tynamo.watchdog.services;
002    
003    import java.io.File;
004    import java.io.FileOutputStream;
005    import java.io.IOException;
006    import java.io.InputStream;
007    import java.io.OutputStream;
008    import java.net.JarURLConnection;
009    import java.net.URISyntaxException;
010    import java.net.URL;
011    import java.util.jar.JarFile;
012    
013    import org.apache.tapestry5.SymbolConstants;
014    import org.apache.tapestry5.internal.InternalConstants;
015    import org.apache.tapestry5.ioc.annotations.EagerLoad;
016    import org.apache.tapestry5.ioc.annotations.Inject;
017    import org.apache.tapestry5.ioc.annotations.Symbol;
018    import org.slf4j.Logger;
019    import org.tynamo.watchdog.StreamGobbler;
020    
021    import tynamo_watchdog.Watchdog;
022    
023    @EagerLoad
024    public class WatchdogServiceImpl implements WatchdogService {
025    
026            private Process watchdog;
027            private WatchdogLeash watchdogLeash;
028            private volatile boolean watchdogAlarmed;
029    
030            OutputStream watchdogOutputStream;
031            private final String appPackageName;
032            private final String smtpHost;
033            private final Integer smtpPort;
034            private final String sendEmail;
035            private final Logger logger;
036    
037            public WatchdogServiceImpl(Logger logger, @Symbol(SymbolConstants.PRODUCTION_MODE) boolean productionMode,
038                            @Inject @Symbol(InternalConstants.TAPESTRY_APP_PACKAGE_PARAM) final String appPackageName,
039                            @Inject @Symbol(Watchdog.SMTP_HOST) final String smtpHost, @Symbol(Watchdog.SMTP_PORT) final Integer smtpPort,
040                            @Inject @Symbol(Watchdog.SEND_EMAIL) String sendEmail) throws IOException, URISyntaxException, InterruptedException {
041                    this.logger = logger;
042                    this.appPackageName = appPackageName;
043                    this.smtpHost = smtpHost;
044                    this.smtpPort = smtpPort;
045                    this.sendEmail = sendEmail;
046                    if (productionMode) startWatchdog();
047            }
048    
049            /**
050             * Extract a resource from jar, mark it for deletion upon exit, and return its location.
051             */
052            File extractFromJar(URL resource, File watchdogFolder) throws IOException {
053                    // put this jar in a file system so that we can load jars from there
054                    String fileName = resource.getPath().substring(resource.getPath().lastIndexOf("/"));
055    
056                    File file = new File(watchdogFolder, fileName);
057                    try {
058                            file.createNewFile();
059                    } catch (IOException e) {
060                            String tmpdir = System.getProperty("java.io.tmpdir");
061                            IOException x = new IOException("Watchdog failed to create a temporary file in " + tmpdir);
062                            x.initCause(e);
063                            throw x;
064                    }
065                    InputStream is = resource.openStream();
066                    try {
067                            OutputStream os = new FileOutputStream(file);
068                            try {
069                                    copyStream(is, os);
070                            } finally {
071                                    os.close();
072                            }
073                    } finally {
074                            is.close();
075                    }
076    
077                    file.deleteOnExit();
078                    return file;
079            }
080    
081            private static void copyStream(InputStream in, OutputStream out) throws IOException {
082                    byte[] buf = new byte[8192];
083                    int len;
084                    while ((len = in.read(buf)) > 0)
085                            out.write(buf, 0, len);
086            }
087    
088            File prepareWatchdog() throws IOException {
089                    File tempFile = File.createTempFile("forwatchdog", "test");
090                    tempFile.deleteOnExit();
091                    File watchdogFolder = new File(tempFile.getParentFile(), Watchdog.class.getPackage().getName());
092                    watchdogFolder.mkdir();
093    
094                    extractFromJar(Watchdog.class.getResource(Watchdog.class.getSimpleName() + ".class"), watchdogFolder);
095                    extractFromJar(Watchdog.class.getResource(WatchdogModule.javamailSpec + ".jar"), watchdogFolder);
096                    extractFromJar(Watchdog.class.getResource(WatchdogModule.javamailProvider + ".jar"), watchdogFolder);
097                    return watchdogFolder.getParentFile();
098            }
099    
100            /*
101             * (non-Javadoc)
102             * 
103             * @see org.tynamo.watchdog.services.WatchdogService#startWatchdog()
104             */
105            public synchronized void startWatchdog() throws IOException, URISyntaxException, InterruptedException {
106                    File watchdogFolder = prepareWatchdog();
107                    // whoamI gives us the watchdog jar when libs are loaded separately which is less than ideal
108                    // So don't use this at all, use appPackageName instead
109                    // String appName = whoAmI();
110                    // if (appName.isEmpty()) appName = "dev/exploded";
111    
112                    Process testJavaProcess = Runtime.getRuntime().exec("java -version");
113                    // TODO You could also read the output and make sure java is at least 1.5
114    
115                    try {
116                            if (testJavaProcess.waitFor() != 0) {
117                                    logger.error("Couldn't execute java in given environment - is java on PATH? Cannot start the watchdog");
118                                    return;
119                            }
120                    } catch (IllegalThreadStateException e) {
121                            logger.error("Testing java execution didn't return immediately. Report this issue to Tynamo.org");
122                    }
123    
124                    final String packageName = Watchdog.class.getPackage().getName();
125                    String[] args = new String[11];
126                    args[0] = "java";
127                    args[1] = "-D" + Watchdog.SEND_EMAIL + "=" + sendEmail;
128                    args[2] = "-D" + Watchdog.SMTP_HOST + "=" + smtpHost;
129                    args[3] = "-D" + Watchdog.SMTP_PORT + "=" + smtpPort;
130                    // With -Xms4m, at least 64-bit 1.6 jvm you get:
131                    // Error occurred during initialization of VM
132                    // Too small initial heap for new size specified
133                    args[4] = "-Xms8m";
134                    args[5] = "-Xmx16m";
135                    args[6] = "-XX:MaxPermSize=16m";
136                    args[7] = "-cp";
137                    args[8] = "." + File.pathSeparator + packageName + File.separator + WatchdogModule.javamailSpec + ".jar" + File.pathSeparator
138                                    + packageName + File.separator + WatchdogModule.javamailProvider + ".jar";
139                    args[9] = Watchdog.class.getName();
140                    args[10] = appPackageName;
141    
142                    StringBuilder command = new StringBuilder();
143                    for (String value : args) {
144                            command.append(value);
145                            command.append(" ");
146                    }
147    
148                    logger.info("Starting watchdog with command: " + command.toString());
149    
150                    // You *have* to start with inherited environment or set at least some of the most critical
151                    // environment variables manually (such as SystemRoot), otherwise I got
152                    // Unrecognized Windows Sockets error: 10106: errors
153                    watchdog = Runtime.getRuntime().exec(args, null, watchdogFolder);
154    
155                    (new StreamGobbler(watchdog.getErrorStream(), "WATCHDOG ERROR")).start();
156                    (new StreamGobbler(watchdog.getInputStream(), "WATCHDOG OUTPUT")).start();
157    
158                    watchdogOutputStream = watchdog.getOutputStream();
159    
160                    // Intentionally try to cause an exception to see if the process is still alive and kicking
161                    try {
162                            int exitCode = watchdog.exitValue();
163                            logger.error("Watchdog failed to start: the process exited immediately with exit code " + exitCode);
164                            return;
165                    } catch (IllegalThreadStateException e) {
166                            // Ignore, process hasn't exited
167                    }
168    
169                    watchdogLeash = new WatchdogLeash();
170                    watchdogLeash.start();
171    
172                    // TODO Make adding shutdownhook configurable
173                    Runtime.getRuntime().addShutdownHook(new Thread() {
174                            @Override
175                            public void run() {
176                                    try {
177                                            logger.warn("Dismissing watchdog before controlled JVM shutdown");
178                                            dismissWatchdog();
179                                    } catch (IOException e) {
180                                            logger.warn("Couldn't controllably dismiss the watchdog. Is watchdog still alive?");
181                                    }
182                                    try {
183                                            int exitCode = watchdog.exitValue();
184                                            logger.error("Watchdog has already exited with exit code " + exitCode);
185                                    } catch (IllegalThreadStateException e) {
186                                            // Ignore, can't do anything about the watchdog process
187                                    }
188                            }
189                    });
190    
191            }
192    
193            /*
194             * (non-Javadoc)
195             * 
196             * @see org.tynamo.watchdog.services.WatchdogService#dismissWatchdog()
197             */
198            public void dismissWatchdog() throws IOException {
199                    watchdogOutputStream.write(Watchdog.STOP_MESSAGE.getBytes());
200                    watchdogOutputStream.flush();
201            }
202    
203            /*
204             * (non-Javadoc)
205             * 
206             * @see org.tynamo.watchdog.services.WatchdogService#alarmWatchdog()
207             */
208            public void alarmWatchdog() throws IOException {
209                    watchdogOutputStream.close();
210            }
211    
212            class WatchdogLeash extends Thread {
213    
214                    public WatchdogLeash() {
215                            setDaemon(true);
216                    }
217    
218                    @Override
219                    public void run() {
220                            try {
221                                    while (true) {
222                                            watchdogOutputStream.write(0);
223                                            watchdogOutputStream.flush();
224                                            sleep(5000);
225                                    }
226                            } catch (IOException e) {
227                                    // Alarming the watchdog manually triggers the IOException
228                                    if (watchdogAlarmed) return;
229                                    logger.warn("IO exception occurred while communicating with the watchdog process. Was the watchdog killed? Releasing the leash");
230                                    try {
231                                            logger.info("Watchdog process exited with exit code " + watchdog.exitValue());
232                                    } catch (IllegalThreadStateException e1) {
233                                            // Ignore, process hasn't exited
234                                    }
235                            } catch (InterruptedException e) {
236                            }
237                    }
238            }
239    
240            /**
241             * Figures out the URL of <tt>war</tt>.
242             */
243            public String whoAmI() throws IOException, URISyntaxException {
244                    // There is no portable way to find where the locally cached copy
245                    // of war/jar is; JDK 6 is too smart. (See HUDSON-2326 - this code was adapted from Hudson.)
246                    try {
247                            URL classFile = Watchdog.class.getResource(Watchdog.class.getSimpleName() + ".class");
248                            JarFile jf = ((JarURLConnection) classFile.openConnection()).getJarFile();
249                            String fileName = jf.getName();
250                            fileName = fileName.substring(fileName.lastIndexOf("/") + 1);
251                    } catch (Exception x) {
252                            System.err.println("ZipFile.name trick did not work, using fallback: " + x);
253                    }
254                    URL classFile = Watchdog.class.getProtectionDomain().getCodeSource().getLocation();
255                    String fileName = classFile.toString();
256                    return fileName.substring(fileName.lastIndexOf("/") + 1);
257            }
258    }