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 }