Repository navigation
fix(driver): fail commands instead of hanging when the Dart VM connection closes #937
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -70,7 +70,7 @@ export async function connectSocket( | |
| // `await` is needed so that rejected promise will be thrown and caught | ||
| return await originalSocketCall.apply(socket, args); | ||
| } catch (e) { | ||
| this.log.errorWithException(new Error(JSON.stringify(e))); | ||
| this.log.errorWithException(e instanceof Error ? e : new Error(JSON.stringify(e))); | ||
| } | ||
| }; | ||
| this.log.info(`Connecting to Dart Observatory: ${dartObservatoryURL}`); | ||
|
|
@@ -183,6 +183,11 @@ export async function executeElementCommand( | |
| this.log.debug(`>>> ${JSON.stringify(serializedCommand)}`); | ||
| const data = await (this.socket as IsolateSocket).executeSocketCommand(serializedCommand); | ||
| this.log.debug(`<<< ${JSON.stringify(data)} | previous command ${command}`); | ||
| if (!data) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] Propagate disconnect failures to commands that bypass executeElementCommand This guard only covers commands routed through executeElementCommand. enterText, requestData, setFrameSync, and other handlers call executeSocketCommand directly, so the socket.call wrapper still swallows the newly rejected pending call and resolves with undefined. Using a local RPC server that terminated the connection during ext.flutter.driver, I reproduced execute('flutter:enterText', ['hello']) resolving successfully with undefined instead of rejecting. Please propagate the disconnect failure through those paths too, with a regression test for a direct command handler. |
||
| throw new Error( | ||
| `Cannot execute command ${command}, no response from the Dart VM. Check the server log for the connection error`, | ||
| ); | ||
| } | ||
| if (data.isError) { | ||
| throw new Error(`Cannot execute command ${command}, server response ${JSON.stringify(data, null, 2)}`); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| import assert from 'node:assert/strict'; | ||
| import {after, before, it} from 'node:test'; | ||
|
|
||
| import {Server} from 'rpc-websockets'; | ||
|
|
||
| import {IsolateSocket} from '../build/lib/sessions/isolate_socket.js'; | ||
| import {connectSocket, executeElementCommand} from '../build/lib/sessions/observatory.js'; | ||
|
|
||
| const noopLog = {info() {}, debug() {}, warn() {}, error() {}, errorWithException() {}}; | ||
| const sockets = []; | ||
| let server; | ||
| let url; | ||
|
|
||
| // A fake Dart VM service that drops the connection while a Flutter Driver command is pending, | ||
| // like an app that crashes or relaunches in the middle of `waitForAbsent`. | ||
| before(async () => { | ||
| server = new Server({host: '127.0.0.1', port: 0}); | ||
| await new Promise((resolve) => server.on('listening', resolve)); | ||
| url = `ws://127.0.0.1:${server.wss.address().port}/ws`; | ||
| server.register('getIsolate', () => ({extensionRPCs: ['ext.flutter.driver']}), '/ws'); | ||
| server.register( | ||
| 'ext.flutter.driver', | ||
| () => { | ||
| for (const client of server.wss.clients) { | ||
| client.terminate(); | ||
| } | ||
| return new Promise(() => {}); | ||
| }, | ||
| '/ws', | ||
| ); | ||
| }); | ||
|
|
||
| after(async () => { | ||
| for (const socket of sockets) { | ||
| socket.reconnect = false; | ||
| socket.close(); | ||
| } | ||
| await server.close(); | ||
| }); | ||
|
|
||
| it('rejects a pending command when the Dart VM connection closes', {timeout: 5000}, async () => { | ||
| const socket = new IsolateSocket(url); | ||
| sockets.push(socket); | ||
| await new Promise((resolve) => socket.once('open', resolve)); | ||
|
|
||
| await assert.rejects(socket.executeSocketCommand({command: 'waitForAbsent'}), /connection was closed/); | ||
| }); | ||
|
|
||
| it('fails an element command instead of hanging when the Dart VM connection closes', {timeout: 5000}, async () => { | ||
| const logged = []; | ||
| const driver = {log: {...noopLog, errorWithException: (err) => logged.push(err.message)}}; | ||
| driver.socket = await connectSocket.call(driver, url, {isolateId: 'isolates/1'}); | ||
| sockets.push(driver.socket); | ||
| const finder = Buffer.from( | ||
| JSON.stringify({finderType: 'ByValueKey', keyValueString: 'my_key', keyValueType: 'String'}), | ||
| ).toString('base64url'); | ||
|
|
||
| await assert.rejects( | ||
| executeElementCommand.call(driver, 'waitForAbsent', finder), | ||
| /Cannot execute command waitForAbsent, no response from the Dart VM/, | ||
| ); | ||
| assert.deepEqual(logged, ['The Dart VM connection was closed before it replied']); | ||
| }); |
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.
[P1] Handle disconnect rejection during initial VM discovery
This also rejects the initial getVM call in connectSocket when no isolateId is supplied. The socket.call wrapper catches that rejection and returns undefined, after which the async onOpenListener reads vm.isolates outside its try/catch. I reproduced an unhandled TypeError (Cannot read properties of undefined, reading 'isolates') by terminating the connection while getVM was pending; the connection promise remained unsettled. Please handle this setup failure and settle the connection promise rather than allowing the event listener to reject unhandled, and add a test for a disconnect during getVM.