Skip to content

Commit c339d4a

Browse files
authored
Merge pull request #491 from makermelissa-piclaw/fix/issue-229-writeback
Document Linux USB mount-option workaround for OSError after Ctrl-D (#229)
2 parents eb3b9bd + c6562d7 commit c339d4a

5 files changed

Lines changed: 223 additions & 1 deletion

File tree

docs/LINUX_USB_MOUNT.md

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# Linux USB Mount Notes (CIRCUITPY)
2+
3+
If you use the CircuitPython Web Editor on Linux with the **USB workflow**
4+
and you see
5+
6+
```
7+
OSError: [Errno 5] Input/output error
8+
```
9+
10+
in the serial terminal after pressing <kbd>Ctrl</kbd>+<kbd>D</kbd> following
11+
a save, this page is for you. The cause is in the host operating system,
12+
not the editor and not your CircuitPython device.
13+
14+
## What is happening
15+
16+
When the browser writes to `code.py` (or any other file on the CIRCUITPY
17+
drive), it goes through the operating system's filesystem layer. On Linux,
18+
the default behavior of the `vfat` filesystem on a USB Mass Storage Class
19+
(MSC) device is to **buffer those writes in the kernel's page cache** and
20+
only push them to the device some time later (the kernel default is up to
21+
~30 seconds).
22+
23+
When you then press <kbd>Ctrl</kbd>+<kbd>D</kbd>, CircuitPython on the
24+
device tries to import `code.py` immediately. If the host hasn't yet
25+
flushed its writes, the device sees an inconsistent filesystem and
26+
returns `OSError: [Errno 5] Input/output error`.
27+
28+
This only affects **Linux** with the **USB MSC workflow**. macOS and
29+
Windows do not have this issue. Network and Bluetooth workflows are also
30+
unaffected.
31+
32+
## Quick fix (current session)
33+
34+
Remount the CIRCUITPY drive with the `sync` mount option so writes are
35+
sent to the device synchronously:
36+
37+
```sh
38+
udisksctl unmount -b /dev/sdX1
39+
udisksctl mount -b /dev/sdX1 -o sync
40+
```
41+
42+
Replace `/dev/sdX1` with the actual block device. Find it with:
43+
44+
```sh
45+
lsblk
46+
# or
47+
mount | grep CIRCUITPY
48+
```
49+
50+
After this, return to the editor, **reconnect** (the previous filesystem
51+
handle is invalidated by the remount), and resume editing.
52+
53+
## Permanent fix: udev rule
54+
55+
To make every CircuitPython device automount with `sync` going forward,
56+
add a `udev` rule.
57+
58+
1. Find your board's USB Vendor ID (`idVendor`) and Product ID
59+
(`idProduct`) using `lsusb`. CircuitPython boards often share VID
60+
`239a` (Adafruit) but PIDs vary by board.
61+
62+
```sh
63+
lsusb
64+
```
65+
66+
2. Create `/etc/udev/rules.d/99-circuitpython-sync.rules` with:
67+
68+
```
69+
# CircuitPython CIRCUITPY drive: mount synchronously to avoid
70+
# OSError: [Errno 5] Input/output error from web-editor saves.
71+
ENV{ID_FS_LABEL}=="CIRCUITPY", ENV{UDISKS_MOUNT_OPTIONS}+="sync"
72+
```
73+
74+
The label-based match catches any CircuitPython board, regardless of
75+
VID/PID. If you'd rather scope it tighter, you can add additional
76+
`ATTRS{idVendor}=="..."` clauses.
77+
78+
3. Reload udev rules:
79+
80+
```sh
81+
sudo udevadm control --reload-rules
82+
sudo udevadm trigger
83+
```
84+
85+
4. Replug your CircuitPython board (or simply unmount/remount the
86+
CIRCUITPY drive). It should now appear with `sync` in its mount
87+
options. Verify with:
88+
89+
```sh
90+
mount | grep CIRCUITPY
91+
```
92+
93+
You should see something like:
94+
95+
```
96+
/dev/sda1 on /media/<user>/CIRCUITPY type vfat (rw,...,sync,...)
97+
```
98+
99+
## Trade-offs
100+
101+
The `sync` mount option means every write to CIRCUITPY blocks until the
102+
device has accepted the data. For interactive editing of small files
103+
this is generally not noticeable. For copying large files (firmware
104+
updates, large libraries) it will be slower than the default async
105+
behavior, but the data is more reliably on the device when the copy
106+
returns.
107+
108+
## See also
109+
110+
- [Issue #229](https://github.com/circuitpython/web-editor/issues/229)
111+
— original report and discussion
112+
- `man 8 mount` — see the `sync` option under FILESYSTEM-INDEPENDENT
113+
MOUNT OPTIONS

index.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,17 @@ <h1>Select Serial Device</h1>
338338
<div class="step-content">
339339
<h1>Select USB Host Folder</h1>
340340
<p>Select the root folder of your device. This is typically the CIRCUITPY Drive on your computer unless you renamed it. If your device does not appear as a drive on your computer, it will need to have the USB Host functionality enabled.</p>
341+
<details id="linux-mount-notice" class="linux-mount-notice" hidden>
342+
<summary><strong>Linux:</strong> seeing <code>OSError: [Errno 5]</code> after <kbd>Ctrl</kbd>+<kbd>D</kbd>? Click for fix.</summary>
343+
<p>Your CIRCUITPY drive is mounted with asynchronous writes. Remount it with <code>sync</code>:</p>
344+
<pre><code>udisksctl unmount -b /dev/sdX1
345+
udisksctl mount -b /dev/sdX1 -o sync</code></pre>
346+
<p>Replace <code>sdX1</code> with the correct device. Find it with:</p>
347+
<pre><code>lsblk</code></pre>
348+
<p>or:</p>
349+
<pre><code>mount | grep CIRCUITPY</code></pre>
350+
<p>For a permanent fix, see <a href="https://github.com/circuitpython/web-editor/blob/main/docs/LINUX_USB_MOUNT.md" target="_blank" rel="noopener">Linux USB mount notes</a>.</p>
351+
</details>
341352
<p>
342353
<button class="purple-button hidden" id="useHostFolder"><span id="workingFolder"></span></button>
343354
<button class="purple-button first-item" id="selectHostFolder">Select New Folder</button>

js/common/utilities.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,26 @@ function isChromeOs() {
7575
return false;
7676
}
7777

78+
// Test to see if browser is running on Linux (and is not Chrome OS or
79+
// Android, which can also report "Linux" in the legacy userAgent string).
80+
function isLinux() {
81+
// Newer test on Chromium
82+
if (navigator.userAgentData?.platform === "Linux") {
83+
return true;
84+
}
85+
// Avoid false positives for Chrome OS and Android.
86+
if (isChromeOs()) {
87+
return false;
88+
}
89+
if (navigator.userAgent.includes("Android")) {
90+
return false;
91+
}
92+
if (navigator.userAgent.includes("Linux")) {
93+
return true;
94+
}
95+
return false;
96+
}
97+
7898
// Parse out the url parameters from the current url
7999
function getUrlParams() {
80100
// This should look for and validate very specific values
@@ -167,6 +187,7 @@ export {
167187
isLocal,
168188
isMicrosoftWindows,
169189
isChromeOs,
190+
isLinux,
170191
getUrlParams,
171192
getUrlParam,
172193
timeout,

js/workflows/usb.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {GenericModal, DeviceInfoModal} from '../common/dialogs.js';
44
import {FileOps} from '@adafruit/circuitpython-repl-js'; // Use this to determine which FileTransferClient to load
55
import {FileTransferClient as ReplFileTransferClient} from '../common/repl-file-transfer.js';
66
import {FileTransferClient as FSAPIFileTransferClient} from '../common/fsapi-file-transfer.js';
7-
import { isChromeOs, isMicrosoftWindows } from '../common/utilities.js';
7+
import { isChromeOs, isLinux, isMicrosoftWindows } from '../common/utilities.js';
88

99
let btnRequestSerialDevice, btnSelectHostFolder, btnUseHostFolder, lblWorkingfolder;
1010

@@ -211,6 +211,16 @@ class USBWorkflow extends Workflow {
211211
btnUseHostFolder = modal.querySelector('#useHostFolder');
212212
lblWorkingfolder = modal.querySelector('#workingFolder');
213213

214+
// Show the Linux-only mount-option notice when relevant (#229).
215+
// CIRCUITPY mounted without `sync` on Linux can produce
216+
// "OSError: [Errno 5] Input/output error" on Ctrl-D after a save
217+
// because Chromium's File System Access writes are deferred by the
218+
// kernel writeback for up to ~30s.
219+
const linuxNotice = modal.querySelector('#linux-mount-notice');
220+
if (linuxNotice) {
221+
linuxNotice.hidden = !isLinux();
222+
}
223+
214224
// Map the button states to the buttons
215225
this.connectButtons = {
216226
request: btnRequestSerialDevice,

sass/layout/_layout.scss

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,3 +542,70 @@
542542
}
543543

544544
}
545+
546+
// Inline notice shown in the USB connect dialog for Linux users (#229).
547+
.linux-mount-notice {
548+
margin-top: 1rem;
549+
padding: 0.5rem 1rem;
550+
border-left: 4px solid #f0ad4e;
551+
background-color: #fff8e1;
552+
border-radius: 3px;
553+
font-size: 0.95em;
554+
555+
summary {
556+
cursor: pointer;
557+
padding: 0.25rem 0;
558+
list-style: none;
559+
position: relative;
560+
padding-left: 1.25rem;
561+
562+
&::-webkit-details-marker {
563+
display: none;
564+
}
565+
566+
&::before {
567+
content: "\25B6"; // right-pointing triangle
568+
position: absolute;
569+
left: 0;
570+
font-size: 0.8em;
571+
transition: transform 150ms ease-in-out;
572+
display: inline-block;
573+
}
574+
}
575+
576+
&[open] summary {
577+
margin-bottom: 0.5rem;
578+
579+
&::before {
580+
transform: rotate(90deg);
581+
}
582+
}
583+
584+
p {
585+
margin: 0.25rem 0;
586+
}
587+
588+
pre {
589+
margin: 0.5rem 0;
590+
padding: 0.5rem;
591+
background-color: #2d2d2d;
592+
color: #f1f1f1;
593+
border-radius: 3px;
594+
overflow-x: auto;
595+
font-size: 0.9em;
596+
}
597+
598+
code {
599+
font-family: monospace;
600+
}
601+
602+
kbd {
603+
display: inline-block;
604+
padding: 1px 4px;
605+
font-family: monospace;
606+
font-size: 0.9em;
607+
background-color: #eee;
608+
border: 1px solid #ccc;
609+
border-radius: 3px;
610+
}
611+
}

0 commit comments

Comments
 (0)