// Copyright (C) 2021 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import * as m from 'mithril'; import {Actions} from '../common/actions'; import {tryGetTrace} from '../common/cache_manager'; import {loadAndroidBugToolInfo} from './android_bug_tool'; import {globals} from './globals'; import {showModal} from './modal'; import {Route, Router} from './router'; import {taskTracker} from './task_tracker'; export function maybeOpenTraceFromRoute(route: Route) { if (route.args.s) { // /?s=xxxx for permalinks. globals.dispatch(Actions.loadPermalink({hash: route.args.s})); return; } if (route.args.url) { // /?url=https://commondatastorage.googleapis.com/bucket/trace // This really works only for GCS because the Content Security Policy // forbids any other url. loadTraceFromUrl(route.args.url); return; } if (route.args.openFromAndroidBugTool) { // Handles interaction with the Android Bug Tool extension. See b/163421158. openTraceFromAndroidBugTool(); return; } if (route.args.p && route.page === '/record') { // Handles backwards compatibility for old URLs (linked from various docs), // generated before we switched URL scheme. e.g., 'record?p=power' vs // 'record/power'. See b/191255021#comment2. Router.navigate(`#!/record/${route.args.p}`); return; } if (route.args.local_cache_key) { // Handles the case of loading traces from the cache storage. maybeOpenCachedTrace(route.args.local_cache_key); return; } } /* * openCachedTrace(uuid) is called: (1) on startup, from frontend/index.ts; (2) * every time the fragment changes (from Router.onRouteChange). * This function must be idempotent (imagine this is called on every frame). * It must take decision based on the app state, not on URL change events. * Fragment changes are handled by the union of Router.onHashChange() and this * function, as follows: * 1. '' -> URL without a ?local_cache_key=xxx arg: * - no effect (except redrawing) * 2. URL without local_cache_key -> URL with local_cache_key: * - Load cached trace (without prompting any dialog). * - Show a (graceful) error dialog in the case of cache misses. * 3. '' -> URL with a ?local_cache_key=xxx arg: * - Same as case 2. * 4. URL with local_cache_key=1 -> URL with local_cache_key=2: * a) If 2 != uuid of the trace currently loaded (globals.state.traceUuid): * - Ask the user if they intend to switch trace and load 2. * b) If 2 == uuid of current trace (e.g., after a new trace has loaded): * - no effect (except redrawing). * 5. URL with local_cache_key -> URL without local_cache_key: * - Redirect to ?local_cache_key=1234 where 1234 is the UUID of the previous * URL (this might or might not match globals.state.traceUuid). * * Backward navigation cases: * 6. URL without local_cache_key <- URL with local_cache_key: * - Same as case 5. * 7. URL with local_cache_key=1 <- URL with local_cache_key=2: * - Same as case 4a: go back to local_cache_key=1 but ask the user to confirm. * 8. landing page <- URL with local_cache_key: * - Same as case 5: re-append the local_cache_key. */ async function maybeOpenCachedTrace(traceUuid: string) { if (traceUuid === globals.state.traceUuid) { // Do nothing, matches the currently loaded trace. return; } if (traceUuid === '') { // This can happen if we switch from an empty UI state to an invalid UUID // (e.g. due to a cache miss, below). This can also happen if the user just // types /#!/viewer?local_cache_key=. return; } // This handles the case when a trace T1 is loaded and then the url is set to // ?local_cache_key=T2. In that case globals.state.traceUuid remains set to T1 // until T2 has been loaded by the trace processor (can take several seconds). // This early out prevents to re-trigger the openTraceFromXXX() action if the // URL changes (e.g. if the user navigates back/fwd) while the new trace is // being loaded. for (const eng of Object.values(globals.state.engines)) { if (eng.source.type === 'ARRAY_BUFFER' && eng.source.uuid === traceUuid) { return; } } // Fetch the trace from the cache storage. If available load it. If not, show // a dialog informing the user about the cache miss. const maybeTrace = await tryGetTrace(traceUuid); const navigateToOldTraceUuid = () => { Router.navigate( `#!/viewer?local_cache_key=${globals.state.traceUuid || ''}`); }; if (!maybeTrace) { showModal({ title: 'Could not find the trace in the cache storage', content: m( 'div', m('p', 'You are trying to load a cached trace by setting the ' + '?local_cache_key argument in the URL.'), m('p', 'Unfortunately the trace wasn\'t in the cache storage.'), m('p', 'This can happen if a tab was discarded and wasn\'t opened ' + 'for too long, or if you just mis-pasted the URL.'), m('pre', `Trace UUID: ${traceUuid}`), ), buttons: [], }); navigateToOldTraceUuid(); return; } // If the UI is in a blank state (no trace has been ever opened), just load // the trace without showing any further dialog. This is the case of tab // discarding, reloading or pasting a url with a local_cache_key in an empty // instance. if (globals.state.traceUuid === undefined) { globals.dispatch(Actions.openTraceFromBuffer(maybeTrace)); return; } // If, instead, another trace is loaded, ask confirmation to the user. // Switching to another trace clears the UI state. It can be quite annoying to // lose the UI state by accidentally navigating back too much. let hasOpenedNewTrace = false; await showModal({ title: 'You are about to load a different trace and reset the UI state', content: m( 'div', m('p', 'You are seeing this because you either pasted a URL with ' + 'a different ?local_cache_key=xxx argument or because you hit ' + 'the history back/fwd button and reached a different trace.'), m('p', 'If you continue another trace will be loaded and the UI ' + 'state will be cleared.'), m('pre', `Old trace: ${globals.state.traceUuid || ''}\n` + `New trace: ${traceUuid}`), ), buttons: [ { text: 'Continue', primary: true, id: 'trace_id_open', action: () => { hasOpenedNewTrace = true; globals.dispatch(Actions.openTraceFromBuffer(maybeTrace)); } }, {text: 'Cancel', primary: false, id: 'trace_id_cancel', action: () => {}}, ], }); if (!hasOpenedNewTrace) { // We handle this after the modal await rather than in the cancel button // action so this has effect even if the user clicks Esc or clicks outside // of the modal dialog and dismisses it. navigateToOldTraceUuid(); } } function loadTraceFromUrl(url: string) { const isLocalhostTraceUrl = ['127.0.0.1', 'localhost'].includes((new URL(url)).hostname); if (isLocalhostTraceUrl) { // This handles the special case of tools/record_android_trace serving the // traces from a local webserver and killing it immediately after having // seen the HTTP GET request. In those cases store the trace as a file, so // when users click on share we don't fail the re-fetch(). const fileName = url.split('/').pop() || 'local_trace.pftrace'; const request = fetch(url) .then(response => response.blob()) .then(blob => { globals.dispatch(Actions.openTraceFromFile({ file: new File([blob], fileName), })); }) .catch(e => alert(`Could not load local trace ${e}`)); taskTracker.trackPromise(request, 'Downloading local trace'); } else { globals.dispatch(Actions.openTraceFromUrl({url})); } } function openTraceFromAndroidBugTool() { // TODO(hjd): Unify updateStatus and TaskTracker globals.dispatch(Actions.updateStatus( {msg: 'Loading trace from ABT extension', timestamp: Date.now() / 1000})); const loadInfo = loadAndroidBugToolInfo(); taskTracker.trackPromise(loadInfo, 'Loading trace from ABT extension'); loadInfo .then(info => { globals.dispatch(Actions.openTraceFromFile({ file: info.file, })); }) .catch(e => { console.error(e); }); }