File: browser_sw_lifetime_extension.js

package info (click to toggle)
firefox 143.0.3-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 4,617,328 kB
  • sloc: cpp: 7,478,492; javascript: 6,417,157; ansic: 3,720,058; python: 1,396,372; xml: 627,523; asm: 438,677; java: 186,156; sh: 63,477; makefile: 19,171; objc: 13,059; perl: 12,983; yacc: 4,583; cs: 3,846; pascal: 3,405; lex: 1,720; ruby: 1,003; exp: 762; php: 436; lisp: 258; awk: 247; sql: 66; sed: 53; csh: 10
file content (397 lines) | stat: -rw-r--r-- 15,198 bytes parent folder | download | duplicates (11)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */

/**
 * Verify that ServiceWorkers interacting with each other can only set/extend
 * the lifetime of other ServiceWorkers to match their own lifetime, while
 * other clients that correspond to an open tab can provide fresh lifetime
 * extensions.  The specific scenario we want to ensure is impossible is two
 * ServiceWorkers interacting to keep each other alive indefinitely without the
 * involvement of a live tab.
 *
 * ### Test Machinery
 *
 * #### Determining Lifetimes
 *
 * In order to determine the lifetime deadline of ServiceWorkers, we have
 * exposed `lifetimeDeadline` on nsIServiceWorkerInfo.  This is a value
 * maintained exclusively by the ServiceWorkerManager on the
 * ServiceWorkerPrivate instances corresponding to each ServiceWorker.  It's not
 * something the ServiceWorker workers know, so it's appropriate to implement
 * this as a browser test with most of the logic in the parent process.
 *
 * #### Communicating with ServiceWorkers
 *
 * We use BroadcastChannel to communicate from a page in the test origin that
 * does not match any ServiceWorker scopes with the ServiceWorkers under test.
 * BroadcastChannel explicitly will not do anything to extend the lifetime of
 * the ServiceWorkers and is much simpler for us to use than trying to transfer
 * MessagePorts around since that would involve ServiceWorker.postMessage()
 * which will extend the ServiceWorker lifetime if used from a window client.
 *
 * #### Making a Service Worker that can Keep Updating
 *
 * ServiceWorker update checks do a byte-wise comparison; if the underlying
 * script/imports have not changed, the update process will be aborted.  So we
 * use an .sjs script that generates a script payload that has a "version" that
 * updates every time the script is fetched.
 *
 * Note that one has to be careful with an .sjs like that because
 * non-subresource fetch events will automatically run a soft update check, and
 * functional events will run a soft update if the registration is stale.  We
 * never expect the registration to be stale in our tests because 24 hours won't
 * have passed, but page navigation is obviously a very common testing thing.
 * We ensure we don't perform any intercepted navigations.
 *
 * To minimize code duplication, we have that script look like:
 * ```
 * var version = ${COUNTER};
 * importScripts("sw_inter_sw_postmessage.js");
 * ```
 */

/* import-globals-from browser_head.js */
Services.scriptloader.loadSubScript(
  "chrome://mochitests/content/browser/dom/serviceworkers/test/browser_head.js",
  this
);

const TEST_ORIGIN = "https://test1.example.org";

/**
 * Install equivalent ServiceWorkers on 2 scopes that will message each other
 * on request via BroadcastChannel message, verifying that the ServiceWorkers
 * cannot extend each other's deadlines beyond their own deadline.
 */
