Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/www/js/safe.js

5.3 KiB, 1 run

created by r2519314175:1435, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1/* safe.js — boot without the machinery that is under suspicion.
2 *
3 * WHY THIS EXISTS. A phone reported: unlock with a passkey, the app appears for
4 * about a second, and the lock screen is back. Six diagnoses have been made
5 * across four sessions and all six were wrong, because a phone has no console
6 * and every explanation was therefore a guess made from reading source. The
7 * trail in breadcrumb.js ended that for what the app SAYS; this ends it for
8 * what the app DOES.
9 *
10 * A safe start does one thing: the sync engine does not run. That is the whole
11 * of it, and the narrowness is the point. If the app then stays up, the cause is
12 * inside what was skipped, and that is a cleaner answer than any further reading
13 * of code. If it loops anyway, sync is exonerated -- which is worth just as much,
14 * and nothing else so far has been able to say either.
15 *
16 * IT ARMS ITSELF. Three boots inside ninety seconds is not something a person
17 * does, and a user whose app will not stay open long enough to be used cannot be
18 * asked to find a button. So the loop turns it on and the app says so, loudly and
19 * permanently, on a chip that turns it back off again. A quiet safe mode would be
20 * a device that silently stopped syncing, which is worse than the bug.
21 */
22(function () {
23 'use strict';
24
25 var KEY = 'daimond-safe-mode'; // '1' while the app must start without sync
26 var WKEY = 'daimond-safe-why'; // 'auto' or 'user', for the trail and the chip
27 var BOOTS = 3; // in the window below, before it arms itself
28 var WINDOW_MS = 90000;
29
30 function trail(w, d) { try { window.DaimondTrail.note(w, d); } catch (e) {} }
31
32 function on() {
33 try { return localStorage.getItem(KEY) === '1'; } catch (e) { return false; }
34 }
35
36 function why() {
37 try { return localStorage.getItem(WKEY) || ''; } catch (e) { return ''; }
38 }
39
40 /// Turn a safe start on or off. `reason` is 'auto' (the loop detector) or
41 /// 'user' (the chip), and it is kept because the two read differently to
42 /// somebody looking at a trail: one is the app protecting itself, the other
43 /// is a person choosing.
44 function set(v, reason) {
45 try {
46 if (v) {
47 localStorage.setItem(KEY, '1');
48 localStorage.setItem(WKEY, reason || 'user');
49 } else {
50 localStorage.removeItem(KEY);
51 localStorage.removeItem(WKEY);
52 }
53 } catch (e) { /* storage refused: it simply does not persist */ }
54 trail(v ? 'SAFE MODE on' : 'safe mode off', reason || 'user');
55 }
56
57 /// How many times this app has started in the last ninety seconds.
58 ///
59 /// Read from the trail rather than from a counter of its own, because the
60 /// trail is the thing that survives the tab being killed -- and a tab being
61 /// killed is precisely the event being counted.
62 function bootsRecently() {
63 var rows = [];
64 try { rows = (window.DaimondTrail && DaimondTrail.rows()) || []; } catch (e) { return 0; }
65 var now = Date.now(), n = 0;
66 for (var i = 0; i < rows.length; i++) {
67 var r = rows[i];
68 if (r && r.w === 'boot' && now - r.t < WINDOW_MS) n++;
69 }
70 return n;
71 }
72
73 /// How many of those starts followed a tab that was KILLED rather than one
74 /// that went away tidily.
75 ///
76 /// THIS IS THE WHOLE ARMING RULE, and counting boots alone was not good
77 /// enough. Three starts in ninety seconds is also a developer pressing
78 /// reload, a person on a bad connection, or a verifier driving the app --
79 /// and arming a safe start on any of those turns sync off for somebody whose
80 /// app is fine. It would have done exactly that inside this project's own
81 /// test suite.
82 ///
83 /// The distinction is the one hard fact four sessions of hunting have
84 /// established: THE LOOPING TAB IS NOT RELOADING. A reload -- deliberate,
85 /// forced or scripted -- fires `pagehide` on the way out, every time. The
86 /// phone's trail has never contained one. So a boot with no `pagehide`
87 /// between it and the boot before it is a tab that was taken away, which is
88 /// the only thing a safe start is for.
89 function killedStarts() {
90 var rows = [];
91 try { rows = (window.DaimondTrail && DaimondTrail.rows()) || []; } catch (e) { return 0; }
92 var now = Date.now(), n = 0, sawExit = true; // nothing before the first boot
93 for (var i = 0; i < rows.length; i++) {
94 var r = rows[i];
95 if (!r) continue;
96 if (r.w === 'pagehide') { sawExit = true; continue; }
97 if (r.w !== 'boot') continue;
98 if (!sawExit && now - r.t < WINDOW_MS) n++;
99 sawExit = false;
100 }
101 return n;
102 }
103
104 // Arm on the way in, before anything else has had a chance to start. This
105 // runs at script load and not on DOMContentLoaded: sync.js consults `on()`
106 // from `ready()`, and `ready()` is asked the moment a session exists.
107 //
108 // On KILLED starts, not merely on starts. See `killedStarts`: a reload of any
109 // kind leaves a `pagehide` behind it, and turning sync off for somebody who
110 // pressed reload three times would be a worse bug than the one being hunted.
111 if (!on() && killedStarts() >= BOOTS) {
112 set(true, 'auto');
113 }
114 // Say so on EVERY boot, not only the one that armed it. A trail that showed
115 // the arming line once, two hundred rows ago, would leave every later cycle
116 // looking like an ordinary boot that happened not to sync.
117 if (on()) trail('safe start', why() === 'auto' ? 'armed by the loop' : 'chosen by the user');
118
119 window.DaimondSafe = {
120 on: on,
121 why: why,
122 set: set,
123 boots: bootsRecently,
124 killed: killedStarts,
125 };
126})();