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