Fix bug 329570 - Concurrent editing ICS calendars by multiple users can lose data. r=mvl
authorDaniel Boelzle [:dbo] <daniel.boelzle@sun.com>
Sat, 25 Oct 2008 19:08:09 +0200
changeset 712 106c97fa0fcfad135d77ea72cb1f3295efa2aec2
parent 711 76a3276c98ec4a3dc476c9767162b647d85f2ffb
child 713 a227ba3c0aa004594078e95f7a9fcc2ae69012dd
push id639
push userdaniel.boelzle@sun.com
push dateSat, 25 Oct 2008 17:08:48 +0000
treeherdercomm-central@106c97fa0fcf [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmvl
bugs329570
Fix bug 329570 - Concurrent editing ICS calendars by multiple users can lose data. r=mvl
calendar/providers/ics/calICSCalendar.js
--- a/calendar/providers/ics/calICSCalendar.js
+++ b/calendar/providers/ics/calICSCalendar.js
@@ -59,16 +59,17 @@ var isOnBranch = appInfo.platformVersion
 
 function calICSCalendar() {
     this.initProviderBase();
     this.initICSCalendar();
 
     this.unmappedComponents = [];
     this.unmappedProperties = [];
     this.queue = new Array();
+    this.mModificationActions = [];
 }
 
 calICSCalendar.prototype = {
     __proto__: calProviderBase.prototype,
 
     mObserver: null,
     locked: false,
 
@@ -273,17 +274,17 @@ calICSCalendar.prototype = {
     writeICS: function () {
         this.lock();
         try {
             if (!this.mUri)
                 throw Components.results.NS_ERROR_FAILURE;
             // makeBackup will call doWriteICS
             this.makeBackup(this.doWriteICS);
         } catch (exc) {
-            this.unlock();
+            this.unlock(calIErrors.MODIFICATION_FAILED);
             throw exc;
         }
     },
 
     doWriteICS: function () {
         var savedthis = this;
         var appStartup = Components.classes["@mozilla.org/toolkit/app-startup;1"]
                                    .getService(Components.interfaces.nsIAppStartup);
@@ -323,17 +324,17 @@ calICSCalendar.prototype = {
                 } catch (ex) {
                     if (inLastWindowClosingSurvivalArea) {
                         appStartup.exitLastWindowClosingSurvivalArea();
                     }
                     savedthis.mObserver.onError(savedthis.superCalendar,
                                                 ex.result, "The calendar could not be saved; there " +
                                                 "was a failure: 0x" + ex.result.toString(16));
                     savedthis.mObserver.onError(savedthis.superCalendar, calIErrors.MODIFICATION_FAILED, "");
-                    savedthis.unlock();
+                    savedthis.unlock(calIErrors.MODIFICATION_FAILED);
                 }
             },
             onGetResult: function(aCalendar, aStatus, aItemType, aDetail, aCount, aItems)
             {
                 this.serializer.addItems(aItems, aCount);
             }
         };
         listener.serializer = Components.classes["@mozilla.org/calendar/ics-serializer;1"].
@@ -367,33 +368,40 @@ calICSCalendar.prototype = {
         ctxt = ctxt.wrappedJSObject;
         var channel;
         try {
             channel = request.QueryInterface(Components.interfaces.nsIHttpChannel);
             LOG("calICSCalendar channel.requestSucceeded: " + channel.requestSucceeded);
         } catch(e) {
         }
 
+        let appStartup = Components.classes["@mozilla.org/toolkit/app-startup;1"]
+                                   .getService(Components.interfaces.nsIAppStartup);
+
         if ((channel && !channel.requestSucceeded) ||
             (!channel && !Components.isSuccessCode(request.status))) {
             ctxt.mObserver.onError(this.superCalendar,
                                    Components.isSuccessCode(request.status)
                                    ? calIErrors.DAV_PUT_ERROR
                                    : request.status,
                                    "Publishing the calendar file failed\n" +
                                        "Status code: "+request.status.toString(16)+"\n");
             ctxt.mObserver.onError(this.superCalendar, calIErrors.MODIFICATION_FAILED, "");
+
+            // the PUT has failed, refresh, and signal error to all modifying operations:
+            this.refresh();
+            ctxt.unlock(calIErrors.MODIFICATION_FAILED);
+            appStartup.exitLastWindowClosingSurvivalArea();
+            return;
         }
 
         // Allow the hook to grab data of the channel, like the new etag
         ctxt.mHooks.onAfterPut(channel,
                                function() {
                                    ctxt.unlock();
-                                   var appStartup = Components.classes["@mozilla.org/toolkit/app-startup;1"]
-                                       .getService(Components.interfaces.nsIAppStartup);
                                    appStartup.exitLastWindowClosingSurvivalArea();
                                });
     },
 
     // Always use the queue, just to reduce the amount of places where
     // this.mMemoryCalendar.addItem() and friends are called. less
     // copied code.
     addItem: function (aItem, aListener) {
@@ -435,32 +443,45 @@ calICSCalendar.prototype = {
                          listener:aListener});
         this.processQueue();
     },
 
     processQueue: function ()
     {
         if (this.isLocked())
             return;
+
+        function modListener(action) {
+            this.mAction = action;
+        }
+        modListener.prototype = {
+            onGetResult: function() {},
+            onOperationComplete: function() {
+                this.mAction.opCompleteArgs = arguments;
+            }
+        };
+
         var a;
         var writeICS = false;
         var refreshAction = null;
         while ((a = this.queue.shift())) {
             switch (a.action) {
                 case 'add':
-                    this.mMemoryCalendar.addItem(a.item, a.listener);
+                    this.mMemoryCalendar.addItem(a.item, new modListener(a));
+                    this.mModificationActions.push(a);
                     writeICS = true;
                     break;
                 case 'modify':
-                    this.mMemoryCalendar.modifyItem(a.newItem, a.oldItem,
-                                                    a.listener);
+                    this.mMemoryCalendar.modifyItem(a.newItem, a.oldItem, new modListener(a));
+                    this.mModificationActions.push(a);
                     writeICS = true;
                     break;
                 case 'delete':
-                    this.mMemoryCalendar.deleteItem(a.item, a.listener);
+                    this.mMemoryCalendar.deleteItem(a.item, new modListener(a));
+                    this.mModificationActions.push(a);
                     writeICS = true;
                     break;
                 case 'get_item':
                     this.mMemoryCalendar.getItem(a.id, a.listener);
                     break;
                 case 'get_items':
                     this.mMemoryCalendar.getItems(a.itemFilter, a.count,
                                                   a.rangeStart, a.rangeEnd,
@@ -489,17 +510,35 @@ calICSCalendar.prototype = {
             this.doRefresh();
         }
     },
 
     lock: function () {
         this.locked = true;
     },
 
-    unlock: function () {
+    unlock: function (errCode) {
+        ASSERT(this.locked, "unexpected!");
+
+        this.mModificationActions.forEach(
+            function(action) {
+                let args = action.opCompleteArgs;
+                ASSERT(args, "missing onOperationComplete call!");
+                let listener = action.listener;
+                if (listener) {
+                    if (Components.isSuccessCode(args[1]) &&
+                        errCode && !Components.isSuccessCode(errCode)) {
+                        listener.onOperationComplete(args[0], errCode, args[2], args[3], null);
+                    } else {
+                        listener.onOperationComplete.apply(listener, args);
+                    }
+                }
+            });
+        this.mModificationActions = [];
+
         this.locked = false;
         this.processQueue();
     },
     
     isLocked: function () {
         return this.locked;
     },