Skip to content

Commit e0e4bef

Browse files
author
dirkwa
committed
fix(streams): audit round on the wasm connection elements
- WasmN2kBytes tracks its reconnect timer and clears it in end(); a pending retry previously kept the event loop alive after shutdown. J1939Can already did this, so the two now behave the same. - The socket data handler and the CAN frame listener guard their decode paths. Both run inside event handlers, where an uncaught throw takes the process down — malformed framing from a gateway now degrades to a logged error and a canboatjs:error emit. - .gitignore keeps its trailing newline.
1 parent dd592c7 commit e0e4bef

3 files changed

Lines changed: 50 additions & 26 deletions

File tree

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,3 @@ nul
6060
*.sln
6161
test/server-test-config/debug
6262
cr-review-*.txt
63-

packages/streams/src/j1939-can.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,19 @@ export default class J1939Can extends Transform {
9797
this.channel = channel
9898

9999
channel.addListener('onMessage', (msg) => {
100-
this.push({
101-
pgn: parseCanId(msg.id),
102-
length: msg.data.length,
103-
data: msg.data
104-
})
100+
// Inside a listener: an uncaught throw would take the process
101+
// down, so a frame the header parser rejects is dropped with a
102+
// log line instead.
103+
try {
104+
this.push({
105+
pgn: parseCanId(msg.id),
106+
length: msg.data.length,
107+
data: msg.data
108+
})
109+
} catch (err: unknown) {
110+
const message = err instanceof Error ? err.message : String(err)
111+
this.debug(`[frame] ${message}`)
112+
}
105113
})
106114
channel.addListener('onStopped', () => {
107115
if (this.channel !== channel) {

packages/streams/src/wasm-n2k-bytes.ts

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export default class WasmN2kBytes extends Transform {
8383
private decoder!: InstanceType<WasmByteApi['ByteDecoder']>
8484
private socket: ByteTransport | null = null
8585
private keepaliveTimer: NodeJS.Timeout | null = null
86+
private reconnectTimer: NodeJS.Timeout | null = null
8687
private stopped = false
8788
private readonly txHandler: (pgn: unknown) => void
8889
private readonly debug: DebugLogger
@@ -173,25 +174,34 @@ export default class WasmN2kBytes extends Transform {
173174
this.socket = socket
174175

175176
socket.on('data', (buf: Buffer) => {
176-
const records = this.decoder.decodeBytes(buf)
177-
const timestamp = new Date().toISOString()
178-
for (const record of records) {
179-
const pgnData = this.wasm.unwrapAnalyzerOutput(JSON.parse(record))
180-
pgnData.timestamp = timestamp
181-
pgnData.providerId = this.options.providerId
182-
this.push(pgnData)
183-
this.options.app.emit(
184-
this.options.analyzerOutEvent ?? 'N2KAnalyzerOut',
185-
pgnData
186-
)
187-
}
188-
const pending = this.decoder.takePendingTx()
189-
if (pending.length > 0) {
190-
socket.write(Buffer.from(pending))
191-
}
192-
for (const error of this.decoder.takeErrors()) {
193-
this.debug(`[error] ${error}`)
194-
this.options.app.emit('canboatjs:error', new Error(error))
177+
// Everything here runs inside a socket event handler, where an
178+
// uncaught throw takes the process down. Malformed framing from
179+
// a gateway must degrade to a logged error, not a crash.
180+
try {
181+
const records = this.decoder.decodeBytes(buf)
182+
const timestamp = new Date().toISOString()
183+
for (const record of records) {
184+
const pgnData = this.wasm.unwrapAnalyzerOutput(JSON.parse(record))
185+
pgnData.timestamp = timestamp
186+
pgnData.providerId = this.options.providerId
187+
this.push(pgnData)
188+
this.options.app.emit(
189+
this.options.analyzerOutEvent ?? 'N2KAnalyzerOut',
190+
pgnData
191+
)
192+
}
193+
const pending = this.decoder.takePendingTx()
194+
if (pending.length > 0) {
195+
socket.write(Buffer.from(pending))
196+
}
197+
for (const error of this.decoder.takeErrors()) {
198+
this.debug(`[error] ${error}`)
199+
this.options.app.emit('canboatjs:error', new Error(error))
200+
}
201+
} catch (err: unknown) {
202+
const message = err instanceof Error ? err.message : String(err)
203+
this.debug(`[decode] ${message}`)
204+
this.options.app.emit('canboatjs:error', err)
195205
}
196206
})
197207

@@ -209,7 +219,10 @@ export default class WasmN2kBytes extends Transform {
209219
if (this.options.providerId) {
210220
this.options.app.setProviderError?.(this.options.providerId, why)
211221
}
212-
setTimeout(() => this.connect(), RECONNECT_DELAY)
222+
this.reconnectTimer = setTimeout(() => {
223+
this.reconnectTimer = null
224+
this.connect()
225+
}, RECONNECT_DELAY)
213226
}
214227
}
215228
socket.on('error', (err: Error) => retry(err.message))
@@ -228,6 +241,10 @@ export default class WasmN2kBytes extends Transform {
228241

229242
end(): this {
230243
this.stopped = true
244+
if (this.reconnectTimer) {
245+
clearTimeout(this.reconnectTimer)
246+
this.reconnectTimer = null
247+
}
231248
// Detach from the shared app emitter: a stale instance would keep
232249
// encoding outbound PGNs and writing to a destroyed socket.
233250
this.options.app.removeListener('nmea2000JsonOut', this.txHandler)

0 commit comments

Comments
 (0)