Skip to content

Commit b202a7a

Browse files
committed
Fix broken pipe, reconnect, unplug
1 parent 7a0c83a commit b202a7a

File tree

1 file changed

+124
-12
lines changed

1 file changed

+124
-12
lines changed

src/js/protocols/TauriSerial.js

Lines changed: 124 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@ import { serialDevices, vendorIdNames } from "./devices";
33

44
const logHead = "[TAURI SERIAL]";
55

6+
/**
7+
* Detects Broken pipe/EPIPE errors across platforms.
8+
*/
9+
function isBrokenPipeError(error) {
10+
const s = typeof error === "string" ? error : error?.message || (error?.toString ? error.toString() : "") || "";
11+
return /broken pipe|EPIPE|os error 32|code:\s*32/i.test(s);
12+
}
13+
614
/**
715
* Async generator that polls the serial port for incoming data
816
* Similar to streamAsyncIterable in WebSerial but uses polling instead of streams
@@ -25,10 +33,18 @@ async function* pollSerialData(path, keepReadingFlag) {
2533
// Small delay between polls to avoid overwhelming the system
2634
await new Promise((resolve) => setTimeout(resolve, 5));
2735
} catch (error) {
36+
const msg = error?.message || (error?.toString ? error.toString() : "");
2837
// Timeout is expected when no data available
29-
if (!error.toString().includes("no data received")) {
30-
console.warn(`${logHead} Poll error:`, error);
38+
if (msg && msg.toLowerCase().includes("no data received")) {
39+
// Continue polling
40+
await new Promise((resolve) => setTimeout(resolve, 5));
41+
continue;
3142
}
43+
if (isBrokenPipeError(msg)) {
44+
console.error(`${logHead} Fatal poll error (broken pipe) on ${path}:`, error);
45+
throw error;
46+
}
47+
console.warn(`${logHead} Poll error:`, error);
3248
// Continue polling
3349
await new Promise((resolve) => setTimeout(resolve, 5));
3450
}
@@ -68,7 +84,12 @@ class TauriSerial extends EventTarget {
6884
// Detect if running on macOS with AT32 (needs batch writes)
6985
this.isNeedBatchWrite = false;
7086

87+
// Device monitoring
88+
this.monitoringDevices = false;
89+
this.deviceMonitorInterval = null;
90+
7191
this.loadDevices();
92+
this.startDeviceMonitoring();
7293
}
7394

7495
handleReceiveBytes(info) {
@@ -79,6 +100,99 @@ class TauriSerial extends EventTarget {
79100
return this.connectionId;
80101
}
81102

103+
handleFatalSerialError(error) {
104+
// On fatal errors (broken pipe, etc.), just disconnect cleanly
105+
// Device monitoring will automatically detect the removal and emit removedDevice
106+
if (this.connected) {
107+
this.disconnect();
108+
}
109+
}
110+
111+
startDeviceMonitoring() {
112+
if (this.monitoringDevices) {
113+
return;
114+
}
115+
116+
this.monitoringDevices = true;
117+
// Check for device changes every 1 second
118+
this.deviceMonitorInterval = setInterval(async () => {
119+
await this.checkDeviceChanges();
120+
}, 1000);
121+
122+
console.log(`${logHead} Device monitoring started`);
123+
}
124+
125+
stopDeviceMonitoring() {
126+
if (this.deviceMonitorInterval) {
127+
clearInterval(this.deviceMonitorInterval);
128+
this.deviceMonitorInterval = null;
129+
}
130+
this.monitoringDevices = false;
131+
console.log(`${logHead} Device monitoring stopped`);
132+
}
133+
134+
async checkDeviceChanges() {
135+
try {
136+
const portsMap = await invoke("plugin:serialplugin|available_ports");
137+
138+
// Convert to our format
139+
const allPorts = Object.entries(portsMap).map(([path, info]) => {
140+
let vendorId = undefined;
141+
let productId = undefined;
142+
143+
if (info.vid) {
144+
vendorId = typeof info.vid === "number" ? info.vid : parseInt(info.vid, 10);
145+
}
146+
if (info.pid) {
147+
productId = typeof info.pid === "number" ? info.pid : parseInt(info.pid, 10);
148+
}
149+
150+
return {
151+
path,
152+
displayName: this.getDisplayName(path, vendorId, productId),
153+
vendorId,
154+
productId,
155+
serialNumber: info.serial_number,
156+
};
157+
});
158+
159+
// Filter to only known devices
160+
const currentPorts = allPorts.filter((port) => {
161+
if (!port.vendorId || !port.productId) {
162+
return false;
163+
}
164+
return serialDevices.some((d) => d.vendorId === port.vendorId && d.productId === port.productId);
165+
});
166+
167+
// Check for removed devices
168+
const removedPorts = this.ports.filter(
169+
(oldPort) => !currentPorts.find((newPort) => newPort.path === oldPort.path),
170+
);
171+
172+
// Check for added devices
173+
const addedPorts = currentPorts.filter(
174+
(newPort) => !this.ports.find((oldPort) => oldPort.path === newPort.path),
175+
);
176+
177+
// Emit events for removed devices
178+
for (const removed of removedPorts) {
179+
this.dispatchEvent(new CustomEvent("removedDevice", { detail: removed }));
180+
console.log(`${logHead} Device removed: ${removed.path}`);
181+
}
182+
183+
// Emit events for added devices
184+
for (const added of addedPorts) {
185+
this.dispatchEvent(new CustomEvent("addedDevice", { detail: added }));
186+
console.log(`${logHead} Device added: ${added.path}`);
187+
}
188+
189+
// Update our ports list
190+
this.ports = currentPorts;
191+
} catch (error) {
192+
console.warn(`${logHead} Error checking device changes:`, error);
193+
}
194+
}
195+
82196
async loadDevices() {
83197
try {
84198
const portsMap = await invoke("plugin:serialplugin|available_ports");
@@ -200,9 +314,7 @@ class TauriSerial extends EventTarget {
200314
}
201315
} catch (error) {
202316
console.error(`${logHead} Error in read loop:`, error);
203-
if (this.connected) {
204-
this.disconnect();
205-
}
317+
this.handleFatalSerialError(error);
206318
}
207319
}
208320

@@ -259,6 +371,10 @@ class TauriSerial extends EventTarget {
259371
} catch (error) {
260372
console.error(`${logHead} Error sending data:`, error);
261373
this.transmitting = false;
374+
if (isBrokenPipeError(error)) {
375+
// Treat as device removal to trigger reconnect flow
376+
this.handleFatalSerialError(error);
377+
}
262378
const res = { bytesSent: 0 };
263379
callback?.(res);
264380
return res;
@@ -329,14 +445,10 @@ class TauriSerial extends EventTarget {
329445
}
330446
}
331447

448+
// Deprecated: addPort is no longer needed since monitoring handles this
332449
addPort(path) {
333-
// Reload devices to get updated port info
334-
this.loadDevices().then(() => {
335-
const added = this.ports.find((p) => p.path === path);
336-
if (added) {
337-
this.dispatchEvent(new CustomEvent("addedDevice", { detail: added }));
338-
}
339-
});
450+
// Device monitoring will automatically detect and emit addedDevice
451+
console.log(`${logHead} addPort called for ${path}, monitoring will handle detection`);
340452
}
341453
}
342454

0 commit comments

Comments
 (0)