async function test_post_message_between_service_workers() {
  info("## Installing the ServiceWorkers");
  const aSwDesc = {
    origin: TEST_ORIGIN,
    scope: "sw-a",
    script: "sw_inter_sw_postmessage.js?a",
  };
  const bSwDesc = {
    origin: TEST_ORIGIN,
    scope: "sw-b",
    script: "sw_inter_sw_postmessage.js?b",
  };

  // Wipe the origin for cleanup; this will remove the registrations too.
  registerCleanupFunction(async () => {
    await clear_qm_origin_group_via_clearData(TEST_ORIGIN);
  });

  const aReg = await install_sw(aSwDesc);
  const bReg = await install_sw(bSwDesc);

  info("## Terminating the ServiceWorkers");
  // We always want to wait for the workers to be fully terminated because they
  // listen for our BroadcastChannel messages and until ServiceWorkers are no
  // longer owned by the main thread, a race is possible if we don't wait.
  const aSWInfo = aReg.activeWorker;
  await aSWInfo.terminateWorker();

  const bSWInfo = bReg.activeWorker;
  await bSWInfo.terminateWorker();

  is(aSWInfo.lifetimeDeadline, 0, "SW A not running.");
  is(bSWInfo.lifetimeDeadline, 0, "SW B not running.");
  is(aSWInfo.launchCount, 1, "SW A did run once, though.");
  is(bSWInfo.launchCount, 1, "SW B did run once, though.");

  info("## Beginning PostMessage Checks");
  let testStart = Cu.now();

  const { closeHelperTab, postMessageScopeAndWaitFor, broadcastAndWaitFor } =
    await createMessagingHelperTab(TEST_ORIGIN, "inter-sw-postmessage");
  registerCleanupFunction(closeHelperTab);

  // - Have the helper page postMessage SW A to spawn it, waiting for SW A to report in.
  await postMessageScopeAndWaitFor(
    "sw-a",
    "Hello, the contents of this message don't matter!",
    "a:received-post-message-from:wc-helper"
  );

  let aLifetime = aSWInfo.lifetimeDeadline;
  Assert.greater(
    aLifetime,
    testStart,
    "SW A should be running with a deadline in the future."
  );
  is(bSWInfo.lifetimeDeadline, 0, "SW B not running.");
  is(aSWInfo.launchCount, 2, "SW A was launched by our postMessage.");
  is(bSWInfo.launchCount, 1, "SW B has not been re-launched yet.");

  // - Ask SW A to postMessage SW B, waiting for SW B to report in.
  await broadcastAndWaitFor(
    "a:post-message-to:reg-sw-b",
    "b:received-post-message-from:sw-a"
  );

  is(
    bSWInfo.lifetimeDeadline,
    aLifetime,
    "SW B has same deadline as SW A after cross-SW postMessage"
  );

  // - Ask SW B to postMessage SW A, waiting for SW A to report in.
  await broadcastAndWaitFor(
    "b:post-message-to:reg-sw-a",
    "a:received-post-message-from:sw-b"
  );

  is(
    bSWInfo.lifetimeDeadline,
    aLifetime,
    "SW A still has the same deadline after B's cross-SW postMessage"
  );
  is(bSWInfo.launchCount, 2, "SW B was re-launched.");
  is(aSWInfo.lifetimeDeadline, aLifetime, "SW A deadline unchanged");
  is(aSWInfo.launchCount, 2, "SW A launch count unchanged.");

  // - Have the helper page postMessage SW B, waiting for B to report in.
  await postMessageScopeAndWaitFor(
    "sw-b",
    "Hello, the contents of this message don't matter!",
    "b:received-post-message-from:wc-helper"
  );
  let bLifetime = bSWInfo.lifetimeDeadline;
  Assert.greater(
    bLifetime,
    aLifetime,
    "SW B should have a deadline after A's after the page postMessage"
  );
  is(aSWInfo.lifetimeDeadline, aLifetime, "SW A deadline unchanged");
  is(aSWInfo.launchCount, 2, "SW A launch count unchanged.");

  // - Have SW B postMessage SW A, waiting for SW A to report in.
  await broadcastAndWaitFor(
    "b:post-message-to:reg-sw-a",
    "a:received-post-message-from:sw-b"
  );
  is(
    aSWInfo.lifetimeDeadline,
    bLifetime,
    "SW A should have the same deadline as B after B's cross-SW postMessage"
  );
  is(aSWInfo.launchCount, 2, "SW A launch count unchanged.");
  is(bSWInfo.lifetimeDeadline, bLifetime, "SW B deadline unchanged");
  is(bSWInfo.launchCount, 2, "SW B launch count unchanged.");
}
add_task(test_post_message_between_service_workers);

/**
 * Install a ServiceWorker that will update itself on request via
 * BroadcastChannel message and verify that the lifetimes of the new updated
 * ServiceWorker are the same as the requesting ServiceWorker.  We also want to
 * verify that a request to update from a page gets a fresh lifetime.
 */
