diff options
Diffstat (limited to '')
5 files changed, 409 insertions, 0 deletions
diff --git a/dom/serviceworkers/test/download/window.html b/dom/serviceworkers/test/download/window.html new file mode 100644 index 0000000000..7d7893e0e6 --- /dev/null +++ b/dom/serviceworkers/test/download/window.html @@ -0,0 +1,46 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> +</head> +<body> +<script type="text/javascript"> + +function wait_until_controlled() { + return new Promise(function(resolve) { + if (navigator.serviceWorker.controller) { + return resolve(); + } + navigator.serviceWorker.addEventListener('controllerchange', function onController() { + if (navigator.serviceWorker.controller) { + navigator.serviceWorker.removeEventListener('controllerchange', onController); + return resolve(); + } + }); + }); +} +addEventListener('load', function(event) { + var registration; + navigator.serviceWorker.register('worker.js').then(function(swr) { + registration = swr; + + // While the iframe below is a navigation, we still wait until we are + // controlled here. We want an active client to hold the service worker + // alive since it calls unregister() on itself. + return wait_until_controlled(); + + }).then(function() { + var frame = document.createElement('iframe'); + document.body.appendChild(frame); + frame.src = 'fake_download'; + + // The service worker is unregistered in the fetch event. The window and + // frame are cleaned up from the browser chrome script. + }); +}); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/download/worker.js b/dom/serviceworkers/test/download/worker.js new file mode 100644 index 0000000000..71fb502f1c --- /dev/null +++ b/dom/serviceworkers/test/download/worker.js @@ -0,0 +1,34 @@ +addEventListener("install", function(evt) { + evt.waitUntil(self.skipWaiting()); +}); + +addEventListener("activate", function(evt) { + // We claim the current clients in order to ensure that we have an + // active client when we call unregister in the fetch handler. Otherwise + // the unregister() can kill the current worker before returning a + // response. + evt.waitUntil(clients.claim()); +}); + +addEventListener("fetch", function(evt) { + // This worker may live long enough to receive a fetch event from the next + // test. Just pass such requests through to the network. + if (!evt.request.url.includes("fake_download")) { + return; + } + + // We should only get a single download fetch event. Automatically unregister. + evt.respondWith( + registration.unregister().then(function() { + return new Response("service worker generated download", { + headers: { + "Content-Disposition": 'attachment; filename="fake_download.bin"', + // Prevent the default text editor from being launched + "Content-Type": "application/octet-stream", + // fake encoding header that should have no effect + "Content-Encoding": "gzip", + }, + }); + }) + ); +}); diff --git a/dom/serviceworkers/test/download_canceled/page_download_canceled.html b/dom/serviceworkers/test/download_canceled/page_download_canceled.html new file mode 100644 index 0000000000..e3904c4967 --- /dev/null +++ b/dom/serviceworkers/test/download_canceled/page_download_canceled.html @@ -0,0 +1,58 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> +</head> +<body> + +<script src="../utils.js"></script> +<script type="text/javascript"> +function wait_until_controlled() { + return new Promise(function(resolve) { + if (navigator.serviceWorker.controller) { + return resolve('controlled'); + } + navigator.serviceWorker.addEventListener('controllerchange', function onController() { + if (navigator.serviceWorker.controller) { + navigator.serviceWorker.removeEventListener('controllerchange', onController); + return resolve('controlled'); + } + }); + }); +} +addEventListener('load', async function(event) { + window.controlled = wait_until_controlled(); + window.registration = + await navigator.serviceWorker.register('sw_download_canceled.js'); + let sw = registration.installing || registration.waiting || + registration.active; + await waitForState(sw, 'activated'); + sw.postMessage('claim'); +}); + +// Place to hold promises for stream closures reported by the SW. +window.streamClosed = {}; + +// The ServiceWorker will postMessage to this BroadcastChannel when the streams +// are closed. (Alternately, the SW could have used the clients API to post at +// us, but the mechanism by which that operates would be different when this +// test is uplifted, and it's desirable to avoid timing changes.) +// +// The browser test will use this promise to wait on stream shutdown. +window.swStreamChannel = new BroadcastChannel("stream-closed"); +function trackStreamClosure(path) { + let resolve; + const promise = new Promise(r => { resolve = r }); + window.streamClosed[path] = { promise, resolve }; +} +window.swStreamChannel.onmessage = ({ data }) => { + window.streamClosed[data.what].resolve(data); +}; +</script> + +</body> +</html> diff --git a/dom/serviceworkers/test/download_canceled/server-stream-download.sjs b/dom/serviceworkers/test/download_canceled/server-stream-download.sjs new file mode 100644 index 0000000000..efcc310420 --- /dev/null +++ b/dom/serviceworkers/test/download_canceled/server-stream-download.sjs @@ -0,0 +1,121 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +Cu.import("resource://gre/modules/Timer.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +// stolen from file_blocked_script.sjs +function setGlobalState(data, key) +{ + x = { data: data, QueryInterface: function(iid) { return this } }; + x.wrappedJSObject = x; + setObjectState(key, x); +} + +function getGlobalState(key) +{ + var data; + getObjectState(key, function(x) { + data = x && x.wrappedJSObject.data; + }); + return data; +} + +/* + * We want to let the sw_download_canceled.js service worker know when the + * stream was canceled. To this end, we let it issue a monitor request which we + * fulfill when the stream has been canceled. In order to coordinate between + * multiple requests, we use the getObjectState/setObjectState mechanism that + * httpd.js exposes to let data be shared and/or persist between requests. We + * handle both possible orderings of the requests because we currently don't + * try and impose an ordering between the two requests as issued by the SW, and + * file_blocked_script.sjs encourages us to do this, but we probably could order + * them. + */ +const MONITOR_KEY = "stream-monitor"; +function completeMonitorResponse(response, data) { + response.write(JSON.stringify(data)); + response.finish(); +} +function handleMonitorRequest(request, response) { + response.setHeader("Content-Type", "application/json"); + response.setStatusLine(request.httpVersion, 200, "Found"); + + response.processAsync(); + // Necessary to cause the headers to be flushed; that or touching the + // bodyOutputStream getter. + response.write(""); + dump("server-stream-download.js: monitor headers issued\n"); + + const alreadyCompleted = getGlobalState(MONITOR_KEY); + if (alreadyCompleted) { + completeMonitorResponse(response, alreadyCompleted); + setGlobalState(null, MONITOR_KEY); + } else { + setGlobalState(response, MONITOR_KEY); + } +} + +const MAX_TICK_COUNT = 3000; +const TICK_INTERVAL = 2; +function handleStreamRequest(request, response) { + const name = "server-stream-download"; + + // Create some payload to send. + let strChunk = + 'Static routes are the future of ServiceWorkers! So say we all!\n'; + while (strChunk.length < 1024) { + strChunk += strChunk; + } + + response.setHeader("Content-Disposition", `attachment; filename="${name}"`); + response.setHeader("Content-Type", `application/octet-stream; name="${name}"`); + response.setHeader("Content-Length", `${strChunk.length * MAX_TICK_COUNT}`); + response.setStatusLine(request.httpVersion, 200, "Found"); + + response.processAsync(); + response.write(strChunk); + dump("server-stream-download.js: stream headers + first payload issued\n"); + + let count = 0; + let intervalId; + function closeStream(why, message) { + dump("server-stream-download.js: closing stream: " + why + "\n"); + clearInterval(intervalId); + response.finish(); + + const data = { why, message }; + const monitorResponse = getGlobalState(MONITOR_KEY); + if (monitorResponse) { + completeMonitorResponse(monitorResponse, data); + setGlobalState(null, MONITOR_KEY); + } else { + setGlobalState(data, MONITOR_KEY); + } + } + function tick() { + try { + // bound worst-case behavior. + if (count++ > MAX_TICK_COUNT) { + closeStream("timeout", "timeout"); + return; + } + response.write(strChunk); + } catch(e) { + closeStream("canceled", e.message); + } + } + intervalId = setInterval(tick, TICK_INTERVAL); +} + +Components.utils.importGlobalProperties(["URLSearchParams"]); +function handleRequest(request, response) { + dump("server-stream-download.js: processing request for " + request.path + + "?" + request.queryString + "\n"); + const query = new URLSearchParams(request.queryString); + if (query.has("monitor")) { + handleMonitorRequest(request, response); + } else { + handleStreamRequest(request, response); + } +}
\ No newline at end of file diff --git a/dom/serviceworkers/test/download_canceled/sw_download_canceled.js b/dom/serviceworkers/test/download_canceled/sw_download_canceled.js new file mode 100644 index 0000000000..5d9d5f9bfd --- /dev/null +++ b/dom/serviceworkers/test/download_canceled/sw_download_canceled.js @@ -0,0 +1,150 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This file is derived from :bkelly's https://glitch.com/edit/#!/html-sw-stream + +addEventListener("install", evt => { + evt.waitUntil(self.skipWaiting()); +}); + +// Create a BroadcastChannel to notify when we have closed our streams. +const channel = new BroadcastChannel("stream-closed"); + +const MAX_TICK_COUNT = 3000; +const TICK_INTERVAL = 4; +/** + * Generate a continuous stream of data at a sufficiently high frequency that a + * there"s a good chance of racing channel cancellation. + */ +function handleStream(evt, filename) { + // Create some payload to send. + const encoder = new TextEncoder(); + let strChunk = + "Static routes are the future of ServiceWorkers! So say we all!\n"; + while (strChunk.length < 1024) { + strChunk += strChunk; + } + const dataChunk = encoder.encode(strChunk); + + evt.waitUntil( + new Promise(resolve => { + let body = new ReadableStream({ + start: controller => { + const closeStream = why => { + console.log("closing stream: " + JSON.stringify(why) + "\n"); + clearInterval(intervalId); + resolve(); + // In event of error, the controller will automatically have closed. + if (why.why != "canceled") { + try { + controller.close(); + } catch (ex) { + // If we thought we should cancel but experienced a problem, + // that's a different kind of failure and we need to report it. + // (If we didn't catch the exception here, we'd end up erroneously + // in the tick() method's canceled handler.) + channel.postMessage({ + what: filename, + why: "close-failure", + message: ex.message, + ticks: why.ticks, + }); + return; + } + } + // Post prior to performing any attempt to close... + channel.postMessage(why); + }; + + controller.enqueue(dataChunk); + let count = 0; + let intervalId; + function tick() { + try { + // bound worst-case behavior. + if (count++ > MAX_TICK_COUNT) { + closeStream({ + what: filename, + why: "timeout", + message: "timeout", + ticks: count, + }); + return; + } + controller.enqueue(dataChunk); + } catch (e) { + closeStream({ + what: filename, + why: "canceled", + message: e.message, + ticks: count, + }); + } + } + // Alternately, streams' pull mechanism could be used here, but this + // test doesn't so much want to saturate the stream as to make sure the + // data is at least flowing a little bit. (Also, the author had some + // concern about slowing down the test by overwhelming the event loop + // and concern that we might not have sufficent back-pressure plumbed + // through and an infinite pipe might make bad things happen.) + intervalId = setInterval(tick, TICK_INTERVAL); + tick(); + }, + }); + evt.respondWith( + new Response(body, { + headers: { + "Content-Disposition": `attachment; filename="${filename}"`, + "Content-Type": "application/octet-stream", + }, + }) + ); + }) + ); +} + +/** + * Use an .sjs to generate a similar stream of data to the above, passing the + * response through directly. Because we're handing off the response but also + * want to be able to report when cancellation occurs, we create a second, + * overlapping long-poll style fetch that will not finish resolving until the + * .sjs experiences closure of its socket and terminates the payload stream. + */ +function handlePassThrough(evt, filename) { + evt.waitUntil( + (async () => { + console.log("issuing monitor fetch request"); + const response = await fetch("server-stream-download.sjs?monitor"); + console.log("monitor headers received, awaiting body"); + const data = await response.json(); + console.log("passthrough monitor fetch completed, notifying."); + channel.postMessage({ + what: filename, + why: data.why, + message: data.message, + }); + })() + ); + evt.respondWith( + fetch("server-stream-download.sjs").then(response => { + console.log("server-stream-download.sjs Response received, propagating"); + return response; + }) + ); +} + +addEventListener("fetch", evt => { + console.log(`SW processing fetch of ${evt.request.url}`); + if (evt.request.url.includes("sw-stream-download")) { + return handleStream(evt, "sw-stream-download"); + } + if (evt.request.url.includes("sw-passthrough-download")) { + return handlePassThrough(evt, "sw-passthrough-download"); + } +}); + +addEventListener("message", evt => { + if (evt.data === "claim") { + evt.waitUntil(clients.claim()); + } +}); |