1// Copyright (C) 2018 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 15// Keep this import first. 16import '../base/disposable_polyfill'; 17import '../base/static_initializers'; 18import NON_CORE_PLUGINS from '../gen/all_plugins'; 19import CORE_PLUGINS from '../gen/all_core_plugins'; 20import m from 'mithril'; 21import {defer} from '../base/deferred'; 22import {addErrorHandler, reportError} from '../base/logging'; 23import {featureFlags} from '../core/feature_flags'; 24import {initLiveReload} from '../core/live_reload'; 25import {raf} from '../core/raf_scheduler'; 26import {initWasm} from '../trace_processor/wasm_engine_proxy'; 27import {setScheduleFullRedraw} from '../widgets/raf'; 28import {UiMain} from './ui_main'; 29import {initCssConstants} from './css_constants'; 30import {registerDebugGlobals} from './debug'; 31import {maybeShowErrorDialog} from './error_dialog'; 32import {installFileDropHandler} from './file_drop_handler'; 33import {globals} from './globals'; 34import {HomePage} from './home_page'; 35import {postMessageHandler} from './post_message_handler'; 36import {Route, Router} from '../core/router'; 37import {CheckHttpRpcConnection} from './rpc_http_dialog'; 38import {maybeOpenTraceFromRoute} from './trace_url_handler'; 39import {ViewerPage} from './viewer_page'; 40import {HttpRpcEngine} from '../trace_processor/http_rpc_engine'; 41import {showModal} from '../widgets/modal'; 42import {IdleDetector} from './idle_detector'; 43import {IdleDetectorWindow} from './idle_detector_interface'; 44import {AppImpl} from '../core/app_impl'; 45import {addSqlTableTab} from '../components/details/sql_table_tab'; 46import {configureExtensions} from '../components/extensions'; 47import { 48 addDebugCounterTrack, 49 addDebugSliceTrack, 50} from '../components/tracks/debug_tracks'; 51import {addVisualizedArgTracks} from '../components/tracks/visualized_args_tracks'; 52import {addQueryResultsTab} from '../components/query_table/query_result_tab'; 53import {assetSrc, initAssets} from '../base/assets'; 54 55const CSP_WS_PERMISSIVE_PORT = featureFlags.register({ 56 id: 'cspAllowAnyWebsocketPort', 57 name: 'Relax Content Security Policy for 127.0.0.1:*', 58 description: 59 'Allows simultaneous usage of several trace_processor_shell ' + 60 '-D --http-port 1234 by opening ' + 61 'https://ui.perfetto.dev/#!/?rpc_port=1234', 62 defaultValue: false, 63}); 64 65function routeChange(route: Route) { 66 raf.scheduleFullRedraw('force', () => { 67 if (route.fragment) { 68 // This needs to happen after the next redraw call. It's not enough 69 // to use setTimeout(..., 0); since that may occur before the 70 // redraw scheduled above. 71 const e = document.getElementById(route.fragment); 72 if (e) { 73 e.scrollIntoView(); 74 } 75 } 76 }); 77 maybeOpenTraceFromRoute(route); 78} 79 80function setupContentSecurityPolicy() { 81 // Note: self and sha-xxx must be quoted, urls data: and blob: must not. 82 83 let rpcPolicy = [ 84 'http://127.0.0.1:9001', // For trace_processor_shell --httpd. 85 'ws://127.0.0.1:9001', // Ditto, for the websocket RPC. 86 ]; 87 if (CSP_WS_PERMISSIVE_PORT.get()) { 88 const route = Router.parseUrl(window.location.href); 89 if (/^\d+$/.exec(route.args.rpc_port ?? '')) { 90 rpcPolicy = [ 91 `http://127.0.0.1:${route.args.rpc_port}`, 92 `ws://127.0.0.1:${route.args.rpc_port}`, 93 ]; 94 } 95 } 96 const policy = { 97 'default-src': [ 98 `'self'`, 99 // Google Tag Manager bootstrap. 100 `'sha256-LirUKeorCU4uRNtNzr8tlB11uy8rzrdmqHCX38JSwHY='`, 101 ], 102 'script-src': [ 103 `'self'`, 104 // TODO(b/201596551): this is required for Wasm after crrev.com/c/3179051 105 // and should be replaced with 'wasm-unsafe-eval'. 106 `'unsafe-eval'`, 107 'https://*.google.com', 108 'https://*.googleusercontent.com', 109 'https://www.googletagmanager.com', 110 'https://*.google-analytics.com', 111 ], 112 'object-src': ['none'], 113 'connect-src': [ 114 `'self'`, 115 'ws://127.0.0.1:8037', // For the adb websocket server. 116 'https://*.google-analytics.com', 117 'https://*.googleapis.com', // For Google Cloud Storage fetches. 118 'blob:', 119 'data:', 120 ].concat(rpcPolicy), 121 'img-src': [ 122 `'self'`, 123 'data:', 124 'blob:', 125 'https://*.google-analytics.com', 126 'https://www.googletagmanager.com', 127 'https://*.googleapis.com', 128 ], 129 'style-src': [`'self'`, `'unsafe-inline'`], 130 'navigate-to': ['https://*.perfetto.dev', 'self'], 131 }; 132 const meta = document.createElement('meta'); 133 meta.httpEquiv = 'Content-Security-Policy'; 134 let policyStr = ''; 135 for (const [key, list] of Object.entries(policy)) { 136 policyStr += `${key} ${list.join(' ')}; `; 137 } 138 meta.content = policyStr; 139 document.head.appendChild(meta); 140} 141 142function main() { 143 // Setup content security policy before anything else. 144 setupContentSecurityPolicy(); 145 initAssets(); 146 AppImpl.initialize({ 147 initialRouteArgs: Router.parseUrl(window.location.href).args, 148 }); 149 150 // Wire up raf for widgets. 151 setScheduleFullRedraw((force?: 'force') => raf.scheduleFullRedraw(force)); 152 153 // Load the css. The load is asynchronous and the CSS is not ready by the time 154 // appendChild returns. 155 const cssLoadPromise = defer<void>(); 156 const css = document.createElement('link'); 157 css.rel = 'stylesheet'; 158 css.href = assetSrc('perfetto.css'); 159 css.onload = () => cssLoadPromise.resolve(); 160 css.onerror = (err) => cssLoadPromise.reject(err); 161 const favicon = document.head.querySelector('#favicon'); 162 if (favicon instanceof HTMLLinkElement) { 163 favicon.href = assetSrc('assets/favicon.png'); 164 } 165 166 // Load the script to detect if this is a Googler (see comments on globals.ts) 167 // and initialize GA after that (or after a timeout if something goes wrong). 168 function initAnalyticsOnScriptLoad() { 169 AppImpl.instance.analytics.initialize(globals.isInternalUser); 170 } 171 const script = document.createElement('script'); 172 script.src = 173 'https://storage.cloud.google.com/perfetto-ui-internal/is_internal_user.js'; 174 script.async = true; 175 script.onerror = () => initAnalyticsOnScriptLoad(); 176 script.onload = () => initAnalyticsOnScriptLoad(); 177 setTimeout(() => initAnalyticsOnScriptLoad(), 5000); 178 179 document.head.append(script, css); 180 181 // Route errors to both the UI bugreport dialog and Analytics (if enabled). 182 addErrorHandler(maybeShowErrorDialog); 183 addErrorHandler((e) => AppImpl.instance.analytics.logError(e)); 184 185 // Add Error handlers for JS error and for uncaught exceptions in promises. 186 window.addEventListener('error', (e) => reportError(e)); 187 window.addEventListener('unhandledrejection', (e) => reportError(e)); 188 189 initWasm(); 190 AppImpl.instance.serviceWorkerController.install(); 191 192 // Put debug variables in the global scope for better debugging. 193 registerDebugGlobals(); 194 195 // Prevent pinch zoom. 196 document.body.addEventListener( 197 'wheel', 198 (e: MouseEvent) => { 199 if (e.ctrlKey) e.preventDefault(); 200 }, 201 {passive: false}, 202 ); 203 204 cssLoadPromise.then(() => onCssLoaded()); 205 206 if (AppImpl.instance.testingMode) { 207 document.body.classList.add('testing'); 208 } 209 210 (window as {} as IdleDetectorWindow).waitForPerfettoIdle = (ms?: number) => { 211 return new IdleDetector().waitForPerfettoIdle(ms); 212 }; 213} 214 215function onCssLoaded() { 216 initCssConstants(); 217 // Clear all the contents of the initial page (e.g. the <pre> error message) 218 // And replace it with the root <main> element which will be used by mithril. 219 document.body.innerHTML = ''; 220 221 const pages = AppImpl.instance.pages; 222 const traceless = true; 223 pages.registerPage({route: '/', traceless, page: HomePage}); 224 pages.registerPage({route: '/viewer', page: ViewerPage}); 225 const router = new Router(); 226 router.onRouteChanged = routeChange; 227 228 // Mount the main mithril component. This also forces a sync render pass. 229 raf.mount(document.body, UiMain); 230 231 if ( 232 (location.origin.startsWith('http://localhost:') || 233 location.origin.startsWith('http://127.0.0.1:')) && 234 !AppImpl.instance.embeddedMode && 235 !AppImpl.instance.testingMode 236 ) { 237 initLiveReload(); 238 } 239 240 // Will update the chip on the sidebar footer that notifies that the RPC is 241 // connected. Has no effect on the controller (which will repeat this check 242 // before creating a new engine). 243 // Don't auto-open any trace URLs until we get a response here because we may 244 // accidentially clober the state of an open trace processor instance 245 // otherwise. 246 maybeChangeRpcPortFromFragment(); 247 CheckHttpRpcConnection().then(() => { 248 const route = Router.parseUrl(window.location.href); 249 if (!AppImpl.instance.embeddedMode) { 250 installFileDropHandler(); 251 } 252 253 // Don't allow postMessage or opening trace from route when the user says 254 // that they want to reuse the already loaded trace in trace processor. 255 const traceSource = AppImpl.instance.trace?.traceInfo.source; 256 if (traceSource && traceSource.type === 'HTTP_RPC') { 257 return; 258 } 259 260 // Add support for opening traces from postMessage(). 261 window.addEventListener('message', postMessageHandler, {passive: true}); 262 263 // Handles the initial ?local_cache_key=123 or ?s=permalink or ?url=... 264 // cases. 265 routeChange(route); 266 }); 267 268 // Initialize plugins, now that we are ready to go. 269 const pluginManager = AppImpl.instance.plugins; 270 CORE_PLUGINS.forEach((p) => pluginManager.registerPlugin(p)); 271 NON_CORE_PLUGINS.forEach((p) => pluginManager.registerPlugin(p)); 272 const route = Router.parseUrl(window.location.href); 273 const overrides = (route.args.enablePlugins ?? '').split(','); 274 pluginManager.activatePlugins(overrides); 275} 276 277// If the URL is /#!?rpc_port=1234, change the default RPC port. 278// For security reasons, this requires toggling a flag. Detect this and tell the 279// user what to do in this case. 280function maybeChangeRpcPortFromFragment() { 281 const route = Router.parseUrl(window.location.href); 282 if (route.args.rpc_port !== undefined) { 283 if (!CSP_WS_PERMISSIVE_PORT.get()) { 284 showModal({ 285 title: 'Using a different port requires a flag change', 286 content: m( 287 'div', 288 m( 289 'span', 290 'For security reasons before connecting to a non-standard ' + 291 'TraceProcessor port you need to manually enable the flag to ' + 292 'relax the Content Security Policy and restart the UI.', 293 ), 294 ), 295 buttons: [ 296 { 297 text: 'Take me to the flags page', 298 primary: true, 299 action: () => Router.navigate('#!/flags/cspAllowAnyWebsocketPort'), 300 }, 301 ], 302 }); 303 } else { 304 HttpRpcEngine.rpcPort = route.args.rpc_port; 305 } 306 } 307} 308 309// TODO(primiano): this injection is to break a cirular dependency. See 310// comment in sql_table_tab_interface.ts. Remove once we add an extension 311// point for context menus. 312configureExtensions({ 313 addDebugCounterTrack, 314 addDebugSliceTrack, 315 addVisualizedArgTracks, 316 addSqlTableTab, 317 addQueryResultsTab, 318}); 319 320main(); 321