Skip to content

Commit 65bd477

Browse files
committed
Add conditional disabling of unsafe multi host features
1 parent 7aa57d1 commit 65bd477

File tree

12 files changed

+292
-106
lines changed

12 files changed

+292
-106
lines changed

config/gni/devtools_grd_files.gni

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -995,7 +995,7 @@ grd_files_debug_sources = [
995995
"front_end/entrypoints/node_app/NodeMain.js",
996996
"front_end/entrypoints/node_app/nodeConnectionsPanel.css.js",
997997
"front_end/entrypoints/rn_fusebox/FuseboxAppMetadataObserver.js",
998-
"front_end/entrypoints/rn_fusebox/FuseboxExperimentsObserver.js",
998+
"front_end/entrypoints/rn_fusebox/FuseboxFeatureObserver.js",
999999
"front_end/entrypoints/rn_fusebox/FuseboxReconnectDeviceButton.js",
10001000
"front_end/entrypoints/rn_fusebox/FuseboxWindowTitleManager.js",
10011001
"front_end/entrypoints/shell/browser_compatibility_guard.js",

front_end/core/sdk/ReactNativeApplicationModel.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,23 @@ export class ReactNativeApplicationModel extends SDKModel<EventTypes> implements
5252
this.dispatchEventToListeners(Events.METADATA_UPDATED, metadata);
5353
}
5454

55+
systemStateChanged(params: Protocol.ReactNativeApplication.SystemStateChangedEvent): void {
56+
this.dispatchEventToListeners(Events.SYSTEM_STATE_CHANGED, params);
57+
}
58+
5559
traceRequested(): void {
5660
this.dispatchEventToListeners(Events.TRACE_REQUESTED);
5761
}
5862
}
5963

6064
export const enum Events {
6165
METADATA_UPDATED = 'MetadataUpdated',
66+
SYSTEM_STATE_CHANGED = 'SystemStateChanged',
6267
TRACE_REQUESTED = 'TraceRequested',
6368
}
6469

6570
export interface EventTypes {
6671
[Events.METADATA_UPDATED]: Protocol.ReactNativeApplication.MetadataUpdatedEvent;
72+
[Events.SYSTEM_STATE_CHANGED]: Protocol.ReactNativeApplication.SystemStateChangedEvent;
6773
[Events.TRACE_REQUESTED]: void;
6874
}

