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 | })(); |