• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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