async function test_eternally_updating_service_worker() {
  info("## Installing the Eternally Updating ServiceWorker");
  const swDesc = {
    origin: TEST_ORIGIN,
    scope: "sw-u",
    script: "sw_always_updating_inter_sw_postmessage.sjs?u",
  };

  // Wipe the origin for cleanup; this will remove the registrations too.
  registerCleanupFunction(async () => {
    await clear_qm_origin_group_via_clearData(TEST_ORIGIN);
  });

  let testStart = Cu.now();

  const reg = await install_sw(swDesc);
  const firstInfo = reg.activeWorker;
  const firstLifetime = firstInfo.lifetimeDeadline;

  Assert.greater(
    firstLifetime,
    testStart,
    "The first generation should be running with a deadline in the future."
  );

  const { closeHelperTab, broadcastAndWaitFor, updateScopeAndWaitFor } =
    await createMessagingHelperTab(TEST_ORIGIN, "inter-sw-postmessage");
  registerCleanupFunction(closeHelperTab);

  info("## Beginning Self-Update Requests");

  // - Ask 1st gen SW to update the reg, 2nd gen SW should have same lifetime.
  await broadcastAndWaitFor("u#1:update-reg:sw-u", "u:version-activated:2");

  // We don't have to worry about async races here because these state changes
  // are authoritative on the parent process main thread, which is where we are
  // running; we can only have heard about the activation via BroadcastChannel
  // after the state has updated.
  const secondInfo = reg.activeWorker;
  const secondLifetime = secondInfo.lifetimeDeadline;
  is(firstLifetime, secondLifetime, "Version 2 has same lifetime as 1.");

  // - Ask 2nd gen SW to update the reg, 3rd gen SW should have same lifetime.
  await broadcastAndWaitFor("u#2:update-reg:sw-u", "u:version-activated:3");

  const thirdInfo = reg.activeWorker;
  const thirdLifetime = thirdInfo.lifetimeDeadline;
  is(firstLifetime, thirdLifetime, "Version 3 has same lifetime as 1 and 2.");

  // - Ask the helper page to update the reg, 4th gen SW should have fresh life.
  await updateScopeAndWaitFor("sw-u", "u:version-activated:4");

  const fourthInfo = reg.activeWorker;
  const fourthLifetime = fourthInfo.lifetimeDeadline;
  Assert.greater(
    fourthLifetime,
    firstLifetime,
    "Version 4 has a fresh lifetime."
  );

  // - Ask 4th gen SW to update the reg, 5th gen SW should have same lifetime.
  await broadcastAndWaitFor("u#4:update-reg:sw-u", "u:version-activated:5");

  const fifthInfo = reg.activeWorker;
  const fifthLifetime = fifthInfo.lifetimeDeadline;
  is(fourthLifetime, fifthLifetime, "Version 5 has same lifetime as 4.");
}
add_task(test_eternally_updating_service_worker);

/**
 * Install a ServiceWorker that will create a new registration and verify that
 * the lifetime for the new ServiceWorker being installed for the new
 * registration is the same as the requesting ServiceWorker.
 */
async function test_service_worker_creating_new_registrations() {
  info("## Installing the Bootstrap ServiceWorker");
  const cSwDesc = {
    origin: TEST_ORIGIN,
    scope: "sw-c",
    script: "sw_inter_sw_postmessage.js?c",
  };

  // Wipe the origin for cleanup; this will remove the registrations too.
  registerCleanupFunction(async () => {
    await clear_qm_origin_group_via_clearData(TEST_ORIGIN);
  });

  let testStart = Cu.now();

  const cReg = await install_sw(cSwDesc);
  const cSWInfo = cReg.activeWorker;
  const cLifetime = cSWInfo.lifetimeDeadline;

  Assert.greater(
    cLifetime,
    testStart,
    "The bootstrap registration worker should be running with a deadline in the future."
  );

  const { closeHelperTab, broadcastAndWaitFor, updateScopeAndWaitFor } =
    await createMessagingHelperTab(TEST_ORIGIN, "inter-sw-postmessage");
  registerCleanupFunction(closeHelperTab);

  info("## Beginning Propagating Registrations");

  // - Ask the SW to install a ServiceWorker at scope d
  await broadcastAndWaitFor("c:install-reg:d", "d:version-activated:0");

  const dSwDesc = {
    origin: TEST_ORIGIN,
    scope: "sw-d",
    script: "sw_inter_sw_postmessage.js?d",
  };
  const dReg = swm_lookup_reg(dSwDesc);
  ok(dReg, "found the new 'd' registration");
  const dSWInfo = dReg.activeWorker;
  ok(dSWInfo, "The 'd' registration has the expected active worker.");
  const dLifetime = dSWInfo.lifetimeDeadline;
  is(
    dLifetime,
    cLifetime,
    "The new worker has the same lifetime as the worker that triggered its installation."
  );
}
add_task(test_service_worker_creating_new_registrations);

