Bug 955292 - Read/write chat logs asynchronously. r=aleth, florian draft
authorNihanth Subramanya <nhnt11@gmail.com>
Wed, 18 Jun 2014 16:40:01 +0530
changeset 24551 0a424d9abac4ee07436717749a5fcfef57dd7d90
parent 24550 445db1ef062daa66a3fc45bb27ca42588308e1a7
child 24552 9778ca592b148f338bb8b5509e38c727db3ed713
push id1511
push usernhnt11@gmail.com
push dateWed, 18 Jun 2014 11:17:22 +0000
treeherdertry-comm-central@d06f8552d274 [default view] [failures only]
reviewersaleth, florian
bugs955292
Bug 955292 - Read/write chat logs asynchronously. r=aleth, florian
chat/components/public/imILogger.idl
chat/components/public/prplIConversation.idl
chat/components/public/prplIMessage.idl
chat/components/src/logger.js
--- a/chat/components/public/imILogger.idl
+++ b/chat/components/public/imILogger.idl
@@ -12,16 +12,17 @@ interface imIBuddy;
 interface imIContact;
 interface prplIConversation;
 interface prplIMessage;
 
 [scriptable, uuid(5bc06f3b-33a1-412b-a4d8-4fc7ba4c962b)]
 interface imILogConversation: nsISupports {
   readonly attribute AUTF8String title;
   readonly attribute AUTF8String name;
+  // Value in microseconds.
   readonly attribute PRTime startDate;
 
   // Simplified account implementation:
   //  - alias will always be empty
   //  - name (always the normalizedName)
   //  - statusInfo will return Services.core.globalUserStatus
   //  - protocol will only contain a "name" attribute, with the prpl's normalized name.
   // Other methods/attributes aren't implemented.
@@ -34,40 +35,48 @@ interface imILogConversation: nsISupport
                    [retval, array, size_is(messageCount)] out prplIMessage messages);
 
   // Callers that process the messages asynchronously should use the enumerator
   // instead of the array version of the getMessages* methods to avoid paying
   // up front the cost of xpconnect wrapping all message objects.
   nsISimpleEnumerator getMessagesEnumerator([optional] out unsigned long messageCount);
 };
 
-[scriptable, uuid(164ff6c3-ca64-4880-b8f3-67eb1817955f)]
+[scriptable, uuid(27712ece-ad2c-4504-87d5-9e2c16d40fef)]
 interface imILog: nsISupports {
   readonly attribute AUTF8String path;
+  // Value in seconds.
   readonly attribute PRTime time;
   readonly attribute AUTF8String format;
-  // Will return null if the log format isn't json.
-  imILogConversation getConversation();
+  // Returns a promise that resolves to an imILogConversation instance, or null
+  // if the log format isn't JSON.
+  jsval getConversation();
 };
 
-[scriptable, uuid(327ba58c-ee9c-4d1c-9216-fd505c45a3e0)]
+[scriptable, uuid(b9d5701a-df53-4e0e-99b7-706e0118e075)]
 interface imILogger: nsISupports {
-  imILog getLogFromFile(in nsIFile aFile, [optional] in boolean aGroupByDay);
-  nsIFile getLogFileForOngoingConversation(in prplIConversation aConversation);
+  // Returns a promise that resolves to an imILog instance.
+  jsval getLogFromFile(in AUTF8String aFilePath, [optional] in boolean aGroupByDay);
+  // Returns a promise that resolves to the log file path if a log writer
+  // exists for the conversation, or null otherwise. The promise resolves
+  // after any pending I/O operations on the file complete.
+  jsval getLogPathForConversation(in prplIConversation aConversation);
+
+  // Below methods return promises that resolve to nsISimpleEnumerator instances.
 
   // Get logs for a username that may not be in the contact list.
-  nsISimpleEnumerator getLogsForAccountAndName(in imIAccount aAccount,
-                                               in string aNormalizedName,
-                                               [optional] in boolean aGroupByDay);
+  jsval getLogsForAccountAndName(in imIAccount aAccount,
+                                 in AUTF8String aNormalizedName,
+                                 [optional] in boolean aGroupByDay);
 
-  nsISimpleEnumerator getLogsForAccountBuddy(in imIAccountBuddy aAccountBuddy,
-                                             [optional] in boolean aGroupByDay);
-  nsISimpleEnumerator getLogsForBuddy(in imIBuddy aBuddy,
-                                      [optional] in boolean aGroupByDay);
-  nsISimpleEnumerator getLogsForContact(in imIContact aContact,
-                                        [optional] in boolean aGroupByDay);
+  jsval getLogsForAccountBuddy(in imIAccountBuddy aAccountBuddy,
+                               [optional] in boolean aGroupByDay);
+  jsval getLogsForBuddy(in imIBuddy aBuddy,
+                        [optional] in boolean aGroupByDay);
+  jsval getLogsForContact(in imIContact aContact,
+                          [optional] in boolean aGroupByDay);
 
-  nsISimpleEnumerator getLogsForConversation(in prplIConversation aConversation,
-                                             [optional] in boolean aGroupByDay);
-  nsISimpleEnumerator getSystemLogsForAccount(in imIAccount aAccount);
-  nsISimpleEnumerator getSimilarLogs(in imILog aLog,
-                                     [optional] in boolean aGroupByDay);
+  jsval getLogsForConversation(in prplIConversation aConversation,
+                               [optional] in boolean aGroupByDay);
+  jsval getSystemLogsForAccount(in imIAccount aAccount);
+  jsval getSimilarLogs(in imILog aLog,
+                       [optional] in boolean aGroupByDay);
 };
--- a/chat/components/public/prplIConversation.idl
+++ b/chat/components/public/prplIConversation.idl
@@ -30,17 +30,17 @@ interface prplIConversation: nsISupports
 
   /* A name that can be used to check for duplicates and is the basis
      for the directory name for log storage. */
   readonly attribute AUTF8String normalizedName;
 
   /* The title of the conversation, typically localized */
   readonly attribute AUTF8String title;
 
-  /* The time and date of the conversation's creation */
+  /* The time and date of the conversation's creation, in microseconds */
   readonly attribute PRTime startDate;
   /* Unique identifier of the conversation */
   /* Setable only once by purpleCoreService while calling addConversation. */
            attribute unsigned long id;
 
   /* Send a message in the conversation */
   void sendMsg(in AUTF8String aMsg);
 
