OLD | NEW |
(Empty) | |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 package org.chromium.chrome.browser.crash; |
| 6 |
| 7 import android.content.Context; |
| 8 import android.content.SharedPreferences; |
| 9 import android.preference.PreferenceManager; |
| 10 import android.util.Log; |
| 11 |
| 12 import org.chromium.base.VisibleForTesting; |
| 13 import org.chromium.chrome.browser.preferences.privacy.CrashReportingPermissionM
anager; |
| 14 import org.chromium.chrome.browser.preferences.privacy.PrivacyPreferencesManager
; |
| 15 import org.chromium.chrome.browser.util.HttpURLConnectionFactory; |
| 16 import org.chromium.chrome.browser.util.HttpURLConnectionFactoryImpl; |
| 17 import org.chromium.chrome.browser.util.StreamUtil; |
| 18 |
| 19 import java.io.BufferedReader; |
| 20 import java.io.ByteArrayOutputStream; |
| 21 import java.io.File; |
| 22 import java.io.FileInputStream; |
| 23 import java.io.FileReader; |
| 24 import java.io.FileWriter; |
| 25 import java.io.IOException; |
| 26 import java.io.InputStream; |
| 27 import java.io.OutputStream; |
| 28 import java.net.HttpURLConnection; |
| 29 import java.util.Calendar; |
| 30 import java.util.Locale; |
| 31 import java.util.concurrent.Callable; |
| 32 |
| 33 /** |
| 34 * This class tries to upload a minidump to the crash server. |
| 35 * |
| 36 * It is implemented as a Callable<Boolean> and returns true on successful uploa
ds, |
| 37 * and false otherwise. |
| 38 */ |
| 39 public class MinidumpUploadCallable implements Callable<Boolean> { |
| 40 private static final String TAG = "MinidumpUploadCallable"; |
| 41 @VisibleForTesting protected static final int LOG_SIZE_LIMIT_BYTES = 1024 *
1024; // 1MB |
| 42 @VisibleForTesting protected static final int LOG_UPLOAD_LIMIT_PER_DAY = 5; |
| 43 |
| 44 @VisibleForTesting |
| 45 protected static final String PREF_LAST_UPLOAD_DAY = "crash_dump_last_upload
_day"; |
| 46 @VisibleForTesting protected static final String PREF_UPLOAD_COUNT = "crash_
dump_upload_count"; |
| 47 |
| 48 @VisibleForTesting |
| 49 protected static final String CRASH_URL_STRING = "https://clients2.google.co
m/cr/report"; |
| 50 |
| 51 @VisibleForTesting |
| 52 protected static final String CONTENT_TYPE_TMPL = "multipart/form-data; boun
dary=%s"; |
| 53 |
| 54 private final File mFileToUpload; |
| 55 private final File mLogfile; |
| 56 private final HttpURLConnectionFactory mHttpURLConnectionFactory; |
| 57 private final CrashReportingPermissionManager mPermManager; |
| 58 private final SharedPreferences mSharedPreferences; |
| 59 |
| 60 public MinidumpUploadCallable(File fileToUpload, File logfile, Context conte
xt) { |
| 61 this(fileToUpload, logfile, new HttpURLConnectionFactoryImpl(), |
| 62 PrivacyPreferencesManager.getInstance(context), |
| 63 PreferenceManager.getDefaultSharedPreferences(context)); |
| 64 } |
| 65 |
| 66 public MinidumpUploadCallable(File fileToUpload, File logfile, |
| 67 HttpURLConnectionFactory httpURLConnectionFactory, |
| 68 CrashReportingPermissionManager permManager, SharedPreferences share
dPreferences) { |
| 69 mFileToUpload = fileToUpload; |
| 70 mLogfile = logfile; |
| 71 mHttpURLConnectionFactory = httpURLConnectionFactory; |
| 72 mPermManager = permManager; |
| 73 mSharedPreferences = sharedPreferences; |
| 74 } |
| 75 |
| 76 @Override |
| 77 public Boolean call() { |
| 78 if (!mPermManager.isUploadPermitted()) { |
| 79 Log.i(TAG, "Minidump upload is not permitted"); |
| 80 return false; |
| 81 } |
| 82 |
| 83 boolean isLimited = mPermManager.isUploadLimited(); |
| 84 if (isLimited && !isUploadSizeAndFrequencyAllowed()) { |
| 85 Log.i(TAG, "Minidump cannot currently be uploaded due to constraints
"); |
| 86 return false; |
| 87 } |
| 88 |
| 89 HttpURLConnection connection = |
| 90 mHttpURLConnectionFactory.createHttpURLConnection(CRASH_URL_STRI
NG); |
| 91 if (connection == null) { |
| 92 return false; |
| 93 } |
| 94 |
| 95 FileInputStream minidumpInputStream = null; |
| 96 try { |
| 97 if (!configureConnectionForHttpPost(connection)) { |
| 98 return false; |
| 99 } |
| 100 minidumpInputStream = new FileInputStream(mFileToUpload); |
| 101 streamCopy(minidumpInputStream, connection.getOutputStream()); |
| 102 boolean status = handleExecutionResponse(connection); |
| 103 |
| 104 if (isLimited) updateUploadPrefs(); |
| 105 return status; |
| 106 } catch (IOException e) { |
| 107 // For now just log the stack trace. |
| 108 Log.w(TAG, "Error while uploading " + mFileToUpload.getName(), e); |
| 109 return false; |
| 110 } finally { |
| 111 connection.disconnect(); |
| 112 |
| 113 if (minidumpInputStream != null) { |
| 114 StreamUtil.closeQuietly(minidumpInputStream); |
| 115 } |
| 116 } |
| 117 } |
| 118 |
| 119 /** |
| 120 * Configures a HttpURLConnection to send a HTTP POST request for uploading
the minidump. |
| 121 * |
| 122 * This also reads the content-type from the minidump file. |
| 123 * |
| 124 * @param connection the HttpURLConnection to configure |
| 125 * @return true if successful. |
| 126 * @throws IOException |
| 127 */ |
| 128 private boolean configureConnectionForHttpPost(HttpURLConnection connection) |
| 129 throws IOException { |
| 130 // Read the boundary which we need for the content type. |
| 131 String boundary = readBoundary(); |
| 132 if (boundary == null) { |
| 133 return false; |
| 134 } |
| 135 |
| 136 connection.setDoOutput(true); |
| 137 connection.setRequestProperty("Connection", "Keep-Alive"); |
| 138 connection.setRequestProperty("Content-Type", String.format(CONTENT_TYPE
_TMPL, boundary)); |
| 139 return true; |
| 140 } |
| 141 |
| 142 /** |
| 143 * Reads the HTTP response and cleans up successful uploads. |
| 144 * |
| 145 * @param connection the connection to read the response from |
| 146 * @return true if the upload was successful, false otherwise. |
| 147 * @throws IOException |
| 148 */ |
| 149 private Boolean handleExecutionResponse(HttpURLConnection connection) throws
IOException { |
| 150 int responseCode = connection.getResponseCode(); |
| 151 if (isSuccessful(responseCode)) { |
| 152 String responseContent = getResponseContentAsString(connection); |
| 153 // The crash server returns the crash ID. |
| 154 String id = responseContent != null ? responseContent : "unknown"; |
| 155 Log.i(TAG, "Minidump " + mFileToUpload.getName() + " uploaded succes
sfully, id: " + id); |
| 156 |
| 157 // TODO(acleung): MinidumpUploadService is in charge of renaming whi
le this class is |
| 158 // in charge of deleting. We should move all the file system operati
ons into |
| 159 // MinidumpUploadService instead. |
| 160 cleanupMinidumpFile(); |
| 161 |
| 162 try { |
| 163 appendUploadedEntryToLog(id); |
| 164 } catch (IOException ioe) { |
| 165 Log.e(TAG, "Fail to write uploaded entry to log file"); |
| 166 } |
| 167 return true; |
| 168 } else { |
| 169 // Log the results of the upload. Note that periodic upload failures
aren't bad |
| 170 // because we will need to throttle uploads in the future anyway. |
| 171 String msg = String.format(Locale.US, |
| 172 "Failed to upload %s with code: %d (%s).", |
| 173 mFileToUpload.getName(), responseCode, connection.getRespons
eMessage()); |
| 174 Log.i(TAG, msg); |
| 175 |
| 176 // TODO(acleung): The return status informs us about why an upload m
ight be |
| 177 // rejected. The next logical step is to put the reasons in an UMA h
istogram. |
| 178 return false; |
| 179 } |
| 180 } |
| 181 |
| 182 /** |
| 183 * Records the upload entry to a log file |
| 184 * similar to what is done in chrome/app/breakpad_linux.cc |
| 185 * |
| 186 * @param id The crash ID return from the server. |
| 187 */ |
| 188 private void appendUploadedEntryToLog(String id) throws IOException { |
| 189 FileWriter writer = new FileWriter(mLogfile, /* Appending */ true); |
| 190 |
| 191 // The log entries are formated like so: |
| 192 // seconds_since_epoch,crash_id |
| 193 StringBuilder sb = new StringBuilder(); |
| 194 sb.append(System.currentTimeMillis() / 1000); |
| 195 sb.append(","); |
| 196 sb.append(id); |
| 197 sb.append('\n'); |
| 198 |
| 199 try { |
| 200 // Since we are writing one line at a time, lets forget about Buffer
Writers. |
| 201 writer.write(sb.toString()); |
| 202 } finally { |
| 203 writer.close(); |
| 204 } |
| 205 } |
| 206 |
| 207 /** |
| 208 * Get the boundary from the file, we need it for the content-type. |
| 209 * |
| 210 * @return the boundary if found, else null. |
| 211 * @throws IOException |
| 212 */ |
| 213 private String readBoundary() throws IOException { |
| 214 BufferedReader reader = new BufferedReader(new FileReader(mFileToUpload)
); |
| 215 String boundary = reader.readLine(); |
| 216 reader.close(); |
| 217 if (boundary == null || boundary.trim().isEmpty()) { |
| 218 Log.e(TAG, "Ignoring invalid crash dump: '" + mFileToUpload + "'"); |
| 219 return null; |
| 220 } |
| 221 boundary = boundary.trim(); |
| 222 if (!boundary.startsWith("--") || boundary.length() < 10) { |
| 223 Log.e(TAG, "Ignoring invalidly bound crash dump: '" + mFileToUpload
+ "'"); |
| 224 return null; |
| 225 } |
| 226 boundary = boundary.substring(2); // Remove the initial -- |
| 227 return boundary; |
| 228 } |
| 229 |
| 230 /** |
| 231 * Mark file we just uploaded for cleanup later. |
| 232 * |
| 233 * We do not immediately delete the file for testing reasons, |
| 234 * but if marking the file fails, we do delete it right away. |
| 235 */ |
| 236 private void cleanupMinidumpFile() { |
| 237 if (!CrashFileManager.tryMarkAsUploaded(mFileToUpload)) { |
| 238 Log.w(TAG, "Unable to mark " + mFileToUpload + " as uploaded."); |
| 239 if (!mFileToUpload.delete()) { |
| 240 Log.w(TAG, "Cannot delete " + mFileToUpload); |
| 241 } |
| 242 } |
| 243 } |
| 244 |
| 245 /** |
| 246 * Checks whether crash upload satisfies the size and frequency constraints. |
| 247 * |
| 248 * @return whether crash upload satisfies the size and frequency constraints
. |
| 249 */ |
| 250 private boolean isUploadSizeAndFrequencyAllowed() { |
| 251 // Check upload size constraint. |
| 252 if (mFileToUpload.length() > LOG_SIZE_LIMIT_BYTES) return false; |
| 253 |
| 254 // Check upload frequency constraint. |
| 255 // If pref doesn't exist then in both cases default value 0 will be retu
rned and comparison |
| 256 // always would be true. |
| 257 if (mSharedPreferences.getInt(PREF_LAST_UPLOAD_DAY, 0) != getCurrentDay(
)) return true; |
| 258 return mSharedPreferences.getInt(PREF_UPLOAD_COUNT, 0) < LOG_UPLOAD_LIMI
T_PER_DAY; |
| 259 } |
| 260 |
| 261 /** |
| 262 * Updates preferences used for determining crash upload constraints. |
| 263 */ |
| 264 private void updateUploadPrefs() { |
| 265 SharedPreferences.Editor editor = mSharedPreferences.edit(); |
| 266 |
| 267 int day = getCurrentDay(); |
| 268 int prevCount = mSharedPreferences.getInt(PREF_UPLOAD_COUNT, 0); |
| 269 if (mSharedPreferences.getInt(PREF_LAST_UPLOAD_DAY, 0) != day) { |
| 270 prevCount = 0; |
| 271 } |
| 272 editor.putInt(PREF_LAST_UPLOAD_DAY, day).putInt(PREF_UPLOAD_COUNT, prevC
ount + 1).apply(); |
| 273 } |
| 274 |
| 275 /** |
| 276 * Returns number of current day in a year starting from 1. Overridden in te
sts. |
| 277 */ |
| 278 protected int getCurrentDay() { |
| 279 return Calendar.getInstance().get(Calendar.YEAR) * 365 |
| 280 + Calendar.getInstance().get(Calendar.DAY_OF_YEAR); |
| 281 } |
| 282 |
| 283 /** |
| 284 * Returns whether the response code indicates a successful HTTP request. |
| 285 * |
| 286 * @param responseCode the response code |
| 287 * @return true if response code indicates success, false otherwise. |
| 288 */ |
| 289 private static boolean isSuccessful(int responseCode) { |
| 290 return responseCode == 200 || responseCode == 201 || responseCode == 202
; |
| 291 } |
| 292 |
| 293 /** |
| 294 * Reads the response from |connection| as a String. |
| 295 * |
| 296 * @param connection the connection to read the response from. |
| 297 * @return the content of the response. |
| 298 * @throws IOException |
| 299 */ |
| 300 private static String getResponseContentAsString(HttpURLConnection connectio
n) |
| 301 throws IOException { |
| 302 String responseContent = null; |
| 303 ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| 304 streamCopy(connection.getInputStream(), baos); |
| 305 if (baos.size() > 0) { |
| 306 responseContent = baos.toString(); |
| 307 } |
| 308 return responseContent; |
| 309 } |
| 310 |
| 311 /** |
| 312 * Copies all available data from |inStream| to |outStream|. Closes both |
| 313 * streams when done. |
| 314 * |
| 315 * @param inStream the stream to read |
| 316 * @param outStream the stream to write to |
| 317 * @throws IOException |
| 318 */ |
| 319 private static void streamCopy(InputStream inStream, |
| 320 OutputStream outStream) throws IOException { |
| 321 byte[] temp = new byte[4096]; |
| 322 int bytesRead = inStream.read(temp); |
| 323 while (bytesRead >= 0) { |
| 324 outStream.write(temp, 0, bytesRead); |
| 325 bytesRead = inStream.read(temp); |
| 326 } |
| 327 inStream.close(); |
| 328 outStream.close(); |
| 329 } |
| 330 } |
OLD | NEW |