xref: /aosp_15_r20/external/perfetto/ui/src/frontend/index.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
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