front_end/entrypoints/rn_fusebox/BUILD.gn

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ devtools_module("rn_fusebox") {
1111
sources = [
1212
"FuseboxAppMetadataObserver.ts",
1313
"FuseboxReconnectDeviceButton.ts",
14-
"FuseboxExperimentsObserver.ts",
14+
"FuseboxFeatureObserver.ts",
1515
"FuseboxWindowTitleManager.ts",
1616
]
1717

front_end/entrypoints/rn_fusebox/FuseboxExperimentsObserver.ts

Lines changed: 0 additions & 102 deletions
This file was deleted.
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
// Copyright (c) Meta Platforms, Inc. and affiliates.
2+
// Copyright 2024 The Chromium Authors. All rights reserved.
3+
// Use of this source code is governed by a BSD-style license that can be
4+
// found in the LICENSE file.
5+
6+
import type * as Common from '../../core/common/common.js';
7+
import * as i18n from '../../core/i18n/i18n.js';
8+
import * as Root from '../../core/root/root.js';
9+
import * as SDK from '../../core/sdk/sdk.js';
10+
import type * as Protocol from '../../generated/protocol.js';
11+
import * as UI from '../../ui/legacy/legacy.js';
12+
import * as Lit from '../../ui/lit/lit.js';
13+
14+
import {FuseboxWindowTitleManager} from './FuseboxWindowTitleManager.js';
15+
16+
const {html, render} = Lit;
17+
18+
const UIStrings = {
19+
/**
20+
* @description Message for the "settings changed" banner shown when a reload is required for the Network panel.
21+
*/
22+
reloadRequiredForNetworkPanelMessage: 'The Network panel is now available for dogfooding. Please reload to access it.',
23+
/**
24+
* @description Title shown when Network inspection is disabled due to multiple React Native hosts.
25+
*/
26+
networkInspectionUnavailable: 'Network inspection is unavailable',
27+
/**
28+
* @description Title shown when Performance profiling is disabled due to multiple React Native hosts.
29+
*/
30+
performanceProfilingUnavailable: 'Performance profiling is unavailable',
31+
/**
32+
* @description Title shown when a feature is unavailable due to multiple React Native hosts.
33+
*/
34+
multiHostFeatureUnavailableTitle: 'Feature is unavailable',
35+
/**
36+
* @description Detail message shown when a feature is disabled due to multiple React Native hosts.
37+
*/
38+
multiHostFeatureDisabledDetail: 'This feature is disabled as the app or framework has registered multiple React Native hosts, which is not currently supported.',
39+
} as const;
40+
41+
const str_ = i18n.i18n.registerUIStrings('entrypoints/rn_fusebox/FuseboxFeatureObserver.ts', UIStrings);
42+
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
43+
44+
/**
45+
* The set of features that are not guaranteed to behave safely with multiple
46+
* React Native hosts.
47+
*/
48+
const UNSAFE_MULTI_HOST_FEATURES = new Set([
49+
'network',
50+
'timeline',
51+
]);
52+
53+
/**
54+
* [RN] Model observer which configures available DevTools features and
55+
* experiments based on the target's capabilities.
56+
*/
57+
export class FuseboxFeatureObserver implements
58+
SDK.TargetManager.SDKModelObserver<SDK.ReactNativeApplicationModel.ReactNativeApplicationModel> {
59+
#singleHostFeaturesDisabled = false;
60+
61+
constructor(targetManager: SDK.TargetManager.TargetManager) {
62+
targetManager.observeModels(SDK.ReactNativeApplicationModel.ReactNativeApplicationModel, this);
63+
}
64+
65+
modelAdded(model: SDK.ReactNativeApplicationModel.ReactNativeApplicationModel): void {
66+
model.ensureEnabled();
67+
model.addEventListener(SDK.ReactNativeApplicationModel.Events.METADATA_UPDATED, this.#handleMetadataUpdated, this);
68+
model.addEventListener(SDK.ReactNativeApplicationModel.Events.SYSTEM_STATE_CHANGED, this.#handleSystemStateChanged, this);
69+
}
70+
71+
modelRemoved(model: SDK.ReactNativeApplicationModel.ReactNativeApplicationModel): void {
72+
model.removeEventListener(
73+
SDK.ReactNativeApplicationModel.Events.METADATA_UPDATED, this.#handleMetadataUpdated, this);
74+
model.removeEventListener(
75+
SDK.ReactNativeApplicationModel.Events.SYSTEM_STATE_CHANGED, this.#handleSystemStateChanged, this);
76+
}
77+
78+
#handleMetadataUpdated(
79+
event: Common.EventTarget.EventTargetEvent<Protocol.ReactNativeApplication.MetadataUpdatedEvent>): void {
80+
// eslint-disable-next-line @typescript-eslint/naming-convention
81+
const {unstable_isProfilingBuild, unstable_networkInspectionEnabled} = event.data;
82+
83+
if (unstable_isProfilingBuild) {
84+
FuseboxWindowTitleManager.instance().setSuffix('[PROFILING]');
85+
this.#hideUnsupportedFeaturesForProfilingBuilds();
86+
}
87+
88+
if (unstable_networkInspectionEnabled) {
89+
this.#ensureNetworkPanelEnabled();
90+
}
91+
}
92+
93+
#handleSystemStateChanged(
94+
event: Common.EventTarget.EventTargetEvent<Protocol.ReactNativeApplication.SystemStateChangedEvent>): void {
95+
const {isSingleHost} = event.data;
96+
if (!isSingleHost) {
97+
this.#disableSingleHostOnlyFeatures();
98+
}
99+
}
100+
101+
#hideUnsupportedFeaturesForProfilingBuilds(): void {
102+
UI.InspectorView.InspectorView.instance().closeDrawer();
103+
104+
const viewManager = UI.ViewManager.ViewManager.instance();
105+
const panelLocationPromise = viewManager.resolveLocation(UI.ViewManager.ViewLocationValues.PANEL);
106+
const drawerLocationPromise = viewManager.resolveLocation(UI.ViewManager.ViewLocationValues.DRAWER_VIEW);
107+
void Promise.all([panelLocationPromise, drawerLocationPromise])
108+
.then(([panelLocation, drawerLocation]) => {
109+
UI.ViewManager.getRegisteredViewExtensions().forEach(view => {
110+
if (view.location() === UI.ViewManager.ViewLocationValues.DRAWER_VIEW) {
111+
drawerLocation?.removeView(view);
112+
} else {
113+
switch (view.viewId()) {
114+
case 'console':
115+
case 'heap-profiler':
116+
case 'live-heap-profile':
117+
case 'sources':
118+
case 'network':
119+
case 'react-devtools-components':
120+
case 'react-devtools-profiler':
121+
panelLocation?.removeView(view);
122+
break;
123+
}
124+
}
125+
});
126+
});
127+
}
128+
129+
#ensureNetworkPanelEnabled(): void {
130+
if (Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.ENABLE_NETWORK_PANEL)) {
131+
return;
132+
}
133+
134+
Root.Runtime.experiments.setEnabled(
135+
Root.Runtime.ExperimentName.ENABLE_NETWORK_PANEL,
136+
true,
137+
);
138+
139+
UI.InspectorView?.InspectorView?.instance()?.displayReloadRequiredWarning(
140+
i18nString(UIStrings.reloadRequiredForNetworkPanelMessage),
141+
);
142+
}
143+
144+
#disableSingleHostOnlyFeatures(): void {
145+
if (this.#singleHostFeaturesDisabled) {
146+
return;
147+
}
148+
149+
// Disable relevant CDP domains
150+
const targetManager = SDK.TargetManager.TargetManager.instance();
151+
for (const target of targetManager.targets()) {
152+
void target.networkAgent().invoke_disable();
153+
}
154+
155+
// Stop network recording if active
156+
void this.#disableNetworkRecording();
157+
158+
// Show in-panel overlay when disabled panels are selected
159+
const inspectorView = UI.InspectorView.InspectorView.instance();
160+
const overlaidPanels = new Set<string>();
161+
162+
const showPanelOverlay = (panel: UI.Panel.Panel, panelId: string): void => {
163+
const titleText =
164+
panelId === 'network'
165+
? i18nString(UIStrings.networkInspectionUnavailable)
166+
: panelId === 'timeline'
167+
? i18nString(UIStrings.performanceProfilingUnavailable)
168+
: i18nString(UIStrings.multiHostFeatureUnavailableTitle);
169+
170+
// Dim the existing panel content and disable interaction
171+
for (const child of panel.element.children) {
172+
const element = child as HTMLElement;
173+
element.style.opacity = '0.5';
174+
element.style.pointerEvents = 'none';
175+
element.setAttribute('inert', '');
176+
element.setAttribute('aria-hidden', 'true');
177+
}
178+
179+
const alertBar = document.createElement('div');
180+
render(html`
181+
<style>
182+
.alert-bar {
183+
background: var(--sys-color-tonal-container);
184+
color: var(--sys-color-on-tonal-container);
185+
padding: var(--sys-size-6) var(--sys-size-8);
186+
border-bottom: 1px solid var(--sys-color-tonal-outline);
187+
}
188+
.alert-title {
189+
font: var(--sys-typescale-body2-medium);
190+
margin-bottom: var(--sys-size-3);
191+
}
192+
.alert-detail {
193+
font: var(--sys-typescale-body4-regular);
194+
}
195+
</style>
196+
<div class="alert-bar">
197+
<div class="alert-title">${titleText}</div>
198+
<div class="alert-detail">${i18nString(UIStrings.multiHostFeatureDisabledDetail)}</div>
199+
</div>
200+
`, alertBar, {host: this});
201+
202+
panel.element.insertBefore(alertBar, panel.element.firstChild);
203+
};
204+
205+
inspectorView.tabbedPane.addEventListener(UI.TabbedPane.Events.TabSelected, event => {
206+
const tabId = event.data.tabId;
207+
if (UNSAFE_MULTI_HOST_FEATURES.has(tabId) && !overlaidPanels.has(tabId)) {
208+
overlaidPanels.add(tabId);
209+
void inspectorView.panel(tabId).then(panel => {
210+
if (panel) {
211+
showPanelOverlay(panel, tabId);
212+
}
213+
});
214+
}
215+
});
216+
217+
// Show overlay if a disabled panel is currently selected
218+
const currentTabId = inspectorView.tabbedPane.selectedTabId;
219+
if (currentTabId && UNSAFE_MULTI_HOST_FEATURES.has(currentTabId)) {
220+
overlaidPanels.add(currentTabId);
221+
void inspectorView.panel(currentTabId).then(panel => {
222+
if (panel) {
223+
showPanelOverlay(panel, currentTabId);
224+
}
225+
});
226+
}
227+
228+
this.#singleHostFeaturesDisabled = true;
229+
}
230+
231+
async #disableNetworkRecording(): Promise<void> {
232+
const inspectorView = UI.InspectorView.InspectorView.instance();
233+
try {
234+
const networkPanel = await inspectorView.panel('network');
235+
if (networkPanel && 'toggleRecord' in networkPanel) {
236+
(networkPanel as UI.Panel.Panel & {toggleRecord: (toggled: boolean) => void}).toggleRecord(false);
237+
}
238+
} catch {
239+
}
240+
}
241+
}

front_end/entrypoints/rn_fusebox/rn_fusebox.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import * as UI from '../../ui/legacy/legacy.js';
2727
import * as Main from '../main/main.js';
2828

2929
import * as FuseboxAppMetadataObserverModule from './FuseboxAppMetadataObserver.js';
30-
import * as FuseboxFeatureObserverModule from './FuseboxExperimentsObserver.js';
30+
import * as FuseboxFeatureObserverModule from './FuseboxFeatureObserver.js';
3131
import * as FuseboxReconnectDeviceButtonModule from './FuseboxReconnectDeviceButton.js';
3232

3333
// To ensure accurate timing measurements, please make sure these perf metrics

0 commit comments

Comments
 (0)