Closed Bug 1676043 Opened 4 years ago Closed 1 year ago

BroadcastChannel created in ServiceWorker outlives unregistration and page reload

Categories

(Core :: DOM: Service Workers, defect, P3)

Firefox 84
defect

Tracking

()

RESOLVED WORKSFORME

People

(Reporter: guest271314, Unassigned)

Details

User Agent: Mozilla/5.0 (X11; Linux x86_64; rv:84.0) Gecko/20100101 Firefox/84.0

Steps to reproduce:

  1. Unregister all ServiceWorker's if any are registered at window.onload event handler.
  2. Create BroastcastChannel in main thread.
  3. Register new ServiceWorker.
  4. Create BroadcastChannel in ServiceWorker
  5. Call BroadcastChannel.postMessage() in ServiceWorker to main thread.
  6. Create an IFRAME append to BODY
  7. Intercept IFRAME request, send Response in ServiceWorker
  8. postMessage() from main thread to ServiceWorker
  9. When message does not appear to be received by BroadcastChannel in ServiceWorker, reload page, which again unregisters all ServiceWorker's

Actual results:

  1. Does not appear that BroastcastChannel in ServiceWorker receives message from main thread.
  2. BraodcastChannel created in ServiceWorker that was unregistered continues to post messages to main thread after reload of window.

index.html

<!DOCTYPE html>

<html>
<head>
<script src="script.js"></script>
</head>

<body>
<button id="start">Start download</button>

<button id="abort">Abort download</button>

</body>
</html>

script.js

const unregisterServiceWorkers = async (_) => {
const registrations = await navigator.serviceWorker.getRegistrations();
for (const registration of registrations) {
console.log(registration);
try {
await registration.unregister();
} catch (e) {
throw e;
}
}
return ServiceWorker's unregistered;
};

const bc = new BroadcastChannel('downloads');

bc.onmessage = (e) => {
console.log(e.data);
};

onload = async (_) => {

console.log(await unregisterServiceWorkers());

document.querySelector('#abort').onclick = (_) =>
bc.postMessage({ abort: true });

document.querySelector('#start').onclick = async (_) => {
console.log(await unregisterServiceWorkers());
console.log(await navigator.serviceWorker.register('sw.js', { scope: './' }));
let node = document.querySelector('iframe');
if (node) document.body.removeChild(node);
const iframe = document.createElement('iframe');
iframe.onload = async e => {
console.log(e);
}
document.body.append(iframe);
iframe.src = './ping';

};
};

sw.js

// https://stackoverflow.com/a/34046299
self.addEventListener('install', (event) => {
// Bypass the waiting lifecycle stage,
// just in case there's an older version of this SW registration.
event.waitUntil(self.skipWaiting());
});

self.addEventListener('activate', (event) => {
// Take control of all pages under this SW's scope immediately,
// instead of waiting for reload/navigation.
event.waitUntil(self.clients.claim());
});

