-
Notifications
You must be signed in to change notification settings - Fork 39
usb/sagas: Retry USB device open on Linux if SecurityError occurs #2361
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
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 |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| // Copyright (c) 2025 The Pybricks Authors | ||
| // Copyright (c) 2025-2026 The Pybricks Authors | ||
|
|
||
| import { firmwareVersion } from '@pybricks/firmware'; | ||
| import { AnyAction } from 'redux'; | ||
|
|
@@ -128,6 +128,7 @@ function* handleUsbConnectPybricks(hotPlugDevice?: USBDevice): Generator { | |
| return; | ||
| } | ||
|
|
||
| // TODO: show unexpected error message to user here | ||
| console.error('Failed to request USB device:', reqDeviceErr); | ||
| yield* put(usbDidFailToConnectPybricks()); | ||
| yield* cleanup(); | ||
|
|
@@ -140,13 +141,27 @@ function* handleUsbConnectPybricks(hotPlugDevice?: USBDevice): Generator { | |
| usbDevice = hotPlugDevice; | ||
| } | ||
|
|
||
| const [, openErr] = yield* call(() => maybe(usbDevice.open())); | ||
| if (openErr) { | ||
| // TODO: show error message to user here | ||
| console.error('Failed to open USB device:', openErr); | ||
| yield* put(usbDidFailToConnectPybricks()); | ||
| yield* cleanup(); | ||
| return; | ||
| for (let retry = 1; ; retry++) { | ||
| const [, openErr] = yield* call(() => maybe(usbDevice.open())); | ||
| if (openErr) { | ||
| // On Linux/Android, the udev rules could still be processing, try | ||
| // a few times before giving up. | ||
| if (openErr.name === 'SecurityError' && retry <= 5) { | ||
| console.debug( | ||
| `Retrying USB device open (${retry}/5) after SecurityError on Linux`, | ||
| ); | ||
| yield* delay(100); | ||
| continue; | ||
| } | ||
|
|
||
| // TODO: show error message to user here | ||
|
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. Could we make a issue/task for
Member
Author
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. Do you want one issue per error message or one for all of them? I don't remember if we have an issue for this in general or not. |
||
| console.error('Failed to open USB device:', openErr); | ||
| yield* put(usbDidFailToConnectPybricks()); | ||
| yield* cleanup(); | ||
| return; | ||
| } | ||
|
|
||
| break; | ||
| } | ||
|
|
||
| exitStack.push(() => usbDevice.close().catch(console.debug)); | ||
|
|
@@ -439,6 +454,12 @@ function* handleUsbConnectPybricks(hotPlugDevice?: USBDevice): Generator { | |
| writeCommand.matches(a), | ||
| ); | ||
|
|
||
| // Response may come before request returns, so we need to buffer them | ||
| // in a channel to avoid missing responses. | ||
| const responseChannel = yield* actionChannel( | ||
| usbDidReceivePybricksMessageResponse, | ||
| ); | ||
|
|
||
| for (;;) { | ||
| const action = yield* take(chan); | ||
|
|
||
|
|
@@ -510,7 +531,7 @@ function* handleUsbConnectPybricks(hotPlugDevice?: USBDevice): Generator { | |
| } | ||
|
|
||
| const { response, timeout } = yield* race({ | ||
| response: take(usbDidReceivePybricksMessageResponse), | ||
| response: take(responseChannel), | ||
| timeout: delay(1000), | ||
| }); | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
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.
is this something we would occasionally add for particular PRs that need testing? (Otherwise how do we handle more than one PR?)
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 doesn't automatically publish anywhere. It it just uploads the artifact to GitHub. One could download it and run it locally with
serve.pyto test a PR without having to build it themselves. Or we can throw it on labs.pybricks.com if we want wider testing.