/**
 * In bug 1927247 a ServiceWorker respawned sufficiently soon after its
 * termination resulted in a defensive content-process crash when the new SW's
 * ClientSource was registered with the same Client Id while the old SW's
 * ClientSource still existed.  We synthetically induce this situation through
 * use of nsIServiceWorkerInfo::terminateWorker() immediately followed by use of
 * nsIServiceWorkerInfo::attachDebugger().
 */
async function test_respawn_immediately_after_termination() {
  // Make WorkerTestUtils work in the SW.
  await SpecialPowers.pushPrefEnv({
    set: [["dom.workers.testing.enabled", true]],
  });

  // We need to ensure all ServiceWorkers are spawned consistently in the same
  // process because of the trick we do with workerrefs and using the observer
  // service, so force us to only use a single process if fission is not on.
  if (!Services.appinfo.fissionAutostart) {
    // Force use of only a single process
    await SpecialPowers.pushPrefEnv({
      set: [["dom.ipc.processCount", 1]],
    });
  }

  info("## Installing the ServiceWorker we will terminate and respawn");
  const tSwDesc = {
    origin: TEST_ORIGIN,
    scope: "sw-t",
    script: "sw_inter_sw_postmessage.js?t",
  };

  // Wipe the origin for cleanup; this will remove the registrations too.
  registerCleanupFunction(async () => {
    await clear_qm_origin_group_via_clearData(TEST_ORIGIN);
  });

  const tReg = await install_sw(tSwDesc);
  const tSWInfo = tReg.activeWorker;

  info("## Induce the SW to acquire a WorkerRef that prevents shutdown.");

  const { closeHelperTab, broadcastAndWaitFor, postMessageScopeAndWaitFor } =
    await createMessagingHelperTab(TEST_ORIGIN, "inter-sw-postmessage");
  registerCleanupFunction(closeHelperTab);

  // Tell the ServiceWorker to block on a monitor that will prevent the worker
  // from transitioning to the Canceling state and notifying its WorkerRefs,
  // thereby preventing the ClientManagerChild from beginning teardown of itself
  // and thereby the ClientSourceChild.  The monitor will be released when we
  // cause "serviceworker-t-release" to be notified on that process's main
  // thread.
  await broadcastAndWaitFor(
    "t:block:serviceworker-t-release",
    "t:blocking:serviceworker-t-release"
  );

  info("## Terminating and respawning the ServiceWorker via attachDebugger");
  // We must not await the termination if we want to create the lifetime overlap
  const terminationPromise = tSWInfo.terminateWorker();
  // Message the ServiceWorker to cause it to spawn, waiting for the newly
  // spawned ServiceWorker to indicate it is alive and running.
  await postMessageScopeAndWaitFor(
    "sw-t",
    "Hello, the contents of this message don't matter!",
    "t:received-post-message-from:wc-helper"
  );

  // Tell the successor to generate an observer notification that will release
  // the ThreadSafeWorkerRef.  Note that this does assume the ServiceWorker is
  // placed in the same process as its predecessor.  When isolation is enabled,
  // like on desktop, this will always be the same process because there will
  // only be the one possible process.  "browser" tests like this are only run
  // on desktop, never on Android!  But if we weren't isolating,
  await broadcastAndWaitFor(
    "t:notify-observer:serviceworker-t-release",
    "t:notified-observer:serviceworker-t-release"
  );

  info("## Awaiting the termination");
  await terminationPromise;
}
add_task(test_respawn_immediately_after_termination);