Bug 1289292 - Shutdown session info while fail to establish control channel. r=smaug
authorShih-Chiang Chien <schien@mozilla.com>
Wed, 27 Jul 2016 18:53:00 -0400
changeset 349085 4730019ab44c34c7fe99e347647a25ba2b53b628
parent 349084 343a4eced34bdb2c610da8d439da348f1e159eaa
child 349086 5f98e30cc66ebe5b5e3aaf4071d8c69e85a4ae91
push id1230
push userjlund@mozilla.com
push dateMon, 31 Oct 2016 18:13:35 +0000
treeherdermozilla-release@5e06e3766db2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1289292
milestone50.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1289292 - Shutdown session info while fail to establish control channel. r=smaug
dom/presentation/PresentationSessionInfo.cpp
dom/presentation/tests/mochitest/PresentationSessionChromeScript1UA.js
dom/presentation/tests/mochitest/file_presentation_terminate_establish_connection_error.html
dom/presentation/tests/mochitest/mochitest.ini
dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error.js
dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error_inproc.html
dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error_oop.html
--- a/dom/presentation/PresentationSessionInfo.cpp
+++ b/dom/presentation/PresentationSessionInfo.cpp
@@ -296,23 +296,27 @@ PresentationSessionInfo::Close(nsresult 
     case nsIPresentationSessionListener::STATE_CLOSED: {
       Shutdown(aReason);
       break;
     }
     case nsIPresentationSessionListener::STATE_TERMINATED: {
       if (!mControlChannel) {
         nsCOMPtr<nsIPresentationControlChannel> ctrlChannel;
         nsresult rv = mDevice->EstablishControlChannel(getter_AddRefs(ctrlChannel));
-        if (NS_SUCCEEDED(rv)) {
-          SetControlChannel(ctrlChannel);
+        if (NS_FAILED(rv)) {
+          Shutdown(rv);
+          return rv;
         }
+
+        SetControlChannel(ctrlChannel);
         return rv;
       }
 
-      return mControlChannel->Terminate(mSessionId);
+      ContinueTermination();
+      return NS_OK;
     }
   }
 
   return NS_OK;
 }
 
 nsresult
 PresentationSessionInfo::OnTerminate(nsIPresentationControlChannel* aControlChannel)
--- a/dom/presentation/tests/mochitest/PresentationSessionChromeScript1UA.js
+++ b/dom/presentation/tests/mochitest/PresentationSessionChromeScript1UA.js
@@ -18,16 +18,17 @@ function debug(str) {
 
 const originalFactoryData = [];
 var sessionId; // Store the uuid generated by PresentationRequest.
 const address = Cc["@mozilla.org/supports-cstring;1"]
                   .createInstance(Ci.nsISupportsCString);
 address.data = "127.0.0.1";
 const addresses = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
 addresses.appendElement(address, false);
+var triggerControlChannelError = false; // For simulating error during control channel establishment.
 
 function mockChannelDescription(role) {
   this.QueryInterface = XPCOMUtils.generateQI([Ci.nsIPresentationChannelDescription]);
   this.role = role;
   this.type = Ci.nsIPresentationChannelDescription.TYPE_TCP;
   this.tcpAddress = addresses;
   this.tcpPort = (role === 'sender' ? 1234 : 4321); // either sender or receiver
 }
@@ -257,16 +258,19 @@ const mockControlChannelOfReceiver = {
 };
 
 const mockDevice = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevice]),
   id:   'id',
   name: 'name',
   type: 'type',
   establishControlChannel: function(url, presentationId) {
+    if (triggerControlChannelError) {
+      throw Cr.NS_ERROR_FAILURE;
+    }
     sendAsyncMessage('control-channel-established');
     return mockControlChannelOfSender;
   },
 };
 
 const mockDevicePrompt = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevicePrompt,
                                          Ci.nsIFactory]),
@@ -392,16 +396,21 @@ function initMockAndListener() {
   });
 
   addMessageListener('trigger-control-channel-open', function(reason) {
     debug('Got message: trigger-control-channel-open');
     mockControlChannelOfSender.notifyConnected();
     mockControlChannelOfReceiver.notifyConnected();
   });
 
