-
Notifications
You must be signed in to change notification settings - Fork 24.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feature(dev-middleware): add custom message handlers to extend CDP capabilities #43291
feature(dev-middleware): add custom message handlers to extend CDP capabilities #43291
Conversation
…ateDeviceMessageMiddleware` factory
… integrate in `Device`
d95c558
to
49b0a79
Compare
…Device` functions and properties
Edit: this is now applied to this PR One other change we could make in this API, is to move "instantiation of middleware" to whenever the debugger is connecting to the device/page. By doing that, we limit the middleware instantiation to exactly the logical pairs we need to intercept. And, when the debugger disconnects, we can "destroy" the middleware and empty states. The API itself would look somewhat like this: const { middleware, websocketEndpoints } = createDevMiddleware({
unstable_deviceMessageMiddleware: ({ page, deviceInfo, debuggerInfo }) => {
// Do not enable middleware for page other than "SOMETHING", or for vscode debugging
// Can also include `page.capabilities` to determine if middleware is required
if (page.title !== 'SOMETHING' || debuggerInfo.userAgent.includes('vscode')) {
return null;
}
return {
handleDeviceMessage(message) {
if (message.type === 'CDP_MESSAGE') {
// Do something
return true;
}
},
handleDebuggerMessage(message) {
if (message.type === 'CDP_MESSAGE') {
// Do something
return true;
}
},
};
},
}); |
a509df9
to
2085e46
Compare
…, and debugger connection
2085e46
to
964a648
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a massive step in the right direction, thank you @byCedric! I have some thoughts about the specifics of the design. (I might do another pass for nits/naming after the design is more settled.)
|
||
// The device message middleware instances, per debugger, page, and device connection | ||
#messageMiddlewares: WeakMap< | ||
DebuggerInfo, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using DebuggerInfo
and Page
as keys in a map seems brittle. Are there unique IDs we could use / generate for this purpose?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, could we avoid WeakMap
and structure our data differently to begin with, with proper cleanup when a connection is terminated etc?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could nest the middleware instance under the debugger connection instead. That way, once that is cleared, the middleware should be garbage collected as well. I think a debugger connection always is to a single Page/device, so that should match the scope of the middleware too.
But I probably need some help related to Flow typing on that one, in the previous iteration I put it on the Page
object but ran into exact/in-exact object typing issues here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅ This is now nested inside the this.#debuggerConnection
, and get's cleared once this connection is destroyed.
this.#getMessageMiddleware( | ||
this.#debuggerConnection, | ||
page, | ||
)?.handleDeviceMessage(message); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Does
handleDeviceMessage
receive CDP messages or "inspector-proxy protocol" messages? It shouldn't be both. - Doesn't
handleDeviceMessage
return a value we need to branch on?
This looks like it "wants" to just be a separate disconnected()
method on the middleware.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a comment just above that on why the return value is not used here:
// NOTE(bycedric): Notify the device message middleware of the disconnect event, without any further actions.
// This can be used to clean up state in the device message middleware.
But, since we now scope the middleware on the actual device page and debugger connection, I don't think we need to do this anymore. The middleware should be fully garbage collected once either one disconnects. So we don't have to worry about disconnect events.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅ This is now removed, as the whole custom message handler is garbage collected once this happens.
@@ -61,11 +62,11 @@ type Options = $ReadOnly<{ | |||
unstable_experiments?: ExperimentsConfig, | |||
|
|||
/** | |||
* An interface for using a modified inspector proxy implementation. | |||
* Create a middleware for handling unsupported CDP events, per device. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unsupported CDP events
Pedantic nit: It looks like we actually give the middleware precedence over passing messages to/from the app, rather than just passing "unsupported" events to it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
per device
Per page, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pedantic nit: It looks like we actually give the middleware precedence over passing messages to/from the app, rather than just passing "unsupported" events to it.
Yes, unfortunately, we need to be able to influence "semi-supported" events too. It's mostly for vscode debugging, as some events are responded but not fully understood by vscode.js-debug
. E.g. microsoft/vscode-js-debug#1583
There are more cases, or there could be more popping up. Having the ability to mutate or "replace"/workaround incompatible requests/responses would help us maintain the vscode debugging method. It's also the reason we implemented (and asked for) a debugger user agent, so we know if we need to enable more mutations etc.
Per page, right?
Your reaction is correct, I forgot to update this after the last iteration, will update.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Per page, right?
✅ I updated the docs
packages/dev-middleware/src/inspector-proxy/DeviceMessageMiddleware.js
Outdated
Show resolved
Hide resolved
* It is instantiated per device and may contain state that is specific to that device. | ||
* The middleware can also mark messages from the device or debugger as handled, which stops propagating. | ||
*/ | ||
export interface DeviceMessageMiddleware { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does your use case not need the ability to also send notifications/events from the device to the debugger?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, both ways. I kept the Device
name as this happens on device level, just like we would do with the future native Hermes CDP API. But open to other names ofc 😄
For now, I'll apply your naming suggestion to everything, so that would be CustomMessageHandler
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅ This is now fully renamed to CustomMessageHandler
* This is invoked before the message is sent to the debugger. | ||
* When returning true, the message is considered handled and will not be sent to the debugger. | ||
*/ | ||
handleDeviceMessage(message: MessageFromDevice): true | void; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think MessageFromDevice
and MessageToDevice
are the correct types here. Those represent the "inspector-proxy protocol", not CDP.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, my bad. It's a bit rough to find the right types here I think. Has to be generic as this library only types the supported events (while all events from both device and debugger are passing through these functions).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JSONSerializable
type.
}>; | ||
|
||
type ExposedDebuggerInfo = $ReadOnly<{ | ||
socket: $ElementType<DebuggerInfo, 'socket'>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can't expose the raw socket to middleware. If the middleware needs the ability to send CDP messages, then CreateDeviceMessageMiddlewareFn
should include a method to do that as a parameter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, in our implementation I added the sendToDebugger
and sendToDevice
methods anyways. I think we can move them to the debuggerInfo
/deviceInfo
, without exposing the socket directly.
e.g.
debuggerInfo.send = (message: DebuggerEvent | DebuggerMessage) => void
deviceInfo.send = (message: DeviceEvent | DeviceMessage) => void
Would that solve your point here? Not 100% sure yet about the typing, but you get the point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also thought about not allowing sending messages, and just return the message object from middleware (or null
if it should stop propagating). But I think this would limit us too much in terms of supporting vscode.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅ I replaced the sockets with sendMessage
methods.
Here are our custom handlers, and a quick check if this should work or not.
NetworkResponse
This sends a response onceNetwork.getResponseBody
is requested from the debugger, should work fine.PageReload
Only replies with an acknowledge response to the debugger forPage.reload
requests, should work fine.VscodeDebuggerGetPossibleBreakpoints
Replies to the unsupportedDebugger.getPossibleBreakpoints
request, with an empty locations list (as vscode does not check for nullablility / thelocations: []
also required in the CDP spec).VscodeDebuggerSetBreakpointByUrl
Only mutates the message to avoid using unsupported way to set a breakpoint (throughDebugger.setBreakpointByUrl
)VscodeRuntimeCallFunctionOn
Replies to the debugger to avoid vscode injecting unsupported web scripts inside Hermes, which crashes Hermes / app.VscodeRuntimeGetProperties
Only mutates the message to avoid vscode performing actions onSymbols
, which also crashes Hermes / app.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Page.reload
should check the nativePageReloads
capability flags btw.
cc @dannysu for Hermes crashes, let's write Hermes unit tests / RN integration tests around them and see if they still occur in the new backend implementation
Co-authored-by: Moti Zilberman <motiz88@gmail.com>
… description in `createDevMiddleware`
…dMessage` functions
…g weakmaps and cleanups
2a3831f
to
d7a523f
Compare
…ion` to avoid Flow typing issue
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, see minor comments. In general could be worth rethinking some of the naming for clarity, but given that this is a short-term unstable API, I'm OK with shipping this.
@@ -411,6 +477,14 @@ export default class Device { | |||
}); | |||
} | |||
|
|||
if ( | |||
this.#debuggerConnection?.customHandler?.handleDeviceMessage( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typically the thing to do is store the object in a local variable (const debuggerConnection = this.#debuggerConnection
) and do the null checks on that. See https://flow.org/en/docs/lang/refinements/#toc-refinement-invalidations
appId: string, | ||
id: string, | ||
name: string, | ||
sendMessage: (message: JSONSerializable) => void, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Naming nit: I don't think sendMessage()
should be considered part of "info"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅ It's now just connection.device.sendMessage
and connection.debugger.sendMessage
.
const payload = JSON.stringify(message); | ||
debug('(Debugger) <- (Proxy) (Device): ' + payload); | ||
socket.send(payload); | ||
} catch {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why catch all exceptions here? Let's at minimum log them to debug()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is copied from the this.#sendMessageToDevice
method:
react-native/packages/dev-middleware/src/inspector-proxy/Device.js
Lines 431 to 439 in cfa39c0
// Sends single message to device. | |
#sendMessageToDevice(message: MessageToDevice) { | |
try { | |
if (message.event !== 'getPages') { | |
debug('(Debugger) (Proxy) -> (Device): ' + JSON.stringify(message)); | |
} | |
this.#deviceSocket.send(JSON.stringify(message)); | |
} catch (error) {} | |
} |
Happy to add debugging logs for both though.
@@ -74,7 +78,10 @@ export default class Device { | |||
#pages: $ReadOnlyMap<string, Page>; | |||
|
|||
// Stores information about currently connected debugger (if any). | |||
#debuggerConnection: ?DebuggerInfo = null; | |||
#debuggerConnection: ?{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's give this type a name (DebuggerConnection
?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅ Added and implemented the type
@@ -499,7 +573,7 @@ export default class Device { | |||
// Allows to make changes in incoming message from device. | |||
async #processMessageFromDeviceLegacy( | |||
payload: CDPServerMessage, | |||
debuggerInfo: DebuggerInfo, | |||
debuggerInfo: {...DebuggerInfo, ...}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we create the DebuggerConnection
type, we can probably use it here directly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, we can definitely do that; I was hesitant about doing so as this method only needs DebuggerInfo.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅ Used DebuggerConnection
instead
…device` and `debugger`
…ebuggerInfo, ...}` with `DebuggerConnection`
packages/dev-middleware/src/inspector-proxy/DeviceMessageMiddleware.js
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix typo
packages/dev-middleware/src/inspector-proxy/CustomMessageHandler.js
Outdated
Show resolved
Hide resolved
@motiz88 has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator. |
…when header is unset (#43364) Summary: At Expo, we use [Expo Tools](https://github.com/expo/vscode-expo/blob/main/src/expoDebuggers.ts) to connect the [built-in vscode-js-debug](https://github.com/microsoft/vscode-js-debug) to Hermes. Since there are a few differences in vscode vs chrome devtools, we need to enable a couple of modifications through the [`customMessageHandler` API](#43291). Unfortunately, vscode itself doesn't set the `user-agent` header when connecting to the inspector proxy. Becuase of that, we'd need a fallback to "manually" mark the debugger as being vscode ([we use this query parameter here](https://github.com/expo/vscode-expo/blob/main/src/expoDebuggers.ts#L208)). This PR supports setting the `user-agent` through `?userAgent=` when the header is not set. ## Changelog: <!-- Help reviewers and the release process by writing your own changelog entry. Pick one each for the category and type tags: [ANDROID|GENERAL|IOS|INTERNAL] [BREAKING|ADDED|CHANGED|DEPRECATED|REMOVED|FIXED|SECURITY] - Message For more details, see: https://reactnative.dev/contributing/changelogs-in-pull-requests --> [GENERAL] [ADDED] - Fallback to query parameter based `user-agent` when header is unset Pull Request resolved: #43364 Test Plan: - Install [Expo Tools](https://marketplace.visualstudio.com/items?itemName=expo.vscode-expo-tools) - Start Metro with this change. - Connect a device. - Run the vscode command `"Expo: Debug Expo app ..."` - Debugger should connect, and have it's user-agent marked as: `vscode/1.87.0 vscode-expo-tools/1.3.0` Reviewed By: huntie Differential Revision: D54804556 Pulled By: motiz88 fbshipit-source-id: 1ff558ba5350811ad042d08a713438e046759feb
This pull request was successfully merged by @byCedric in 3f41fb5. When will my fix make it into a release? | Upcoming Releases |
…when header is unset (#43364) Summary: At Expo, we use [Expo Tools](https://github.com/expo/vscode-expo/blob/main/src/expoDebuggers.ts) to connect the [built-in vscode-js-debug](https://github.com/microsoft/vscode-js-debug) to Hermes. Since there are a few differences in vscode vs chrome devtools, we need to enable a couple of modifications through the [`customMessageHandler` API](#43291). Unfortunately, vscode itself doesn't set the `user-agent` header when connecting to the inspector proxy. Becuase of that, we'd need a fallback to "manually" mark the debugger as being vscode ([we use this query parameter here](https://github.com/expo/vscode-expo/blob/main/src/expoDebuggers.ts#L208)). This PR supports setting the `user-agent` through `?userAgent=` when the header is not set. ## Changelog: <!-- Help reviewers and the release process by writing your own changelog entry. Pick one each for the category and type tags: [ANDROID|GENERAL|IOS|INTERNAL] [BREAKING|ADDED|CHANGED|DEPRECATED|REMOVED|FIXED|SECURITY] - Message For more details, see: https://reactnative.dev/contributing/changelogs-in-pull-requests --> [GENERAL] [ADDED] - Fallback to query parameter based `user-agent` when header is unset Pull Request resolved: #43364 Test Plan: - Install [Expo Tools](https://marketplace.visualstudio.com/items?itemName=expo.vscode-expo-tools) - Start Metro with this change. - Connect a device. - Run the vscode command `"Expo: Debug Expo app ..."` - Debugger should connect, and have it's user-agent marked as: `vscode/1.87.0 vscode-expo-tools/1.3.0` Reviewed By: huntie Differential Revision: D54804556 Pulled By: motiz88 fbshipit-source-id: 1ff558ba5350811ad042d08a713438e046759feb
…pabilities (#43291) Summary: This is a proposal for the `react-native/dev-middleware` package, to allow implementers to extend the CDP capabilities of the `InspectorProxy`. It's unfortunately needed until we can move to the native Hermes CDP layer. At Expo, we extend the CDP capabilities of this `InspectorProxy` by injecting functionality on the device level. This proposed API does the same, but without having to overwrite internal functions of both the `InspectorProxy` and `InspectorDevice`. A good example of this is the network inspector's capabilities. This currently works through the inspection proxy, and roughly like: - Handle any incoming `Expo(Network.receivedResponseBody)` from the _**device**_, store it, and stop event from propagating - Handle the incoming `Network.getResponseBody` from the _**debugger**_, return the data, and stop event from propagating. This API brings back that capability in a more structured way. ## API: ```ts import { createDevMiddleware } from 'react-native/dev-middleware'; const { middleware, websocketEndpoints } = createDevMiddleware({ unstable_customInspectorMessageHandler: ({ page, deviceInfo, debuggerInfo }) => { // Do not enable handler for page other than "SOMETHING", or for vscode debugging // Can also include `page.capabilities` to determine if handler is required if (page.title !== 'SOMETHING' || debuggerInfo.userAgent?.includes('vscode')) { return null; } return { handleDeviceMessage(message) { if (message.type === 'CDP_MESSAGE') { // Do something and stop message from propagating with return `true` return true; } }, handleDebuggerMessage(message) { if (message.type === 'CDP_MESSAGE') { // Do something and stop message from propagating with return `true` return true; } }, }; }, }); ``` ## Changelog: <!-- Help reviewers and the release process by writing your own changelog entry. Pick one each for the category and type tags: For more details, see: https://reactnative.dev/contributing/changelogs-in-pull-requests --> [GENERAL] [ADDED] - Add inspector proxy device message middleware API Pull Request resolved: #43291 Test Plan: See added tests and code above Reviewed By: huntie Differential Revision: D54804503 Pulled By: motiz88 fbshipit-source-id: ae918dcd5b7e76d3fb31db4c84717567ae60fa96
# Why This is the implementation side of facebook/react-native#43291, and facebook/react-native#43310. ~~PR is back in draft state due to facebook/react-native#43559 Going to merge it as is, to fix CI. # How - Drop `InspectorProxy` and `Device` override classes - Rename `InspectorHandlers` to `MessageMiddleware` - ~~Temporarily polyfill required types until `@react-native/dev-middleware` is published with this new API~~ _We now use all types from this package, derived from the `CustomMessageHandlerConnection` and `CreateCustomMessageHandlerFn` types._ - ~~Drop the `VscodeDebuggerScriptParsed` middleware as that mutates the `Device` instance~~ _This was added to optimize the source map loading. It should still work without this handler. There is work going on from Meta's side to make source map loading follow web behavior more, but that's still in progress._ # Test Plan See updated tests, need to test this when `@react-native/dev-middleware` is updated. # Checklist <!-- Please check the appropriate items below if they apply to your diff. This is required for changes to Expo modules. --> - [ ] Documentation is up to date to reflect these changes (eg: https://docs.expo.dev and README.md). - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin).
…#43559) Summary: This is a follow-up bugfix for expo/expo#27425, related to: - #43291 - #43307 - #43310 - #43364 The middleware API works as intended and can run our extended CDP events. Unfortunately, this only applies to an actual `Page` from the device, not for the `React Native Experimental (Improved Chrome Reloads)` synthetic / virtual page. That's because the middleware instantiation gets aborted when the page can't be found in `this.#pages.get(pageId)`, which always returns `null` for this synthetic page. ## Changelog: [GENERAL] [FIXED] Create custom message handler for synthetic page Pull Request resolved: #43559 Test Plan: See added test case. Reviewed By: motiz88 Differential Revision: D55129412 Pulled By: huntie fbshipit-source-id: 9679d8fe68f3cb4104f4a042f93612b995baddc9
…#43559) Summary: This is a follow-up bugfix for expo/expo#27425, related to: - #43291 - #43307 - #43310 - #43364 The middleware API works as intended and can run our extended CDP events. Unfortunately, this only applies to an actual `Page` from the device, not for the `React Native Experimental (Improved Chrome Reloads)` synthetic / virtual page. That's because the middleware instantiation gets aborted when the page can't be found in `this.#pages.get(pageId)`, which always returns `null` for this synthetic page. ## Changelog: [GENERAL] [FIXED] Create custom message handler for synthetic page Pull Request resolved: #43559 Test Plan: See added test case. Reviewed By: motiz88 Differential Revision: D55129412 Pulled By: huntie fbshipit-source-id: 9679d8fe68f3cb4104f4a042f93612b995baddc9
Summary:
This is a proposal for the
@react-native/dev-middleware
package, to allow implementers to extend the CDP capabilities of theInspectorProxy
. It's unfortunately needed until we can move to the native Hermes CDP layer.At Expo, we extend the CDP capabilities of this
InspectorProxy
by injecting functionality on the device level. This proposed API does the same, but without having to overwrite internal functions of both theInspectorProxy
andInspectorDevice
.A good example of this is the network inspector's capabilities. This currently works through the inspection proxy, and roughly like:
Expo(Network.receivedResponseBody)
from the device, store it, and stop event from propagatingNetwork.getResponseBody
from the debugger, return the data, and stop event from propagating.This API brings back that capability in a more structured way.
API:
Changelog:
[GENERAL] [ADDED] - Add inspector proxy device message middleware API
Test Plan:
See added tests and code above