onfetch = (evt) => {
console.log(evt.request);

if (evt.request.url.endsWith('ping')) {
var encoder = new TextEncoder();

var bytes = 0;

var n = 0;

var abort = false;

let aborted = false;

var res;

const bc = new BroadcastChannel('downloads');

bc.onmessage = (e) => {
  console.log(e.data);
  if (e.data.abort) {
    abort = true;
  }
};

var controller = new AbortController();
var signal = controller.signal;
console.log(controller, signal);
signal.onabort = (e) => {
  console.log(
    `Event type:${e.type}\nEvent target:${e.target.constructor.name}`
  );
};
var readable = new ReadableStream({
  async pull(c) {
    if (n === 10 && !abort) {
      c.close();
      // console.log(await a.cancel('Download aborted!'));
      return;
    }
    const data = encoder.encode(n + '\n');
    bytes += data.buffer.byteLength;
    c.enqueue(data);
    bc.postMessage({ bytes, aborted });
    await new Promise((r) => setTimeout(r, 1000));
    ++n;
  },
  cancel(reason) {
    console.log(
      `readable cancel(reason):${reason.join(
        '\n'
      )}\nreadable ReadableStream.locked:${readable.locked}\na locked:${
        a.locked
      }\nb.locked:${b.locked}`
    );
  },
});

var [a, b] = readable.tee();
console.log({ readable, a, b });

async function cancelable() {
  if ('pipeTo' in b) {
    var writeable = new WritableStream({
      async write(v, c) {
        console.log(v);
        if (abort) {
          controller.abort();
          try {
            console.log(await a.cancel('Download aborted!'));
          } catch (e) {
            console.error(e);
          }
        }
      },
      abort(reason) {
        console.log(
          `abort(reason):${reason}\nWritableStream.locked:${writeable.locked}`
        );
      },
    });
    return b
      .pipeTo(writeable, { preventCancel: false, signal })
      .catch((e) => {
        console.log(
          `catch(e):${e}\nReadableStream.locked:${readable.locked}\nWritableStream.locked:${writeable.locked}`
        );
        bc.postMessage({ aborted: true });
        return 'Download aborted.';
      });
  } else {
    var reader = b.getReader();
    return reader.read().then(async function process({ value, done }) {
      if (done) {
        if (abort) {
          reader.releaseLock();
          reader.cancel();
          console.log(await a.cancel('Download aborted!'));
          bc.postMessage({ aborted: true });
        }
        return reader.closed.then((_) => 'Download aborted.');
      }

      return reader.read().then(process).catch(console.error);
    });
  }
}

var downloadable = cancelable().then((result) => {
  console.log({ result });
  const headers = {
    'content-disposition': 'attachment; filename="filename.txt"',
  };
  try {
    bc.postMessage({ done: true });
    bc.close();
    res = new Response(a, { headers, cache: 'no-store' });
    console.log(res);
    return res;
  } catch (e) {
    console.error(e);
  } finally {
    console.assert(res, { res });
  }
});

evt.respondWith(downloadable);

}
};

console.log('que?');

plnkr https://plnkr.co/edit/P2op0uo5YBA5eEEm?preview

Expected results:

BroadcastChannel handler in ServiceWorker should receive message from BroadcastChannel in main thread, however there does not appear to be any way to verify that process as console messages are not printed in ServiceWorker scope to Web Console.

BroastcastChannel should not survive unregistration of the ServiceWorker wherein the BroastcastChannel was initially created, continuing to postMessage() after multiple page reloads and code that unregisters all ServiceWorker's.

Hi, I will set a component to have a starting point of this. If not the correct component please feel free to route this ticket to the corresponding team.

Component: Untriaged → DOM: Service Workers
Product: Firefox → Core
Severity: -- → S3
Priority: -- → P3

Thank you for the reproduction! I believe the core of what's happening here is that a registration can continue to exist and its active ServiceWorker continue to run even after unregistration as long as the registration is controlling a page or still has pending functional events. The https://w3c.github.io/ServiceWorker/#try-clear-registration and the non-normative info box in https://w3c.github.io/ServiceWorker/#unregister-algorithm and https://w3c.github.io/ServiceWorker/#navigator-service-worker-unregister help explain this behavior.

This seems like a good test of some logging I'm adding to about:serviceworkers, so needinfo on myself to try and more thoroughly trace this.

I need to dig more into what's going on with the stream cancelable teed stream, but it seems likely the SW is remaining alive because of the downloadable stream passed to FetchEvent.respondWith. I do see from the current about:serviceworkers patch that we are aborting the intercepted fetch of the "ping" but the SW is only being terminated at what appears to be the conclusion of the waitUntil/respondWith grace period, so the cancellation/abort would seem not to be sufficiently propagated back to terminate the FetchEvent. There may be some respondWith buffering or use of an nsPipe involved that is effectively eating the cancellation.

Flags: needinfo?(bugmail)

Er, which is to say, any bug here is likely to be in the integration of FetchEvent and streams. The BroadcastChannel remaining alive seems almost certain to be due to the SW global remaining alive because the FetchEvent still is pending.

I believe we've recently seen a number of fixes to our stream usage that were the source of a failure for cancellation to propagate back to the worker, specifically Nika's fixes in https://phabricator.services.mozilla.com/D168481 for bug 1538754 which landed a few months ago. That explains why the ServiceWorker would continue to hang around, allowing the BroadcastChannel to continue to operate.

If you're still seeing delayed cancellation, please let me know (here on the bug is fine).

Status: UNCONFIRMED → RESOLVED
Closed: 1 year ago
Flags: needinfo?(bugmail)
Resolution: --- → WORKSFORME
You need to log in before you can comment on or make changes to this bug.