+  addMessageListener('trigger-control-channel-error', function(reason) {
+    debug('Got message: trigger-control-channel-open');
+    triggerControlChannelError = true;
+  });
+
   addMessageListener('trigger-on-offer', function() {
     debug('Got message: trigger-on-offer');
     mockControlChannelOfReceiver.onOffer(mockChannelDescriptionOfSender);
     mockServerSocket.onSocketAccepted(mockServerSocket, mockSocketTransport);
   });
 
   addMessageListener('trigger-on-answer', function() {
     debug('Got message: trigger-on-answer');
new file mode 100644
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_terminate_establish_connection_error.html
@@ -0,0 +1,114 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+  <head>
+    <meta charset='utf-8'>
+    <title>Test for B2G PresentationReceiver at receiver side</title>
+  </head>
+  <body>
+    <div id='content'></div>
+<script type='application/javascript;version=1.7'>
+
+'use strict';
+
+function is(a, b, msg) {
+  if (a === b) {
+    alert('OK ' + msg);
+  } else {
+    alert('KO ' + msg + ' | reason: ' + a + ' != ' + b);
+  }
+}
+
+function ok(a, msg) {
+  alert((a ? 'OK ' : 'KO ') + msg);
+}
+
+function info(msg) {
+  alert('INFO ' + msg);
+}
+
+function command(name, data) {
+  alert('COMMAND ' + JSON.stringify({name: name, data: data}));
+}
+
+function finish() {
+  alert('DONE');
+}
+
+var connection;
+
+function testConnectionAvailable() {
+  return new Promise(function(aResolve, aReject) {
+    info('Receiver: --- testConnectionAvailable ---');
+    ok(navigator.presentation, 'Receiver: navigator.presentation should be available.');
+    ok(navigator.presentation.receiver, 'Receiver: navigator.presentation.receiver should be available.');
+
+    navigator.presentation.receiver.connectionList
+    .then((aList) => {
+      is(aList.connections.length, 1, 'Should get one connection.');
+      connection = aList.connections[0];
+      ok(connection.id, 'Connection ID should be set: ' + connection.id);
+      is(connection.state, 'connected', 'Connection state at receiver side should be connected.');
+      aResolve();
+    })
+    .catch((aError) => {
+      ok(false, 'Receiver: Error occurred when getting the connection: ' + aError);
+      finish();
+      aReject();
+    });
+  });
+}
+
+function testConnectionReady() {
+  return new Promise(function(aResolve, aReject) {
+    info('Receiver: --- testConnectionReady ---');
+    connection.onconnect = function() {
+      connection.onconnect = null;
+      ok(false, 'Should not get |onconnect| event.')
+      aReject();
+    };
+    if (connection.state === 'connected') {
+      connection.onconnect = null;
+      is(connection.state, 'connected', 'Receiver: Connection state should become connected.');
+      aResolve();
+    }
+  });
+}
+
+function testConnectionTerminate() {
+  return new Promise(function(aResolve, aReject) {
+    info('Receiver: --- testConnectionTerminate ---');
+    connection.onterminate = function() {
+      connection.onterminate = null;
+      // Using window.alert at this stage will cause window.close() fail.
+      // Only trigger it if verdict fail.
+      if (connection.state !== 'terminated') {
+        is(connection.state, 'terminated', 'Receiver: Connection should be terminated.');
+      }
+      aResolve();
+    };
+
+    window.addEventListener('hashchange', function hashchangeHandler(evt) {
+      var message = JSON.parse(decodeURIComponent(window.location.hash.substring(1)));
+      if (message.type === 'ready-to-terminate') {
+        info('Receiver: --- ready-to-terminate ---');
+        connection.terminate();
+      }
+    });
+
+
+    command('forward-command', JSON.stringify({ name: 'prepare-for-terminate' }));
+  });
+}
+
+function runTests() {
+  testConnectionAvailable()
+  .then(testConnectionReady)
+  .then(testConnectionTerminate)
+}
+
+runTests();
+
+</script>
+  </body>
+</html>
--- a/dom/presentation/tests/mochitest/mochitest.ini
+++ b/dom/presentation/tests/mochitest/mochitest.ini
@@ -12,16 +12,18 @@ support-files =
   file_presentation_receiver_inner_iframe.html
   file_presentation_1ua_wentaway.html
   test_presentation_1ua_connection_wentaway.js
   file_presentation_receiver_auxiliary_navigation.html
   test_presentation_receiver_auxiliary_navigation.js
   file_presentation_sandboxed_presentation.html
   file_presentation_terminate.html
   test_presentation_terminate.js
+  file_presentation_terminate_establish_connection_error.html
+  test_presentation_terminate_establish_connection_error.js
 
 [test_presentation_dc_sender.html]
 [test_presentation_dc_receiver.html]
 skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android') # Bug 1129785
 [test_presentation_dc_receiver_oop.html]
 skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android') # Bug 1129785
 [test_presentation_1ua_sender_and_receiver.html]
 skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android') # Bug 1129785
@@ -47,11 +49,15 @@ skip-if = (e10s || toolkit == 'gonk' || 
 [test_presentation_receiver_auxiliary_navigation_inproc.html]
 skip-if = (e10s || toolkit == 'gonk')
 [test_presentation_receiver_auxiliary_navigation_oop.html]
 skip-if = (e10s || toolkit == 'gonk')
 [test_presentation_terminate_inproc.html]
 skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android')
 [test_presentation_terminate_oop.html]
 skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android')
+[test_presentation_terminate_establish_connection_error_inproc.html]
+skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android')
+[test_presentation_terminate_establish_connection_error_oop.html]
+skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android')
 [test_presentation_sender_on_terminate_request.html]
 skip-if = toolkit == 'android'
 [test_presentation_sandboxed_presentation.html]
new file mode 100644
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error.js
@@ -0,0 +1,189 @@
+'use strict';
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout('Test for guarantee not firing async event');
+
+function debug(str) {
+  // info(str);
+}
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL('PresentationSessionChromeScript1UA.js'));
+var receiverUrl = SimpleTest.getTestFileURL('file_presentation_terminate_establish_connection_error.html');
+var request;
+var connection;
+var receiverIframe;
+
+function postMessageToIframe(aType) {
+  receiverIframe.src = receiverUrl + "#" +
+                       encodeURIComponent(JSON.stringify({ type: aType }));
+}
+
+function setup() {
+  gScript.addMessageListener('device-prompt', function devicePromptHandler() {
+    debug('Got message: device-prompt');
+    gScript.removeMessageListener('device-prompt', devicePromptHandler);
+    gScript.sendAsyncMessage('trigger-device-prompt-select');
+  });
+
+  gScript.addMessageListener('control-channel-established', function controlChannelEstablishedHandler() {
+    gScript.removeMessageListener('control-channel-established',
+                                  controlChannelEstablishedHandler);
+    gScript.sendAsyncMessage('trigger-control-channel-open');
+  });
+
+  gScript.addMessageListener('sender-launch', function senderLaunchHandler(url) {
+    debug('Got message: sender-launch');
+    gScript.removeMessageListener('sender-launch', senderLaunchHandler);
+    is(url, receiverUrl, 'Receiver: should receive the same url');
+    receiverIframe = document.createElement('iframe');
+    receiverIframe.setAttribute('mozbrowser', 'true');
+    receiverIframe.setAttribute('mozpresentation', receiverUrl);
+    var oop = location.pathname.indexOf('_inproc') == -1;
+    receiverIframe.setAttribute('remote', oop);
+
+    receiverIframe.setAttribute('src', receiverUrl);
+    receiverIframe.addEventListener('mozbrowserloadend', function mozbrowserloadendHander() {
+      receiverIframe.removeEventListener('mozbrowserloadend', mozbrowserloadendHander);
+      info('Receiver loaded.');
+    });
+
+    // This event is triggered when the iframe calls 'alert'.
+    receiverIframe.addEventListener('mozbrowsershowmodalprompt', function receiverListener(evt) {
+      var message = evt.detail.message;
+      if (/^OK /.exec(message)) {
+        ok(true, message.replace(/^OK /, ''));
+      } else if (/^KO /.exec(message)) {
+        ok(false, message.replace(/^KO /, ''));
+      } else if (/^INFO /.exec(message)) {
+        info(message.replace(/^INFO /, ''));
+      } else if (/^COMMAND /.exec(message)) {
+        var command = JSON.parse(message.replace(/^COMMAND /, ''));
+        gScript.sendAsyncMessage(command.name, command.data);
+      } else if (/^DONE$/.exec(message)) {
+        ok(true, 'Messaging from iframe complete.');
+        receiverIframe.removeEventListener('mozbrowsershowmodalprompt',
+                                            receiverListener);
+      }
+    }, false);
+
+    var promise = new Promise(function(aResolve, aReject) {
+      document.body.appendChild(receiverIframe);
+      aResolve(receiverIframe);
+    });
+
+    var obs = SpecialPowers.Cc['@mozilla.org/observer-service;1']
+                           .getService(SpecialPowers.Ci.nsIObserverService);
+    obs.notifyObservers(promise, 'setup-request-promise', null);
+  });
+
+  gScript.addMessageListener('promise-setup-ready', function promiseSetupReadyHandler() {
+    debug('Got message: promise-setup-ready');
+    gScript.removeMessageListener('promise-setup-ready',
+                                  promiseSetupReadyHandler);
+    gScript.sendAsyncMessage('trigger-on-session-request', receiverUrl);
+  });
+
+  gScript.addMessageListener('offer-sent', function offerSentHandler() {
+    debug('Got message: offer-sent');
+    gScript.removeMessageListener('offer-sent', offerSentHandler);
+    gScript.sendAsyncMessage('trigger-on-offer');
+  });
+
+  gScript.addMessageListener('answer-sent', function answerSentHandler() {
+    debug('Got message: answer-sent');
+    gScript.removeMessageListener('answer-sent', answerSentHandler);
+    gScript.sendAsyncMessage('trigger-on-answer');
+  });
+
+  return Promise.resolve();
+}
+
+function testCreateRequest() {
+  return new Promise(function(aResolve, aReject) {
+    info('Sender: --- testCreateRequest ---');
+    request = new PresentationRequest(receiverUrl);
+    request.getAvailability().then((aAvailability) => {
+      aAvailability.onchange = function() {
+        aAvailability.onchange = null;
+        ok(aAvailability.value, 'Sender: Device should be available.');
+        aResolve();
+      }
+    }).catch((aError) => {
+      ok(false, 'Sender: Error occurred when getting availability: ' + aError);
+      teardown();
+      aReject();
+    });
+
+    gScript.sendAsyncMessage('trigger-device-add');
+  });
+}
+
+function testStartConnection() {
+  return new Promise(function(aResolve, aReject) {
+    request.start().then((aConnection) => {
+      connection = aConnection;
+      ok(connection, 'Sender: Connection should be available.');
+      ok(connection.id, 'Sender: Connection ID should be set.');
+      is(connection.state, 'connecting', 'Sender: The initial state should be connecting.');
+      connection.onconnect = function() {
+        connection.onconnect = null;
+        is(connection.state, 'connected', 'Connection should be connected.');
+        aResolve();
+      };
+    }).catch((aError) => {
+      ok(false, 'Sender: Error occurred when establishing a connection: ' + aError);
+      teardown();
+      aReject();
+    });
+  });
+}
+
+function testConnectionTerminate() {
+  return new Promise(function(aResolve, aReject) {
+    info('Sender: --- testConnectionTerminate---');
+    gScript.addMessageListener('prepare-for-terminate', function prepareForTerminateHandler() {
+      debug('Got message: prepare-for-terminate');
+      gScript.removeMessageListener('prepare-for-terminate', prepareForTerminateHandler);
+      connection.onclose = function() {
+        connection.onclose = null;
+        is(connection.state, 'closed', 'Sender: Connection should be closed.');
+      };
+      gScript.sendAsyncMessage('trigger-control-channel-error');
+      receiverIframe.addEventListener('mozbrowserclose', function() {
+        ok(true, 'observe receiver page closing');
+        aResolve();
+      });
+      postMessageToIframe('ready-to-terminate');
+    });
+  });
+}
+
+function teardown() {
+  gScript.addMessageListener('teardown-complete', function teardownCompleteHandler() {
+    debug('Got message: teardown-complete');
+    gScript.removeMessageListener('teardown-complete', teardownCompleteHandler);
+    gScript.destroy();
+    SimpleTest.finish();
+  });
+  gScript.sendAsyncMessage('teardown');
+}
+
+function runTests() {
+  setup().then(testCreateRequest)
+         .then(testStartConnection)
+         .then(testConnectionTerminate)
+         .then(teardown);
+}
+
+SpecialPowers.pushPermissions([
+  {type: 'presentation-device-manage', allow: false, context: document},
+  {type: 'presentation', allow: true, context: document},
+  {type: 'browser', allow: true, context: document},
+], () => {
+  SpecialPowers.pushPrefEnv({ 'set': [['dom.presentation.enabled', true],
+                                      ['dom.presentation.test.enabled', true],
+                                      ['dom.mozBrowserFramesEnabled', true],
+                                      ['dom.ipc.tabs.disabled', false],
+                                      ['dom.presentation.test.stage', 0]]},
+                            runTests);
+});
new file mode 100644
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error_inproc.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+  <!-- Any copyright is dedicated to the Public Domain.
+    - http://creativecommons.org/publicdomain/zero/1.0/ -->
+  <head>
+    <meta charset='utf-8'>
+    <title>Test for control channel establish error during PresentationConnection.terminate()</title>
+    <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/>
+    <script type='application/javascript' src='/tests/SimpleTest/SimpleTest.js'></script>
+  </head>
+  <body>
+    <a target='_blank' href='https://bugzilla.mozilla.org/show_bug.cgi?id=1289292'>
+      Test for constrol channel establish error during PresentationConnection.terminate()</a>
+    <script type='application/javascript;version=1.8' src='test_presentation_terminate_establish_connection_error.js'>
+    </script>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error_oop.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+  <!-- Any copyright is dedicated to the Public Domain.
+    - http://creativecommons.org/publicdomain/zero/1.0/ -->
+  <head>
+    <meta charset='utf-8'>
+    <title>Test for control channel establish error during PresentationConnection.terminate()</title>
+    <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/>
+    <script type='application/javascript' src='/tests/SimpleTest/SimpleTest.js'></script>
+  </head>
+  <body>
+    <a target='_blank' href='https://bugzilla.mozilla.org/show_bug.cgi?id=1289292'>
+      Test for constrol channel establish error during PresentationConnection.terminate()</a>
+    <script type='application/javascript;version=1.8' src='test_presentation_terminate_establish_connection_error.js'>
+    </script>
+  </body>
+</html>