1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tradefed.log;
18 
19 import com.android.tradefed.config.Option;
20 import com.android.tradefed.config.Option.Importance;
21 import com.android.tradefed.config.OptionClass;
22 import com.android.tradefed.log.LogUtil.CLog;
23 import com.android.tradefed.util.Email;
24 import com.android.tradefed.util.IEmail;
25 import com.android.tradefed.util.IEmail.Message;
26 
27 import java.io.IOException;
28 import java.io.PrintWriter;
29 import java.io.StringWriter;
30 import java.net.InetAddress;
31 import java.net.UnknownHostException;
32 import java.util.Collection;
33 import java.util.HashSet;
34 import java.util.Iterator;
35 
36 /**
37  * A simple handler class that sends an email to interested people when a WTF
38  * (What a Terrible Failure) error occurs within a Trade Federation instance.
39  */
40 @OptionClass(alias = "wtf-email-handler")
41 public class TerribleFailureEmailHandler implements ITerribleFailureHandler {
42     private static final String DEFAULT_SUBJECT_PREFIX = "WTF happened to tradefed";
43 
44     @Option(name = "sender",
45             description = "The envelope-sender address to use for the messages.",
46             importance = Importance.IF_UNSET)
47     private String mSender = null;
48 
49     @Option(name = "destination",
50             description = "One or more destination addresses.",
51             importance = Importance.IF_UNSET)
52     private Collection<String> mDestinations = new HashSet<String>();
53 
54     @Option(name = "subject-prefix",
55             description = "The prefix to be added to the beginning of the email subject.")
56     private String mSubjectPrefix = DEFAULT_SUBJECT_PREFIX;
57 
58     @Option(name = "min-email-interval",
59             description = "The minimum interval between emails in ms. " +
60                     "If a new WTF happens within this interval from the previous one, " +
61                     "it will be ignored.")
62     private long mMinEmailInterval = 5 * 60 * 1000;
63 
64     private IEmail mMailer;
65     private long mLastEmailSentTime = 0;
66 
67     /**
68      * Create a {@link TerribleFailureEmailHandler}
69      */
TerribleFailureEmailHandler()70     public TerribleFailureEmailHandler() {
71         this(new Email());
72     }
73 
74     /**
75      * Create a {@link TerribleFailureEmailHandler} with a custom {@link IEmail}
76      * instance to use.
77      * <p/>
78      * Exposed for unit testing.
79      *
80      * @param mailer the {@link IEmail} instance to use.
81      */
TerribleFailureEmailHandler(IEmail mailer)82     protected TerribleFailureEmailHandler(IEmail mailer) {
83         mMailer = mailer;
84     }
85 
86     /**
87      * Adds an email destination address.
88      *
89      * @param dest
90      */
addDestination(String dest)91     public void addDestination(String dest) {
92         mDestinations.add(dest);
93     }
94 
95     /**
96      * Sets the email sender address.
97      *
98      * @param sender
99      */
setSender(String sender)100     public void setSender(String sender) {
101         mSender = sender;
102     }
103 
104     /**
105      * Sets the minimum email interval.
106      *
107      * @param interval
108      */
setMinEmailInterval(long interval)109     public void setMinEmailInterval(long interval) {
110         mMinEmailInterval = interval;
111     }
112 
113     /**
114      * Gets the local host name of the machine.
115      *
116      * @return the name of the host machine, or "unknown host" if unknown
117      */
getLocalHostName()118     protected String getLocalHostName() {
119         try {
120             return InetAddress.getLocalHost().getHostName();
121         } catch (UnknownHostException e) {
122             CLog.e(e);
123             return "unknown host";
124         }
125     }
126 
127     /**
128      * Gets the current time in milliseconds.
129      */
getCurrentTimeMillis()130     protected long getCurrentTimeMillis() {
131         return System.currentTimeMillis();
132     }
133 
134     /**
135      * A method to generate the subject for email reports.
136      * The subject will be formatted as:
137      *     "<subject-prefix> on <local-host-name>"
138      *
139      * @return A {@link String} containing the subject to use for an email report
140      */
generateEmailSubject()141     protected String generateEmailSubject() {
142         final StringBuilder subj = new StringBuilder();
143 
144         subj.append(mSubjectPrefix);
145         subj.append(" on ");
146         subj.append(getLocalHostName());
147 
148         return subj.toString();
149     }
150 
151     /**
152      * A method to generate the body for WTF email reports.
153      *
154      * @param message summary of the terrible failure
155      * @param cause throwable containing stack trace information
156      * @return A {@link String} containing the body to use for an email report
157      */
generateEmailBody(String message, Throwable cause)158     protected String generateEmailBody(String message, Throwable cause) {
159         StringBuilder bodyBuilder = new StringBuilder();
160 
161         bodyBuilder.append("host: ");
162         bodyBuilder.append(getLocalHostName());
163         bodyBuilder.append("\n\n");
164 
165         bodyBuilder.append("What a Terrible Failure!  ");
166         bodyBuilder.append(message);
167         bodyBuilder.append("\n\n");
168 
169         if (cause != null) {
170             bodyBuilder.append("Invocation failed: ");
171             bodyBuilder.append(getStackTraceString(cause));
172             bodyBuilder.append("\n\n");
173         }
174 
175         return bodyBuilder.toString();
176     }
177 
178     /**
179      * Generates a new email message based on the attributes already gathered
180      * (subject, sender, destinations), as well as the description and cause (Optional)
181      *
182      * @param description Summary of the terrible failure
183      * @param cause (Optional) Throwable that includes stack trace info
184      * @return Message object with all email attributes populated
185      */
generateEmailMessage(String description, Throwable cause)186     protected Message generateEmailMessage(String description, Throwable cause) {
187         Message msg = new Message();
188         msg.setSender(mSender);
189         msg.setSubject(generateEmailSubject());
190         msg.setBody(generateEmailBody(description, cause));
191         msg.setHtml(false);
192         Iterator<String> toAddress = mDestinations.iterator();
193         while (toAddress.hasNext()) {
194             msg.addTo(toAddress.next());
195         }
196         return msg;
197     }
198 
199     /**
200      * {@inheritDoc}
201      */
202     @Override
onTerribleFailure(String description, Throwable cause)203     public boolean onTerribleFailure(String description, Throwable cause) {
204         if (mDestinations.isEmpty()) {
205             CLog.e("Failed to send email because no destination addresses were set.");
206             return false;
207         }
208 
209         final long now = getCurrentTimeMillis();
210         if (0 < mMinEmailInterval && now - mLastEmailSentTime < mMinEmailInterval) {
211             // TODO: consider queuing up skipped failures and send it later.
212             CLog.w("Skipped to send %s email: email interval %dms < %dms", DEFAULT_SUBJECT_PREFIX,
213                     now - mLastEmailSentTime, mMinEmailInterval);
214             return false;
215         }
216 
217         Message msg = generateEmailMessage(description, cause);
218 
219         try {
220             mMailer.send(msg);
221         } catch (IllegalArgumentException e) {
222             CLog.e("Failed to send %s email", DEFAULT_SUBJECT_PREFIX);
223             CLog.e(e);
224             return false;
225         } catch (IOException e) {
226             CLog.e("Failed to send %s email", DEFAULT_SUBJECT_PREFIX);
227             CLog.e(e);
228             return false;
229         }
230 
231         mLastEmailSentTime = now;
232         return true;
233     }
234 
235     /**
236      * A helper method that parses the stack trace string out of the throwable.
237      *
238      * @param t contains the stack trace information
239      * @return A {@link String} containing the stack trace of the throwable.
240      */
getStackTraceString(Throwable t)241     private static String getStackTraceString(Throwable t) {
242         if (t == null)
243             return "";
244 
245         StringWriter sw = new StringWriter();
246         PrintWriter pw = new PrintWriter(sw);
247         t.printStackTrace(pw);
248         pw.flush();
249         return sw.toString();
250     }
251 }
252