1// Copyright (C) 2021 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import m from 'mithril'; 16 17import {Actions} from '../common/actions'; 18import {tryGetTrace} from '../common/cache_manager'; 19 20import {loadAndroidBugToolInfo} from './android_bug_tool'; 21import {globals} from './globals'; 22import {showModal} from './modal'; 23import {Route, Router} from './router'; 24import {taskTracker} from './task_tracker'; 25 26 27export function maybeOpenTraceFromRoute(route: Route) { 28 if (route.args.s) { 29 // /?s=xxxx for permalinks. 30 globals.dispatch(Actions.loadPermalink({hash: route.args.s})); 31 return; 32 } 33 34 if (route.args.url) { 35 // /?url=https://commondatastorage.googleapis.com/bucket/trace 36 // This really works only for GCS because the Content Security Policy 37 // forbids any other url. 38 loadTraceFromUrl(route.args.url); 39 return; 40 } 41 42 if (route.args.openFromAndroidBugTool) { 43 // Handles interaction with the Android Bug Tool extension. See b/163421158. 44 openTraceFromAndroidBugTool(); 45 return; 46 } 47 48 if (route.args.p && route.page === '/record') { 49 // Handles backwards compatibility for old URLs (linked from various docs), 50 // generated before we switched URL scheme. e.g., 'record?p=power' vs 51 // 'record/power'. See b/191255021#comment2. 52 Router.navigate(`#!/record/${route.args.p}`); 53 return; 54 } 55 56 if (route.args.local_cache_key) { 57 // Handles the case of loading traces from the cache storage. 58 maybeOpenCachedTrace(route.args.local_cache_key); 59 return; 60 } 61} 62 63 64/* 65 * openCachedTrace(uuid) is called: (1) on startup, from frontend/index.ts; (2) 66 * every time the fragment changes (from Router.onRouteChange). 67 * This function must be idempotent (imagine this is called on every frame). 68 * It must take decision based on the app state, not on URL change events. 69 * Fragment changes are handled by the union of Router.onHashChange() and this 70 * function, as follows: 71 * 1. '' -> URL without a ?local_cache_key=xxx arg: 72 * - no effect (except redrawing) 73 * 2. URL without local_cache_key -> URL with local_cache_key: 74 * - Load cached trace (without prompting any dialog). 75 * - Show a (graceful) error dialog in the case of cache misses. 76 * 3. '' -> URL with a ?local_cache_key=xxx arg: 77 * - Same as case 2. 78 * 4. URL with local_cache_key=1 -> URL with local_cache_key=2: 79 * a) If 2 != uuid of the trace currently loaded (globals.state.traceUuid): 80 * - Ask the user if they intend to switch trace and load 2. 81 * b) If 2 == uuid of current trace (e.g., after a new trace has loaded): 82 * - no effect (except redrawing). 83 * 5. URL with local_cache_key -> URL without local_cache_key: 84 * - Redirect to ?local_cache_key=1234 where 1234 is the UUID of the previous 85 * URL (this might or might not match globals.state.traceUuid). 86 * 87 * Backward navigation cases: 88 * 6. URL without local_cache_key <- URL with local_cache_key: 89 * - Same as case 5. 90 * 7. URL with local_cache_key=1 <- URL with local_cache_key=2: 91 * - Same as case 4a: go back to local_cache_key=1 but ask the user to confirm. 92 * 8. landing page <- URL with local_cache_key: 93 * - Same as case 5: re-append the local_cache_key. 94 */ 95async function maybeOpenCachedTrace(traceUuid: string) { 96 if (traceUuid === globals.state.traceUuid) { 97 // Do nothing, matches the currently loaded trace. 98 return; 99 } 100 101 if (traceUuid === '') { 102 // This can happen if we switch from an empty UI state to an invalid UUID 103 // (e.g. due to a cache miss, below). This can also happen if the user just 104 // types /#!/viewer?local_cache_key=. 105 return; 106 } 107 108 // This handles the case when a trace T1 is loaded and then the url is set to 109 // ?local_cache_key=T2. In that case globals.state.traceUuid remains set to T1 110 // until T2 has been loaded by the trace processor (can take several seconds). 111 // This early out prevents to re-trigger the openTraceFromXXX() action if the 112 // URL changes (e.g. if the user navigates back/fwd) while the new trace is 113 // being loaded. 114 if (globals.state.engine !== undefined) { 115 const eng = globals.state.engine; 116 if (eng.source.type === 'ARRAY_BUFFER' && eng.source.uuid === traceUuid) { 117 return; 118 } 119 } 120 121 // Fetch the trace from the cache storage. If available load it. If not, show 122 // a dialog informing the user about the cache miss. 123 const maybeTrace = await tryGetTrace(traceUuid); 124 125 const navigateToOldTraceUuid = () => { 126 Router.navigate( 127 `#!/viewer?local_cache_key=${globals.state.traceUuid || ''}`); 128 }; 129 130 if (!maybeTrace) { 131 showModal({ 132 title: 'Could not find the trace in the cache storage', 133 content: m( 134 'div', 135 m('p', 136 'You are trying to load a cached trace by setting the ' + 137 '?local_cache_key argument in the URL.'), 138 m('p', 'Unfortunately the trace wasn\'t in the cache storage.'), 139 m('p', 140 'This can happen if a tab was discarded and wasn\'t opened ' + 141 'for too long, or if you just mis-pasted the URL.'), 142 m('pre', `Trace UUID: ${traceUuid}`), 143 ), 144 }); 145 navigateToOldTraceUuid(); 146 return; 147 } 148 149 // If the UI is in a blank state (no trace has been ever opened), just load 150 // the trace without showing any further dialog. This is the case of tab 151 // discarding, reloading or pasting a url with a local_cache_key in an empty 152 // instance. 153 if (globals.state.traceUuid === undefined) { 154 globals.dispatch(Actions.openTraceFromBuffer(maybeTrace)); 155 return; 156 } 157 158 // If, instead, another trace is loaded, ask confirmation to the user. 159 // Switching to another trace clears the UI state. It can be quite annoying to 160 // lose the UI state by accidentally navigating back too much. 161 let hasOpenedNewTrace = false; 162 163 await showModal({ 164 title: 'You are about to load a different trace and reset the UI state', 165 content: m( 166 'div', 167 m('p', 168 'You are seeing this because you either pasted a URL with ' + 169 'a different ?local_cache_key=xxx argument or because you hit ' + 170 'the history back/fwd button and reached a different trace.'), 171 m('p', 172 'If you continue another trace will be loaded and the UI ' + 173 'state will be cleared.'), 174 m('pre', 175 `Old trace: ${globals.state.traceUuid || '<no trace>'}\n` + 176 `New trace: ${traceUuid}`), 177 ), 178 buttons: [ 179 { 180 text: 'Continue', 181 id: 'trace_id_open', // Used by tests. 182 primary: true, 183 action: () => { 184 hasOpenedNewTrace = true; 185 globals.dispatch(Actions.openTraceFromBuffer(maybeTrace)); 186 }, 187 }, 188 {text: 'Cancel'}, 189 ], 190 }); 191 192 if (!hasOpenedNewTrace) { 193 // We handle this after the modal await rather than in the cancel button 194 // action so this has effect even if the user clicks Esc or clicks outside 195 // of the modal dialog and dismisses it. 196 navigateToOldTraceUuid(); 197 } 198} 199 200function loadTraceFromUrl(url: string) { 201 const isLocalhostTraceUrl = 202 ['127.0.0.1', 'localhost'].includes((new URL(url)).hostname); 203 204 if (isLocalhostTraceUrl) { 205 // This handles the special case of tools/record_android_trace serving the 206 // traces from a local webserver and killing it immediately after having 207 // seen the HTTP GET request. In those cases store the trace as a file, so 208 // when users click on share we don't fail the re-fetch(). 209 const fileName = url.split('/').pop() || 'local_trace.pftrace'; 210 const request = fetch(url) 211 .then((response) => response.blob()) 212 .then((blob) => { 213 globals.dispatch(Actions.openTraceFromFile({ 214 file: new File([blob], fileName), 215 })); 216 }) 217 .catch((e) => alert(`Could not load local trace ${e}`)); 218 taskTracker.trackPromise(request, 'Downloading local trace'); 219 } else { 220 globals.dispatch(Actions.openTraceFromUrl({url})); 221 } 222} 223 224function openTraceFromAndroidBugTool() { 225 // TODO(hjd): Unify updateStatus and TaskTracker 226 globals.dispatch(Actions.updateStatus( 227 {msg: 'Loading trace from ABT extension', timestamp: Date.now() / 1000})); 228 const loadInfo = loadAndroidBugToolInfo(); 229 taskTracker.trackPromise(loadInfo, 'Loading trace from ABT extension'); 230 loadInfo 231 .then((info) => { 232 globals.dispatch(Actions.openTraceFromFile({ 233 file: info.file, 234 })); 235 }) 236 .catch((e) => { 237 console.error(e); 238 }); 239} 240