| … | … | @@ -424,10 +424,34 @@ module.exports = class HyperSync extends ReadyResource { |
| 424 | 424 | if (typeof conn.resume === 'function') conn.resume(); |
| 425 | 425 | |
| 426 | 426 | conn.on('end', () => { try { proc.stdin.end(); } catch {} }); |
| 427 | | proc.stdin.on('error', safetyCatch); |
| 428 | | proc.stdout.on('error', safetyCatch); |
| 429 | 427 | |
| 430 | | proc.on('exit', () => { |
| 428 | // Error teardown — mirrors hyperbeam (canonical Duplex-over-DHT |
| 429 | // pattern): conn errors destroy the parent stream rather than |
| 430 | // getting silently swallowed. Without this, a transient libudx / |
| 431 | // secret-stream error mid-transfer leaves proc.stdout paused |
| 432 | // awaiting a 'drain' that the broken conn will never emit → 30s |
| 433 | // idle stall on the client side. With teardown, the error |
| 434 | // promptly tears down both ends and the client's retry loop can |
| 435 | // act on it. |
| 436 | const teardown = (err) => { |
| 437 | if (proc.exitCode === null) { |
| 438 | try { proc.kill('SIGTERM'); } catch {} |
| 439 | } |
| 440 | if (!conn.destroyed) { |
| 441 | try { conn.destroy(err); } catch {} |
| 442 | } |
| 443 | }; |
| 444 | conn.on('error', teardown); |
| 445 | proc.stdin.on('error', teardown); |
| 446 | proc.stdout.on('error', teardown); |
| 447 | |
| 448 | // Use 'close' not 'exit'. 'exit' fires when the process terminates |
| 449 | // but BEFORE all of proc.stdout has been emitted to our 'data' |
| 450 | // listener — calling conn.end() at that point truncates the tail |
| 451 | // of the pack. 'close' fires after all stdio streams have ended, |
| 452 | // guaranteeing every byte git-upload-pack wrote has reached |
| 453 | // conn.write() before we FIN the conn. |
| 454 | proc.on('close', () => { |
| 431 | 455 | if (!conn.destroyed) conn.end(); |
| 432 | 456 | }); |
| 433 | 457 | |