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