mobile/android/base/background/datareporting/TelemetryRecorder.java

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 package org.mozilla.gecko.background.datareporting;
     7 import java.io.BufferedOutputStream;
     8 import java.io.File;
     9 import java.io.FileOutputStream;
    10 import java.io.IOException;
    11 import java.io.OutputStream;
    12 import java.io.UnsupportedEncodingException;
    13 import java.security.MessageDigest;
    14 import java.security.NoSuchAlgorithmException;
    16 import org.json.JSONObject;
    17 import org.mozilla.gecko.background.common.log.Logger;
    19 import android.util.Base64;
    21 /**
    22  * Writes telemetry ping to file.
    23  *
    24  * Also creates and updates a SHA-256 checksum for the payload to be included in the ping
    25  * file.
    26  *
    27  * A saved telemetry ping file consists of JSON in the following format,
    28  *   {
    29  *     "slug": "<uuid-string>",
    30  *     "payload": "<escaped-json-data-string>",
    31  *     "checksum": "<base64-sha-256-string>"
    32  *   }
    33  *
    34  * This class writes first to a temporary file and then, after finishing the contents of the ping,
    35  * moves that to the file specified by the caller. This is to avoid uploads of partially written
    36  * ping files.
    37  *
    38  * The API provided by this class:
    39  * startPingFile() - opens stream to a tmp File in the Android cache directory and writes the slug header
    40  * appendPayload(String payloadContent) - appends to the payload of the ping and updates the checksum
    41  * finishPingFile() - writes the checksum to the tmp file and moves it to the File specified by the caller.
    42  *
    43  * In the case of errors, we try to close the stream and File.
    44  */
    45 public class TelemetryRecorder {
    46   private final String LOG_TAG = "TelemetryRecorder";
    48   private final File parentDir;
    49   private final String filename;
    51   private File tmpFile;
    52   private File destFile;
    53   private File cacheDir;
    55   private OutputStream  outputStream;
    56   private MessageDigest checksum;
    57   private String        base64Checksum;
    59   /**
    60    * Charset to use for writing pings; default is us-ascii.
    61    *
    62    * When telemetry calculates the checksum for the ping file, it lossily
    63    * converts utf-16 to ascii. Therefore we have to treat characters in the
    64    * traces as ascii rather than say utf-8. Otherwise we will get a "wrong"
    65    * checksum.
    66    */
    67   private String charset = "us-ascii";
    69   /**
    70    * Override blockSize in constructor if desired.
    71    * Default block size is that of BufferedOutputStream.
    72    */
    73   private int blockSize = 0;
    75   /**
    76    * Constructs a TelemetryRecorder for writing a ping file. A temporary file will be written first,
    77    * and then moved to the destination file location specified by the caller.
    78    *
    79    * The directory for writing the temporary file is highly suggested to be the Android internal cache directory,
    80    * fetched by <code>context.getCacheDir()</code>
    81    *
    82    * If the destination file already exists, it will be deleted and overwritten.
    83    *
    84    * Default charset: "us-ascii"
    85    * Default block size: uses constructor default of 8192 bytes (see javadocs for
    86    *                     <code>BufferedOutputStream</code>
    87    * @param parentPath
    88    *        path of parent directory of ping file to be written
    89    * @param cacheDir
    90    *        path of cache directory for writing temporary files.
    91    * @param filename
    92    *        name of ping file to be written
    93    */
    94   public TelemetryRecorder(File parentDir, File cacheDir, String filename) {
    95     if (!parentDir.isDirectory()) {
    96       throw new IllegalArgumentException("Expecting directory, got non-directory File instead.");
    97     }
    98     this.parentDir = parentDir;
    99     this.filename = filename;
   100     this.cacheDir = cacheDir;
   101   }
   103   public TelemetryRecorder(File parentDir, File cacheDir, String filename, String charset) {
   104     this(parentDir, cacheDir, filename);
   105     this.charset = charset;
   106   }
   108   public TelemetryRecorder(File parentDir, File cacheDir, String filename, String charset, int blockSize) {
   109     this(parentDir, cacheDir, filename, charset);
   110     this.blockSize = blockSize;
   111   }
   113   /**
   114    * Start the temporary ping file and write the "slug" header and payload key, of the
   115    * format:
   116    *
   117    *     { "slug": "< filename >", "payload":
   118    *
   119    * @throws Exception
   120    *           Checked exceptions <code>NoSuchAlgorithmException</code>,
   121    *           <code>UnsupportedEncodingException</code>, or
   122    *           <code>IOException</code> and unchecked exception that
   123    *           are rethrown to caller
   124    */
   125   public void startPingFile() throws Exception {
   127     // Open stream to temporary file for writing.
   128     try {
   129       tmpFile = File.createTempFile(filename, "tmp", cacheDir);
   130     } catch (IOException e) {
   131       // Try to create the temporary file in the ping directory.
   132       tmpFile = new File(parentDir, filename + ".tmp");
   133       try {
   134         tmpFile.createNewFile();
   135       } catch (IOException e1) {
   136         cleanUpAndRethrow("Failed to create tmp file in temp directory or ping directory.", e1);
   137       }
   138     }
   140     try {
   141       if (blockSize > 0) {
   142         outputStream = new BufferedOutputStream(new FileOutputStream(tmpFile), blockSize);
   143       } else {
   144         outputStream = new BufferedOutputStream(new FileOutputStream(tmpFile));
   145       }
   147       // Create checksum for ping.
   148       checksum = MessageDigest.getInstance("SHA-256");
   150       // Write ping header.
   151       byte[] header = makePingHeader(filename);
   152       outputStream.write(header);
   153       Logger.debug(LOG_TAG, "Wrote " + header.length + " header bytes.");
   155     } catch (NoSuchAlgorithmException e) {
   156       cleanUpAndRethrow("Error creating checksum digest", e);
   157     } catch (UnsupportedEncodingException e) {
   158       cleanUpAndRethrow("Error writing header", e);
   159     } catch (IOException e) {
   160       cleanUpAndRethrow("Error writing to stream", e);
   161     }
   162   }
   164   private byte[] makePingHeader(String slug)
   165       throws UnsupportedEncodingException {
   166     return ("{\"slug\":" + JSONObject.quote(slug) + "," + "\"payload\":\"")
   167         .getBytes(charset);
   168   }
   170   /**
   171    * Append payloadContent to ping file and update the checksum.
   172    *
   173    * @param payloadContent
   174    *          String content to be written
   175    * @return number of bytes written, or -1 if writing failed
   176    * @throws Exception
   177    *           Checked exceptions <code>UnsupportedEncodingException</code> or
   178    *           <code>IOException</code> and unchecked exception that
   179    *           are rethrown to caller
   180    */
   181   public int appendPayload(String payloadContent) throws Exception {
   182     if (payloadContent == null) {
   183       cleanUpAndRethrow("Payload is null", new Exception());
   184       return -1;
   185     }
   187     try {
   188       byte[] payloadBytes = payloadContent.getBytes(charset);
   189       // If we run into an error, we'll throw and abort, so checksum won't be stale.
   190       checksum.update(payloadBytes);
   192       byte[] quotedPayloadBytes = JSONObject.quote(payloadContent).getBytes(charset);
   194       // First and last bytes are quotes inserted by JSONObject.quote; discard
   195       // them.
   196       int numBytes = quotedPayloadBytes.length - 2;
   197       outputStream.write(quotedPayloadBytes, 1, numBytes);
   198       return numBytes;
   200     } catch (UnsupportedEncodingException e) {
   201       cleanUpAndRethrow("Error encoding payload", e);
   202       return -1;
   203     } catch (IOException e) {
   204       cleanUpAndRethrow("Error writing to stream", e);
   205       return -1;
   206     }
   207   }
   209   /**
   210    * Add the checksum of the payload to the ping file and close the stream.
   211    *
   212    * @throws Exception
   213    *          Checked exceptions <code>UnsupportedEncodingException</code> or
   214    *          <code>IOException</code> and unchecked exception that
   215    *          are rethrown to caller
   216    */
   217   public void finishPingFile() throws Exception {
   218     try {
   219       byte[] footer = makePingFooter(checksum);
   220       outputStream.write(footer);
   221       // We're done writing, so force the stream to flush the buffer.
   222       outputStream.flush();
   223       Logger.debug(LOG_TAG, "Wrote " + footer.length + " footer bytes.");
   224     } catch (UnsupportedEncodingException e) {
   225       cleanUpAndRethrow("Checksum encoding exception", e);
   226     } catch (IOException e) {
   227       cleanUpAndRethrow("Error writing footer to stream", e);
   228     } finally {
   229       try {
   230         outputStream.close();
   231       } catch (IOException e) {
   232         // Failed to close, nothing we can do except discard the reference to the stream.
   233         outputStream = null;
   234       }
   235     }
   237     // Move temp file to destination specified by caller.
   238     try {
   239       File destFile = new File(parentDir, filename);
   240       // Delete file if it exists - docs state that rename may fail if the File already exists.
   241       if (destFile.exists()) {
   242         destFile.delete();
   243       }
   244       boolean result = tmpFile.renameTo(destFile);
   245       if (!result) {
   246         throw new IOException("Could not move tmp file to destination.");
   247       }
   248     } finally {
   249       cleanUp();
   250     }
   251   }
   253   private byte[] makePingFooter(MessageDigest checksum)
   254       throws UnsupportedEncodingException {
   255     base64Checksum = Base64.encodeToString(checksum.digest(), Base64.NO_WRAP);
   256     return ("\",\"checksum\":" + JSONObject.quote(base64Checksum) + "}")
   257         .getBytes(charset);
   258   }
   260   /**
   261    * Get final digested Base64 checksum.
   262    *
   263    * @return String checksum if it has been calculated, null otherwise.
   264    */
   265   protected String getFinalChecksum() {
   266     return base64Checksum;
   267   }
   269   public String getCharset() {
   270     return this.charset;
   271   }
   273   /**
   274    * Clean up checksum and delete the temporary file.
   275    */
   276   private void cleanUp() {
   277     // Discard checksum.
   278     checksum.reset();
   280     // Clean up files.
   281     if (tmpFile != null && tmpFile.exists()) {
   282       tmpFile.delete();
   283     }
   284     tmpFile = null;
   285   }
   287   /**
   288    * Log message and error and clean up, then rethrow exception to caller.
   289    *
   290    * @param message
   291    *          Error message
   292    * @param e
   293    *          Exception
   294    *
   295    * @throws Exception
   296    *           Exception to be rethrown to caller
   297    */
   298   private void cleanUpAndRethrow(String message, Exception e) throws Exception {
   299     Logger.error(LOG_TAG, message, e);
   300     cleanUp();
   302     if (outputStream != null) {
   303       try {
   304         outputStream.close();
   305       } catch (IOException exception) {
   306         // Failed to close stream; nothing we can do, and we're aborting anyways.
   307       }
   308     }
   310     if (destFile != null && destFile.exists()) {
   311       destFile.delete();
   312     }
   313     // Rethrow the exception.
   314     throw e;
   315   }
   316 }

mercurial