--- a/chat/components/public/prplIMessage.idl
+++ b/chat/components/public/prplIMessage.idl
@@ -24,16 +24,17 @@ interface prplIMessage: nsISupports {
      messages of a conversation, not across all messages created
      during the execution of the application. */
   readonly attribute unsigned long id;
   readonly attribute AUTF8String who;
   readonly attribute AUTF8String alias;
   readonly attribute AUTF8String originalMessage;
            attribute AUTF8String message;
   readonly attribute AUTF8String iconURL;
+  // Value in seconds.
   readonly attribute PRTime time;
   readonly attribute prplIConversation conversation;
 
   /* Holds the sender color for Chats.
      Empty string by default, it is set by the conversation binding. */
   attribute AUTF8String color;
 
   /*  PURPLE_MESSAGE_SEND        = 0x0001, /**< Outgoing message. */
--- a/chat/components/src/logger.js
+++ b/chat/components/src/logger.js
@@ -4,74 +4,118 @@
 
 const {classes: Cc, interfaces: Ci, utils: Cu, Constructor: CC} = Components;
 
 Cu.import("resource:///modules/hiddenWindow.jsm");
 Cu.import("resource:///modules/imServices.jsm");
 Cu.import("resource:///modules/imXPCOMUtils.jsm");
 Cu.import("resource:///modules/jsProtoHelper.jsm");
 
-XPCOMUtils.defineLazyGetter(this, "logDir", function() {
-  let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
-  file.append("logs");
-  return file;
-});
-
-XPCOMUtils.defineLazyGetter(this, "bundle", function()
-  Services.strings.createBundle("chrome://chat/locale/logger.properties")
-);
+XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
 
-const FileInputStream = CC("@mozilla.org/network/file-input-stream;1",
-                           "nsIFileInputStream",
-                           "init");
-const ConverterInputStream = CC("@mozilla.org/intl/converter-input-stream;1",
-                                "nsIConverterInputStream",
-                                "init");
-const LocalFile = CC("@mozilla.org/file/local;1",
-                     "nsILocalFile",
-                     "initWithPath");
+XPCOMUtils.defineLazyGetter(this, "_", function()
+  l10nHelper("chrome://chat/locale/logger.properties")
+);
 
 const kLineBreak = "@mozilla.org/windows-registry-key;1" in Cc ? "\r\n" : "\n";
 
+/*
+ * Maps file paths to promises returned by ongoing OS.File operations on them.
+ * This is so that a file can be read after a pending write operation completes
+ * and vice versa (opening a file multiple times concurrently may fail on Windows).
+ */
+let gFilePromises = new Map();
+
+// Uses above map to queue operations on a file.
+function queueFileOperation(aPath, aOperation) {
+  // Ensure the operation is queued regardless of whether the last one succeeded.
+  // This is safe since the promise is returned and consumers are expected to
+  // handle any errors. If there's no promise existing for the given path already,
+  // queue the operation on a dummy pre-resolved promise.
+  let promise =
+    (gFilePromises.get(aPath) || Promise.resolve()).then(aOperation, aOperation);
+  gFilePromises.set(aPath, promise);
+
+  let cleanup = () => {
+    // If no further operations have been queued, remove the reference from the map.
+    if (gFilePromises.get(aPath) === promise)
+      gFilePromises.delete(aPath);
+  };
+  // Ensure we clear unused promises whether they resolved or rejected.
+  promise.then(cleanup, cleanup);
+
+  return promise;
+}
+
+/*
+ * Convenience method to append to a file using the above queue system. If any of
+ * the I/O operations reject, the returned promise will reject with the same reason.
+ * We open the file, append, and close it immediately. The alternative is to keep
+ * it open and append as required, but we want to make sure we don't open a file
+ * for reading while it's already open for writing, so we close it every time
+ * (opening a file multiple times concurrently may fail on Windows).
+ * Note: This function creates parent directories if required.
+ */
+function appendToFile(aPath, aEncodedString, aCreate) {
+  return queueFileOperation(aPath, Task.async(function* () {
+    let dirPath = OS.Path
+    yield OS.File.makeDir(OS.Path.dirname(aPath),
+                          {ignoreExisting: true, from: OS.Constants.Path.profileDir});
+    let file = yield OS.File.open(aPath, {write: true, create: aCreate});
+    try {
+      yield file.write(aEncodedString);
+    }
+    finally {
+      /*
+       * If both the write() above and the close() below throw, and we don't
+       * handle the close error here, the promise will be rejected with the close
+       * error and the write error will be dropped. To avoid this, we log any
+       * close error here so that any write error will be propagated.
+       */
+      yield file.close().catch(Cu.reportError);
+    }
+  }));
+}
+
+
 // This function checks names against OS naming conventions and alters them
 // accordingly so that they can be used as file/folder names.
-function encodeName(aName)
-{
+function encodeName(aName) {
   // Reserved device names by Windows (prefixing "%").
-  var reservedNames = /^(CON|PRN|AUX|NUL|COM\d|LPT\d)$/i;
+  let reservedNames = /^(CON|PRN|AUX|NUL|COM\d|LPT\d)$/i;
   if (reservedNames.test(aName))
     return "%" + aName;
 
   // "." and " " must not be at the end of a file or folder name (appending "_").
   if (/[\. _]/.test(aName.slice(-1)))
     aName += "_";
 
   // Reserved characters are replaced by %[hex value]. encodeURIComponent() is
   // not sufficient, nevertheless decodeURIComponent() can be used to decode.
   function encodeReservedChars(match) "%" + match.charCodeAt(0).toString(16);
   return aName.replace(/[<>:"\/\\|?*&%]/g, encodeReservedChars);
 }
 
-function getLogFolderForAccount(aAccount, aCreate)
-{
-  let file = logDir.clone();
-  function createIfNotExists(aFile) {
-    if (aCreate && !aFile.exists())
-      aFile.create(Ci.nsIFile.DIRECTORY_TYPE, 0777);
-  }
-  createIfNotExists(file);
-  file.append(aAccount.protocol.normalizedName);
-  createIfNotExists(file);
-  file.append(encodeName(aAccount.normalizedName));
-  createIfNotExists(file);
-  return file;
+function getLogFolderPathForAccount(aAccount) {
+  return OS.Path.join(OS.Constants.Path.profileDir,
+                      "logs", aAccount.protocol.normalizedName,
+                      encodeName(aAccount.normalizedName));
 }
 
-function getNewLogFileName(aFormat, aDate)
-{
+function getLogFilePathForConversation(aConv, aFormat) {
+  let path = getLogFolderPathForAccount(aConv.account);
+  let name = aConv.normalizedName;
+  if (convIsRealMUC(aConv))
+    name += ".chat";
+  return OS.Path.join(path, encodeName(name),
+                      getNewLogFileName(aFormat, aConv.startDate));
+}
+
+function getNewLogFileName(aFormat, aDate) {
   let date = aDate ? new Date(aDate / 1000) : new Date();
   let dateTime = date.toLocaleFormat("%Y-%m-%d.%H%M%S");
   let offset = date.getTimezoneOffset();
   if (offset < 0) {
     dateTime += "+";
     offset *= -1;
   }
   else
@@ -80,52 +124,39 @@ function getNewLogFileName(aFormat, aDat
   offset = (offset - minutes) / 60;
   function twoDigits(aNumber)
     aNumber == 0 ? "00" : aNumber < 10 ? "0" + aNumber : aNumber;
   if (!aFormat)
     aFormat = "txt";
   return dateTime + twoDigits(offset) + twoDigits(minutes) + "." + aFormat;
 }
 
-/* Conversation logs stuff */
-function ConversationLog(aConversation)
-{
+
+// One of these is maintained for every conversation being logged. It initializes
+// a log file and appends to it as required.
+function LogWriter(aConversation) {
   this._conv = aConversation;
+  if (Services.prefs.getCharPref("purple.logging.format") == "json")
+    this.format = "json";
+  this.path = getLogFilePathForConversation(aConversation, this.format);
+  this._initialized =
+    appendToFile(this.path, this.encoder.encode(this._getHeader()), true);
+  // Catch the error separately so that _initialized will stay rejected if
+  // writing the header failed.
+  this._initialized.catch(aError =>
+                          Cu.reportError("Failed to initialize log file:\n" + aError));
 }
-ConversationLog.prototype = {
-  _log: null,
-  file: null,
+LogWriter.prototype = {
+  path: null,
+  // Constructor sets this to a promise that will resolve when the log header
+  // has been written.
+  _initialized: null,
   format: "txt",
-  _init: function cl_init() {
-    let file = getLogFolderForAccount(this._conv.account, true);
-    let name = this._conv.normalizedName;
-    if (convIsRealMUC(this._conv))
-      name += ".chat";
-    file.append(encodeName(name));
-    if (!file.exists())
-      file.create(Ci.nsIFile.DIRECTORY_TYPE, 0777);
-    if (Services.prefs.getCharPref("purple.logging.format") == "json")
-      this.format = "json";
-    file.append(getNewLogFileName(this.format, this._conv.startDate));
-    this.file = file;
-    let os = Cc["@mozilla.org/network/file-output-stream;1"].
-             createInstance(Ci.nsIFileOutputStream);
-    const PR_WRITE_ONLY   = 0x02;
-    const PR_CREATE_FILE  = 0x08;
-    const PR_APPEND       = 0x10;
-    os.init(file, PR_WRITE_ONLY | PR_CREATE_FILE | PR_APPEND, 0666, 0);
-    // just to be really sure everything is in UTF8
-    let converter = Cc["@mozilla.org/intl/converter-output-stream;1"].
-                    createInstance(Ci.nsIConverterOutputStream);
-    converter.init(os, "UTF-8", 0, 0);
-    this._log = converter;
-    this._log.writeString(this._getHeader());
-  },
-  _getHeader: function cl_getHeader()
-  {
+  encoder: new TextEncoder(),
+  _getHeader: function cl_getHeader() {
     let account = this._conv.account;
     if (this.format == "json") {
       return JSON.stringify({date: new Date(this._conv.startDate / 1000),
                              name: this._conv.name,
                              title: this._conv.title,
                              account: account.normalizedName,
                              protocol: account.protocol.normalizedName,
                              isChat: this._conv.isChat,
@@ -155,313 +186,141 @@ ConversationLog.prototype = {
         if (url != content)
           aNode.textContent = content + " (" + url + ")";
       }
       return null;
     }});
     return encoder.encodeToString();
   },
   logMessage: function cl_logMessage(aMessage) {
-    if (!this._log)
-      this._init();
-
+    let lineToWrite;
     if (this.format == "json") {
       let msg = {
         date: new Date(aMessage.time * 1000),
         who: aMessage.who,
         text: aMessage.originalMessage,
         flags: ["outgoing", "incoming", "system", "autoResponse",
                 "containsNick", "error", "delayed",
                 "noFormat", "containsImages", "notification",
                 "noLinkification"].filter(function(f) aMessage[f])
       };
       let alias = aMessage.alias;
       if (alias && alias != msg.who)
         msg.alias = alias;
-      this._log.writeString(JSON.stringify(msg) + "\n");
-      return;
-    }
-
-    let date = new Date(aMessage.time * 1000);
-    let line = "(" + date.toLocaleTimeString() + ") ";
-    let msg = this._serialize(aMessage.originalMessage);
-    if (aMessage.system)
-      line += msg;
-    else {
-      let sender = aMessage.alias || aMessage.who;
-      if (aMessage.autoResponse)
-        line += sender + " <AUTO-REPLY>: " + msg;
-      else {
-        if (msg.startsWith("/me "))
-          line += "***" + sender + " " + msg.substr(4);
-        else
-          line += sender + ": " + msg;
-      }
-    }
-    this._log.writeString(line + kLineBreak);
-  },
-
-  close: function cl_close() {
-    if (this._log) {
-      this._log.close();
-      this._log = null;
-      this.file = null;
+      lineToWrite = JSON.stringify(msg) + "\n";
     }
-  }
-};
-
-const dummyConversationLog = {
-  file: null,
-  logMessage: function() {},
-  close: function() {}
-};
-
-var gConversationLogs = { };
-function getLogForConversation(aConversation)
-{
-  let id = aConversation.id;
-  if (!(id in gConversationLogs)) {
-    let prefName =
-      "purple.logging.log_" + (aConversation.isChat ? "chats" : "ims");
-    if (Services.prefs.getBoolPref(prefName))
-      gConversationLogs[id] = new ConversationLog(aConversation);
-    else
-      gConversationLogs[id] = dummyConversationLog;
-  }
-  return gConversationLogs[id];
-}
-
-function closeLogForConversation(aConversation)
-{
-  let id = aConversation.id;
-  if (!(id in gConversationLogs))
-    return;
-  gConversationLogs[id].close();
-  delete gConversationLogs[id];
-}
-
-/* System logs stuff */
-function SystemLog(aAccount)
-{
-  this._init(aAccount);
-  this._log.writeString("System log for account " + aAccount.name +
-                        " (" + aAccount.protocol.normalizedName +
-                        ") connected at " +
-                        (new Date()).toLocaleFormat("%c") + kLineBreak);
-}
-SystemLog.prototype = {
-  _log: null,
-  _init: function sl_init(aAccount) {
-    let file = getLogFolderForAccount(aAccount, true);
-    file.append(".system");
-    if (!file.exists())
-      file.create(Ci.nsIFile.DIRECTORY_TYPE, 0777);
-    file.append(getNewLogFileName());
-    let os = Cc["@mozilla.org/network/file-output-stream;1"].
-             createInstance(Ci.nsIFileOutputStream);
-    const PR_WRITE_ONLY   = 0x02;
-    const PR_CREATE_FILE  = 0x08;
-    const PR_APPEND       = 0x10;
-    os.init(file, PR_WRITE_ONLY | PR_CREATE_FILE | PR_APPEND, 0666, 0);
-    // just to be really sure everything is in UTF8
-    let converter = Cc["@mozilla.org/intl/converter-output-stream;1"].
-                    createInstance(Ci.nsIConverterOutputStream);
-    converter.init(os, "UTF-8", 0, 0);
-    this._log = converter;
-  },
-  logEvent: function sl_logEvent(aString) {
-    if (!this._log)
-      this._init();
-
-    let date = (new Date()).toLocaleFormat("%x %X");
-    this._log.writeString("---- " + aString + " @ " + date + " ----" + kLineBreak);
-  },
-
-  close: function sl_close() {
-    if (this._log) {
-      this._log.close();
-      this._log = null;
+    else {
+      // Text log.
+      let date = new Date(aMessage.time * 1000);
+      let line = "(" + date.toLocaleTimeString() + ") ";
+      let msg = this._serialize(aMessage.originalMessage);
+      if (aMessage.system)
+        line += msg;
+      else {
+        let sender = aMessage.alias || aMessage.who;
+        if (aMessage.autoResponse)
+          line += sender + " <AUTO-REPLY>: " + msg;
+        else {
+          if (msg.startsWith("/me "))
+            line += "***" + sender + " " + msg.substr(4);
+          else
+            line += sender + ": " + msg;
+        }
+      }
+      lineToWrite = line + kLineBreak;
     }
+    lineToWrite = this.encoder.encode(lineToWrite);
+    this._initialized.then(() => {
+      appendToFile(this.path, lineToWrite)
+        .catch(aError => Cu.reportError("Failed to log message:\n" + aError));
+    });
   }
 };
 
-const dummySystemLog = {
-  logEvent: function(aString) {},
-  close: function() {}
+const dummyLogWriter = {
+  path: null,
+  logMessage: function() {}
 };
 
-var gSystemLogs = { };
-function getLogForAccount(aAccount, aCreate)
-{
-  let id = aAccount.id;
-  if (aCreate) {
-    if (id in gSystemLogs)
-      gSystemLogs[id].close();
-    if (!Services.prefs.getBoolPref("purple.logging.log_system"))
-      return dummySystemLog;
-    return (gSystemLogs[id] = new SystemLog(aAccount));
+
+let gLogWritersById = new Map();
+function getLogWriter(aConversation) {
+  let id = aConversation.id;
+  if (!gLogWritersById.has(id)) {
+    let prefName =
+      "purple.logging.log_" + (aConversation.isChat ? "chats" : "ims");
+    if (Services.prefs.getBoolPref(prefName))
+      gLogWritersById.set(id, new LogWriter(aConversation));
+    else
+      gLogWritersById.set(id, dummyLogWriter);
   }
-
-  return (id in gSystemLogs) && gSystemLogs[id] || dummySystemLog;
+  return gLogWritersById.get(id);
 }
 
-function closeLogForAccount(aAccount)
-{
-  let id = aAccount.id;
-  if (!(id in gSystemLogs))
-    return;
-  gSystemLogs[id].close();
-  delete gSystemLogs[id];
+function closeLogWriter(aConversation) {
+  gLogWritersById.delete(aConversation.id);
 }
 
-function LogMessage(aData, aConversation)
-{
-  this._init(aData.who, aData.text);
-  this._conversation = aConversation;
-  this.time = Math.round(new Date(aData.date) / 1000);
-  if ("alias" in aData)
-    this._alias = aData.alias;
-  for each (let flag in aData.flags)
-    this[flag] = true;
+// LogWriter for system logs.
+function SystemLogWriter(aAccount) {
+  this._account = aAccount;
+  this.path = OS.Path.join(getLogFolderPathForAccount(aAccount), ".system",
+                           getNewLogFileName());
+  let header = "System log for account " + aAccount.name +
+               " (" + aAccount.protocol.normalizedName +
+               ") connected at " +
+               (new Date()).toLocaleFormat("%c") + kLineBreak;
+  this._initialized = appendToFile(this.path, this.encoder.encode(header), true);
+  // Catch the error separately so that _initialized will stay rejected if
+  // writing the header failed.
+  this._initialized.catch(aError =>
+                          Cu.reportError("Error initializing system log:\n" + aError));
 }
-LogMessage.prototype = GenericMessagePrototype;
-
-function LogConversation(aLineInputStreams)
-{
-  // If aLineInputStreams isn't an Array, we'll assume that it's a lone
-  // InputStream, and wrap it in an Array.
-  if (!Array.isArray(aLineInputStreams))
-    aLineInputStreams = [aLineInputStreams];
-
-  this._messages = [];
-
-  // We'll read the name, title, account, and protocol data from the first
-  // stream, and skip the others.
-  let firstFile = true;
-
-  for each (let inputStream in aLineInputStreams) {
-    let stream = inputStream.stream;
-    let line = {value: ""};
-    let more = stream.readLine(line);
-    let sessionMsg = {
-      who: "sessionstart",
-      date: getDateFromFilename(inputStream.filename)[0],
-      text: "",
-      flags: ["noLog", "notification"]
-    }
-    if (!line.value) {
-      // Bad log file.
-      sessionMsg.text = bundle.formatStringFromName("badLogfile",
-                                                    [inputStream.filename], 1);
-      sessionMsg.flags.push("error", "system");
-      this._messages.push(sessionMsg);
-      continue;
-    }
-    this._messages.push(sessionMsg);
-
-    if (firstFile) {
-      let data = JSON.parse(line.value);
-      this.startDate = new Date(data.date) * 1000;
-      this.name = data.name;
-      this.title = data.title;
-      this._accountName = data.account;
-      this._protocolName = data.protocol;
-      this._isChat = data.isChat;
-      this.normalizedName = data.normalizedName;
-      firstFile = false;
-    }
-
-    while (more) {
-      more = stream.readLine(line);
-      if (!line.value)
-        break;
-      try {
-        this._messages.push(JSON.parse(line.value));
-      } catch (e) {
-        // if a message line contains junk, just ignore the error and
-        // continue reading the conversation.
-      }
-    }
-  }
-
-  if (firstFile)
-    throw "All selected log files are invalid";
-}
-LogConversation.prototype = {
-  __proto__: ClassInfo("imILogConversation", "Log conversation object"),
-  get isChat() this._isChat,
-  get buddy() null,
-  get account() ({
-    alias: "",
-    name: this._accountName,
-    normalizedName: this._accountName,
-    protocol: {name: this._protocolName},
-    statusInfo: Services.core.globalUserStatus
-  }),
-  getMessages: function(aMessageCount) {
-    if (aMessageCount)
-      aMessageCount.value = this._messages.length;
-    return this._messages.map(function(m) new LogMessage(m, this), this);
-  },
-  getMessagesEnumerator: function(aMessageCount) {
-    if (aMessageCount)
-      aMessageCount.value = this._messages.length;
-    let enumerator = {
-      _index: 0,
-      _conv: this,
-      _messages: this._messages,
-      hasMoreElements: function() this._index < this._messages.length,
-      getNext: function() new LogMessage(this._messages[this._index++], this._conv),
-      QueryInterface: XPCOMUtils.generateQI([Ci.nsISimpleEnumerator])
-    };
-    return enumerator;
+SystemLogWriter.prototype = {
+  encoder: new TextEncoder(),
+  // Constructor sets this to a promise that will resolve when the log header
+  // has been written.
+  _initialized: null,
+  path: null,
+  logEvent: function sl_logEvent(aString) {
+    let date = (new Date()).toLocaleFormat("%x %X");
+    let lineToWrite =
+      this.encoder.encode("---- " + aString + " @ " + date + " ----" + kLineBreak);
+    this._initialized.then(() => {
+      appendToFile(this.path, lineToWrite)
+        .catch(aError => Cu.reportError("Failed to log event:\n" + aError));
+    });
   }
 };
 
-/* Generic log enumeration stuff */
-function Log(aFile)
-{
-  this.file = aFile;
-  this.path = aFile.path;
+const dummySystemLogWriter = {
+  path: null,
+  logEvent: function() {}
+};
 
-  let [date, format] = getDateFromFilename(aFile.leafName);
-  if (!date || !format) {
-    this.format = "invalid";
-    this.time = 0;
-    return;
-  }
-  this.time = date.valueOf() / 1000;
-  this.format = format;
-}
-Log.prototype = {
-  __proto__: ClassInfo("imILog", "Log object"),
-  getConversation: function() {
-    if (this.format != "json")
-      return null;
 
-    const PR_RDONLY = 0x01;
-    let fis = new FileInputStream(this.file, PR_RDONLY, 0444,
-                                  Ci.nsIFileInputStream.CLOSE_ON_EOF);
-    let lis = new ConverterInputStream(fis, "UTF-8", 1024, 0x0);
-    lis.QueryInterface(Ci.nsIUnicharLineInputStream);
-    try {
-      return new LogConversation({
-        stream: lis,
-        filename: this.file.leafName
-      });
-    } catch (e) {
-      // If the file contains some junk (invalid JSON), the
-      // LogConversation code will still read the messages it can parse.
-      // If the first line of meta data is corrupt, there's really no
-      // useful data we can extract from the file so the
-      // LogConversation constructor will throw.
-      return null;
-    }
+let gSystemLogWritersById = new Map();
+function getSystemLogWriter(aAccount, aCreate) {
+  let id = aAccount.id;
+  if (aCreate) {
+    if (!Services.prefs.getBoolPref("purple.logging.log_system"))
+      return dummySystemLogWriter;
+    let writer = new SystemLogWriter(aAccount);
+    gSystemLogWritersById.set(id, writer);
+    return writer;
   }
-};
+
+  return gSystemLogWritersById.has(id) && gSystemLogWritersById.get(id) ||
+    dummySystemLogWriter;
+}
+
+function closeSystemLogWriter(aAccount) {
+  gSystemLogWritersById.delete(aAccount.id);
+}
+
 
 /**
  * Takes a properly formatted log file name and extracts the date information
  * and filetype, returning the results as an Array.
  *
  * Filenames are expected to be formatted as:
  *
  * YYYY-MM-DD.HHmmSS+ZZzz.format
@@ -485,276 +344,430 @@ function getDateFromFilename(aFilename) 
  * Returns true if a Conversation is both a chat conversation, and not
  * a Twitter conversation.
  */
 function convIsRealMUC(aConversation) {
   return (aConversation.isChat &&
           aConversation.account.protocol.id != "prpl-twitter");
 }
 
-function LogEnumerator(aEntries)
-{
-  this._entries = aEntries;
+
+function LogMessage(aData, aConversation) {
+  this._init(aData.who, aData.text);
+  this._conversation = aConversation;
+  this.time = Math.round(new Date(aData.date) / 1000);
+  if ("alias" in aData)
+    this._alias = aData.alias;
+  for (let flag of aData.flags)
+    this[flag] = true;
+}
+LogMessage.prototype = GenericMessagePrototype;
+
+
+function LogConversation(aMessages, aProperties) {
+  this._messages = aMessages;
+  for (let property in aProperties)
+    this[property] = aProperties[property];
 }
-LogEnumerator.prototype = {
-  _entries: [],
-  hasMoreElements: function() {
-    while (this._entries.length > 0 && !this._entries[0].hasMoreElements())
-      this._entries.shift();
-    return this._entries.length > 0;
+LogConversation.prototype = {
+  __proto__: ClassInfo("imILogConversation", "Log conversation object"),
+  get isChat() this._isChat,
+  get buddy() null,
+  get account() ({
+    alias: "",
+    name: this._accountName,
+    normalizedName: this._accountName,
+    protocol: {name: this._protocolName},
+    statusInfo: Services.core.globalUserStatus
+  }),
+  getMessages: function(aMessageCount) {
+    if (aMessageCount)
+      aMessageCount.value = this._messages.length;
+    return this._messages.map(function(m) new LogMessage(m, this), this);
   },
-  getNext: function()
-    new Log(this._entries[0].getNext().QueryInterface(Ci.nsIFile)),
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsISimpleEnumerator])
+  getMessagesEnumerator: function(aMessageCount) {
+    if (aMessageCount)
+      aMessageCount.value = this._messages.length;
+    let enumerator = {
+      _index: 0,
+      _conv: this,
+      _messages: this._messages,
+      hasMoreElements: function() this._index < this._messages.length,
+      getNext: function() new LogMessage(this._messages[this._index++], this._conv),
+      QueryInterface: XPCOMUtils.generateQI([Ci.nsISimpleEnumerator])
+    };
+    return enumerator;
+  }
 };
 
+
+/**
+ * A Log object represents one or more log files. The constructor expects one
+ * argument, which is either a single path to a (json or txt) log file or an
+ * array of objects each having two properties:
+ *   path: The full path of the (json only) log file it represents.
+ *   time: The Date object extracted from the filename of the logfile.
+ *
+ * The returned Log object's time property will be:
+ *   For a single file - exact time extracted from the name of the log file.
+ *   For a set of files - the time extracted, reduced to the day.
+ */
+function Log(aEntries) {
+  if (typeof aEntries == "string") {
+    // Assume that aEntries is a single path.
+    let path = aEntries;
+    this.path = path;
+    let [date, format] = getDateFromFilename(OS.Path.basename(path));
+    if (!date || !format) {
+      this.format = "invalid";
+      this.time = 0;
+      return;
+    }
+    this.time = date.valueOf() / 1000;
+    this.format = format;
+    // Wrap the path in an array
+    this._entryPaths = [path];
+    return;
+  }
+
+  if (!aEntries.length) {
+    throw new Error("Log was passed an invalid argument, " +
+                    "expected a non-empty array or a string.");
+  }
+
+  // Assume aEntries is an array of objects.
+  // Sort our list of entries for this day in increasing order.
+  aEntries.sort(function(aLeft, aRight) aLeft.time - aRight.time);
+
+  this._entryPaths = [entry.path for (entry of aEntries)];
+  // Calculate the timestamp for the first entry down to the day.
+  let timestamp = new Date(aEntries[0].time);
+  timestamp.setHours(0);
+  timestamp.setMinutes(0);
+  timestamp.setSeconds(0);
+  this.time = timestamp.valueOf() / 1000;
+  // Path is used to uniquely identify a Log, and sometimes used to
+  // quickly determine which directory a log file is from.  We'll use
+  // the first file's path.
+  this.path = aEntries[0].path;
+}
+Log.prototype = {
+  __proto__: ClassInfo("imILog", "Log object"),
+  _entryPaths: null,
+  format: "json",
+  getConversation: Task.async(function* () {
+    /*
+     * Read the set of log files asynchronously and return a promise that
+     * resolves to a LogConversation instance. Even if a file contains some
+     * junk (invalid JSON), messages that are valid will be read. If the first
+     * line of metadata is corrupt however, the data isn't useful and the
+     * promise will resolve to null.
+     */
+    if (this.format != "json")
+      return null;
+    let messages = [];
+    let properties = {};
+    let firstFile = true;
+    let decoder = new TextDecoder();
+    for (let path of this._entryPaths) {
+      let lines;
+      try {
+        let contents = yield queueFileOperation(path, () => OS.File.read(path));
+        lines = decoder.decode(contents).split("\n");
+      } catch (aError) {
+        Cu.reportError("Error reading log file \"" + path + "\":\n" + aError);
+        continue;
+      }
+      let nextLine = lines.shift();
+      let filename = OS.Path.basename(path);
+      let sessionMsg = {
+        who: "sessionstart",
+        date: getDateFromFilename(filename)[0],
+        text: "",
+        flags: ["noLog", "notification"]
+      };
+
+      let data;
+      try {
+        // This will fail if either nextLine is undefined, or not valid JSON.
+        data = JSON.parse(nextLine);
+      } catch (aError) {
+        sessionMsg.text = _("badLogFile", filename);
+        sessionMsg.flags.push("error", "system");
+        messages.push(sessionMsg);
+        continue;
+      }
+      messages.push(sessionMsg);
+
+      if (firstFile) {
+        properties.startDate = new Date(data.date) * 1000;
+        properties.name = data.name;
+        properties.title = data.title;
+        properties._accountName = data.account;
+        properties._protocolName = data.protocol;
+        properties._isChat = data.isChat;
+        properties.normalizedName = data.normalizedName;
+        firstFile = false;
+      }
+
+      while (lines.length) {
+        nextLine = lines.shift();
+        if (!nextLine)
+          break;
+        try {
+          messages.push(JSON.parse(nextLine));
+        } catch (e) {
+          // If a message line contains junk, just ignore the error and
+          // continue reading the conversation.
+        }
+      }
+    }
+
+    if (firstFile) // All selected log files are invalid.
+      return null;
+
+    return new LogConversation(messages, properties);
+  })
+};
+
+
+/**
+ * Log enumerators provide lists of log files ("entries"). aEntries is an array
+ * of the OS.File.DirectoryIterator.Entry instances which represent the log
+ * files to be parsed.
+ *
+ * DailyLogEnumerator organizes entries by date, and enumerates them in order.
+ * LogEnumerator enumerates logs in the same order as the input array.
+ */
 function DailyLogEnumerator(aEntries) {
   this._entries = {};
 
-  for each (let entry in aEntries) {
-    while (entry.hasMoreElements()) {
-      let file = entry.getNext();
-      if (!(file instanceof Ci.nsIFile))
-        continue;
+  for (let entry of aEntries) {
+    let path = entry.path;
 
-      let [logDate, logFormat] = getDateFromFilename(file.leafName);
-      if (!logDate) {
-        // We'll skip this one, since it's got a busted filename.
-        continue;
-      }
+    let [logDate, logFormat] = getDateFromFilename(OS.Path.basename(path));
+    if (!logDate) {
+      // We'll skip this one, since it's got a busted filename.
+      continue;
+    }
 
-      let dateForID = new Date(logDate);
-      let dayID;
-      if (logFormat == "json") {
-        // We want to cluster all of the logs that occur on the same day
-        // into the same Arrays. We clone the date for the log, reset it to
-        // the 0th hour/minute/second, and use that to construct an ID for the
-        // Array we'll put the log in.
-        dateForID.setHours(0);
-        dateForID.setMinutes(0);
-        dateForID.setSeconds(0);
-        dayID = dateForID.toISOString();
-      }
-      else {
-        // Add legacy text logs as individual entries.
-        dayID = dateForID.toISOString() + "txt";
-      }
+    let dateForID = new Date(logDate);
+    let dayID;
+    if (logFormat == "json") {
+      // We want to cluster all of the logs that occur on the same day
+      // into the same Arrays. We clone the date for the log, reset it to
+      // the 0th hour/minute/second, and use that to construct an ID for the
+      // Array we'll put the log in.
+      dateForID.setHours(0);
+      dateForID.setMinutes(0);
+      dateForID.setSeconds(0);
+      dayID = dateForID.toISOString();
 
       if (!(dayID in this._entries))
         this._entries[dayID] = [];
 
       this._entries[dayID].push({
-        file: file,
+        path: path,
         time: logDate
       });
     }
+    else {
+      // Add legacy text logs as individual paths.
+      dayID = dateForID.toISOString() + "txt";
+      this._entries[dayID] = path;
+    }
   }
 
   this._days = Object.keys(this._entries).sort();
   this._index = 0;
 }
 DailyLogEnumerator.prototype = {
   _entries: {},
   _days: [],
   _index: 0,
   hasMoreElements: function() this._index < this._days.length,
   getNext: function() {
     let dayID = this._days[this._index++];
-    if (dayID.endsWith("txt"))
-      return new Log(this._entries[dayID][0].file);
-    else
-      return new LogCluster(this._entries[dayID]);
+    return new Log(this._entries[dayID]);
+  },
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsISimpleEnumerator])
+};
+
+function LogEnumerator(aEntries) {
+  this._entries = aEntries;
+}
+LogEnumerator.prototype = {
+  _entries: [],
+  hasMoreElements: function() {
+    return this._entries.length > 0;
+  },
+  getNext: function() {
+    // Create and return a log from the first entry.
+    return new Log(this._entries.shift().path);
   },
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISimpleEnumerator])
 };
 
-/**
- * A LogCluster is a Log representing several log files all at once. The
- * constructor expects aEntries, which is an array of objects that each
- * have two properties: file and time. The file is the nsIFile for the
- * log file, and the time is the Date object extracted from the filename for
- * the log file.
- */
-function LogCluster(aEntries) {
-  if (!aEntries.length)
-    throw new Error("LogCluster was passed an empty Array");
-
-  // Sort our list of entries for this day in increasing order.
-  aEntries.sort(function(aLeft, aRight) aLeft.time - aRight.time);
-
-  this._entries = aEntries;
-  // Calculate the timestamp for the first entry down to the day.
-  let timestamp = new Date(aEntries[0].time);
-  timestamp.setHours(0);
-  timestamp.setMinutes(0);
-  timestamp.setSeconds(0);
-  this.time = timestamp.valueOf() / 1000;
-  // Path is used to uniquely identify a Log, and sometimes used to
-  // quickly determine which directory a log file is from.  We'll use
-  // the first file's path.
-  this.path = aEntries[0].file.path;
-}
-LogCluster.prototype = {
-  __proto__: ClassInfo("imILog", "LogCluster object"),
-  format: "json",
-
-  getConversation: function() {
-    const PR_RDONLY = 0x01;
-    let streams = [];
-    for each (let entry in this._entries) {
-      let fis = new FileInputStream(entry.file, PR_RDONLY, 0444,
-                                    Ci.nsIFileInputStream.CLOSE_ON_EOF);
-      // Pass in 0x0 so that we throw exceptions on unknown bytes.
-      let lis = new ConverterInputStream(fis, "UTF-8", 1024, 0x0);
-      lis.QueryInterface(Ci.nsIUnicharLineInputStream);
-      streams.push({
-        stream: lis,
-        filename: entry.file.leafName
-      });
-    }
-
-    try {
-      return new LogConversation(streams);
-    } catch (e) {
-      // If the file contains some junk (invalid JSON), the
-      // LogConversation code will still read the messages it can parse.
-      // If the first line of meta data is corrupt, there's really no
-      // useful data we can extract from the file so the
-      // LogConversation constructor will throw.
-      return null;
-    }
-  }
-};
 
 function Logger() { }
 Logger.prototype = {
-  _getLogArray: function logger__getLogArray(aAccount, aNormalizedName) {
-    let entries = [];
-    let file = getLogFolderForAccount(aAccount);
-    file.append(encodeName(aNormalizedName));
-    if (file.exists())
-      entries.push(file.directoryEntries);
-    return entries;
-  },
-  getLogFromFile: function logger_getLogFromFile(aFile, aGroupByDay) {
-    if (aGroupByDay)
-      return this._getDailyLogFromFile(aFile);
-
-    return new Log(aFile);
-  },
-  _getDailyLogFromFile: function logger_getDailyLogsForFile(aFile) {
-    let [targetDate] = getDateFromFilename(aFile.leafName);
+  // Returned Promise resolves to an array of entries for the
+  // log folder if it exists, otherwise null.
+  _getLogArray: Task.async(function* (aAccount, aNormalizedName) {
+    let iterator;
+    try {
+      let path = OS.Path.join(getLogFolderPathForAccount(aAccount),
+                              encodeName(aNormalizedName));
+      if (yield queueFileOperation(path, () => OS.File.exists(path))) {
+        iterator = new OS.File.DirectoryIterator(path);
+        let entries = yield iterator.nextBatch();
+        iterator.close();
+        return entries;
+      }
+    } catch (aError) {
+      if (iterator)
+        iterator.close();
+      Cu.reportError("Error getting directory entries for \"" +
+                     path + "\":\n" + aError);
+    }
+    return [];
+  }),
+  getLogFromFile: function logger_getLogFromFile(aFilePath, aGroupByDay) {
+    if (!aGroupByDay)
+      return Promise.resolve(new Log(aFilePath));
+    let [targetDate] = getDateFromFilename(OS.Path.basename(aFilePath));
     if (!targetDate)
       return null;
 
     let targetDay = Math.floor(targetDate / (86400 * 1000));
 
-    // Get the path for the log file - we'll assume that the files relevant
-    // to our interests are in the same folder.
-    let path = aFile.path;
-    let folder = aFile.parent.directoryEntries;
+    // We'll assume that the files relevant to our interests are
+    // in the same folder as the one provided.
+    let iterator = new OS.File.DirectoryIterator(OS.Path.dirname(aFilePath));
     let relevantEntries = [];
-    // Pick out the files that start within our date range.
-    while (folder.hasMoreElements()) {
-      let file = folder.getNext();
-      if (!(file instanceof Ci.nsIFile))
-        continue;
-
-      let [logTime] = getDateFromFilename(file.leafName);
+    return iterator.forEach(function(aEntry) {
+      if (aEntry.isDir)
+        return;
+      let path = aEntry.path;
+      let [logTime] = getDateFromFilename(OS.Path.basename(path));
 
       let day = Math.floor(logTime / (86400 * 1000));
       if (targetDay == day) {
         relevantEntries.push({
-          file: file,
+          path: path,
           time: logTime
         });
       }
-    }
-
-    return new LogCluster(relevantEntries);
+    }).then(() => {
+      iterator.close();
+      return new Log(relevantEntries);
+    }, aError => {
+      iterator.close();
+      throw aError;
+    });
   },
+  // Creates and returns the appropriate LogEnumerator for the given log array
+  // depending on aGroupByDay, or an EmptyEnumerator if the input array is empty.
   _getEnumerator: function logger__getEnumerator(aLogArray, aGroupByDay) {
     let enumerator = aGroupByDay ? DailyLogEnumerator : LogEnumerator;
     return aLogArray.length ? new enumerator(aLogArray) : EmptyEnumerator;
   },
-  getLogFileForOngoingConversation: function logger_getLogFileForOngoingConversation(aConversation)
-    getLogForConversation(aConversation).file,
+  getLogPathForConversation: function logger_getLogPathForConversation(aConversation) {
+    let writer = gLogWritersById.get(aConversation.id);
+    // Resolve to null if we haven't created a LogWriter yet for this conv, or
+    // if logging is disabled (path will be null).
+    if (!writer || !writer.path)
+      return Promise.resolve(null);
+    let path = writer.path;
+    // Wait for any pending file operations to finish, then resolve to the path
+    // regardless of whether these operations succeeded.
+    return (gFilePromises.get(path) || Promise.resolve()).then(
+      () => path, () => path);
+  },
   getLogsForAccountAndName: function logger_getLogsForAccountAndName(aAccount,
                                        aNormalizedName, aGroupByDay) {
-    let entries = this._getLogArray(aAccount, aNormalizedName);
-    return this._getEnumerator(entries, aGroupByDay);
+    return this._getLogArray(aAccount, aNormalizedName)
+               .then(aEntries => this._getEnumerator(aEntries, aGroupByDay));
   },
   getLogsForAccountBuddy: function logger_getLogsForAccountBuddy(aAccountBuddy,
                                                                  aGroupByDay) {
     return this.getLogsForAccountAndName(aAccountBuddy.account,
                                          aAccountBuddy.normalizedName, aGroupByDay);
   },
-  getLogsForBuddy: function logger_getLogsForBuddy(aBuddy, aGroupByDay) {
+  getLogsForBuddy: Task.async(function* (aBuddy, aGroupByDay) {
     let entries = [];
     for (let accountBuddy of aBuddy.getAccountBuddies()) {
-      entries = entries.concat(this._getLogArray(accountBuddy.account,
-                                                 accountBuddy.normalizedName));
+      entries = entries.concat(yield this._getLogArray(accountBuddy.account,
+                                                       accountBuddy.normalizedName));
     }
     return this._getEnumerator(entries, aGroupByDay);
-  },
-  getLogsForContact: function logger_getLogsForContact(aContact, aGroupByDay) {
+  }),
+  getLogsForContact: Task.async(function* (aContact, aGroupByDay) {
     let entries = [];
     for (let buddy of aContact.getBuddies()) {
       for (let accountBuddy of buddy.getAccountBuddies()) {
-        entries = entries.concat(this._getLogArray(accountBuddy.account,
-                                                   accountBuddy.normalizedName));
+        entries = entries.concat(yield this._getLogArray(accountBuddy.account,
+                                                         accountBuddy.normalizedName));
       }
     }
     return this._getEnumerator(entries, aGroupByDay);
-  },
+  }),
   getLogsForConversation: function logger_getLogsForConversation(aConversation,
                                                                  aGroupByDay) {
     let name = aConversation.normalizedName;
     if (convIsRealMUC(aConversation))
       name += ".chat";
     return this.getLogsForAccountAndName(aConversation.account, name, aGroupByDay);
   },
   getSystemLogsForAccount: function logger_getSystemLogsForAccount(aAccount)
     this.getLogsForAccountAndName(aAccount, ".system"),
-  getSimilarLogs: function(aLog, aGroupByDay) {
-    return this._getEnumerator([new LocalFile(aLog.path).parent.directoryEntries],
-                               aGroupByDay);
-  },
+  getSimilarLogs: Task.async(function* (aLog, aGroupByDay) {
+    let iterator = new OS.File.DirectoryIterator(OS.Path.dirname(aLog.path));
+    let entries;
+    try {
+      entries = yield iterator.nextBatch();
+    } catch (aError) {
+      Cu.reportError("Error getting similar logs for \"" +
+                     aLog.path + "\":\n" + aError);
+    }
+    // If there was an error, this will return an EmptyEnumerator.
+    return this._getEnumerator(entries, aGroupByDay);
+  }),
 
   observe: function logger_observe(aSubject, aTopic, aData) {
     switch (aTopic) {
     case "profile-after-change":
       Services.obs.addObserver(this, "final-ui-startup", false);
       break;
     case "final-ui-startup":
       Services.obs.removeObserver(this, "final-ui-startup");
       ["new-text", "conversation-closed", "conversation-left-chat",
        "account-connected", "account-disconnected",
        "account-buddy-status-changed"].forEach(function(aEvent) {
         Services.obs.addObserver(this, aEvent, false);
       }, this);
       break;
     case "new-text":
       if (!aSubject.noLog) {
-        let log = getLogForConversation(aSubject.conversation);
+        let log = getLogWriter(aSubject.conversation);
         log.logMessage(aSubject);
       }
       break;
     case "conversation-closed":
     case "conversation-left-chat":
-      closeLogForConversation(aSubject);
+      closeLogWriter(aSubject);
       break;
     case "account-connected":
-      getLogForAccount(aSubject, true).logEvent("+++ " + aSubject.name +
+      getSystemLogWriter(aSubject, true).logEvent("+++ " + aSubject.name +
                                                 " signed on");
       break;
     case "account-disconnected":
-      getLogForAccount(aSubject).logEvent("+++ " + aSubject.name +
+      getSystemLogWriter(aSubject).logEvent("+++ " + aSubject.name +
                                           " signed off");
-      closeLogForAccount(aSubject);
+      closeSystemLogWriter(aSubject);
       break;
     case "account-buddy-status-changed":
       let status;
       if (!aSubject.online)
         status = "Offline";
       else if (aSubject.mobile)
         status = "Mobile";
       else if (aSubject.idle)
@@ -764,17 +777,17 @@ Logger.prototype = {
       else
         status = "Unavailable";
 
       let statusText = aSubject.statusText;
       if (statusText)
         status += " (\"" + statusText + "\")";
 
       let nameText = aSubject.displayName + " (" + aSubject.userName + ")";
-      getLogForAccount(aSubject.account).logEvent(nameText + " is now " + status);
+      getSystemLogWriter(aSubject.account).logEvent(nameText + " is now " + status);
       break;
     default:
       throw "Unexpected notification " + aTopic;
     }
   },
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.imILogger]),
   classDescription: "Logger",