1- // Shared plumbing for the TCP printer commands: connect, write-with-timeout,
1+ // Shared plumbing for the printer commands: connect, write-with-timeout,
22// read-one-reply, the blocking-pool wrapper, and the payload bound, so a
3- // timeout or classification change lives in one place. The USB transport
4- // (usb.rs) frames its IO differently (ZLP-terminated writes, short-packet
5- // reads), so it does its own bulk IO rather than going through these .
3+ // timeout or classification change lives in one place. The USB transports
4+ // frame their writes themselves (ZLP-terminated on macOS), but their reply
5+ // reads go through read_reply like TCP .
66
77use std:: time:: Duration ;
88use tokio:: io:: { AsyncReadExt , AsyncWriteExt } ;
@@ -16,10 +16,9 @@ const IO_TIMEOUT: Duration = Duration::from_secs(4);
1616
1717// A ~DY upload of a full label bitmap is ~200 KiB base64; bound far above that.
1818const MAX_RESPONSE_BYTES : usize = 8 * 1024 * 1024 ;
19- // The printer renders the format before replying, and a label carrying a font
20- // download (~DY) can take an entry-level printer well past 10s to store, render,
21- // and answer ^HY (observed: the ZD230 keeps working while a shorter wait times
22- // out). Generous, since a preview is a manual one-shot the user waits on.
19+ // The printer renders before replying; a ~DY font download can take an
20+ // entry-level printer past 10s (observed on the ZD230). Generous: a preview is
21+ // a manual one-shot the user waits on.
2322const FIRST_BYTE_TIMEOUT : Duration = Duration :: from_secs ( 30 ) ;
2423// After data flows, a short idle gap marks the end of the reply (the printer
2524// keeps the channel open, so waiting for a close would always hit the timeout).
@@ -76,14 +75,15 @@ pub(crate) async fn write_all_timeout(stream: &mut TcpStream, bytes: &[u8]) -> R
7675/// reply that never starts, trickles past the deadline, or overflows the bound
7776/// is an error. An empty body is reported as "no response" because print-only
7877/// endpoints accept a job silently, which is not a queryable response.
79- pub ( crate ) async fn read_reply ( stream : & mut TcpStream ) -> Result < Vec < u8 > , String > {
78+ pub ( crate ) async fn read_reply < R : tokio:: io:: AsyncRead + Unpin > (
79+ stream : & mut R ,
80+ ) -> Result < Vec < u8 > , String > {
8081 let mut body = Vec :: new ( ) ;
8182 let mut buf = [ 0u8 ; 64 * 1024 ] ;
8283 let deadline = tokio:: time:: Instant :: now ( ) + READ_DEADLINE ;
8384 loop {
84- // A complete reply ends on the idle gap long before this cap, so reaching
85- // it means the stream is incomplete (slow render, trickle): report the
86- // timeout explicitly instead of returning a truncated body downstream.
85+ // Reaching this cap means the reply never idled (slow render, trickle):
86+ // report the timeout instead of returning a truncated body.
8787 if tokio:: time:: Instant :: now ( ) >= deadline {
8888 return Err ( "read timed out" . to_string ( ) ) ;
8989 }
@@ -110,3 +110,64 @@ pub(crate) async fn read_reply(stream: &mut TcpStream) -> Result<Vec<u8>, String
110110 }
111111 Ok ( body)
112112}
113+
114+ #[ cfg( test) ]
115+ mod tests {
116+ use super :: * ;
117+
118+ // Paused clock: the timeouts auto-advance when the reader idles, so the
119+ // 30s/60s waits complete instantly.
120+ fn rt ( ) -> tokio:: runtime:: Runtime {
121+ tokio:: runtime:: Builder :: new_current_thread ( )
122+ . enable_all ( )
123+ . start_paused ( true )
124+ . build ( )
125+ . unwrap ( )
126+ }
127+
128+ #[ test]
129+ fn read_reply_ends_on_idle_gap ( ) {
130+ rt ( ) . block_on ( async {
131+ let ( mut a, mut b) = tokio:: io:: duplex ( 1024 ) ;
132+ a. write_all ( b"~DYreply" ) . await . unwrap ( ) ;
133+ // `a` stays open, so only the idle gap can end the reply.
134+ let body = read_reply ( & mut b) . await . unwrap ( ) ;
135+ assert_eq ! ( body, b"~DYreply" ) ;
136+ } ) ;
137+ }
138+
139+ #[ test]
140+ fn read_reply_reports_no_response ( ) {
141+ rt ( ) . block_on ( async {
142+ let ( _a, mut b) = tokio:: io:: duplex ( 1024 ) ;
143+ let err = read_reply ( & mut b) . await . unwrap_err ( ) ;
144+ assert_eq ! ( err, "no response from printer" ) ;
145+ } ) ;
146+ }
147+
148+ #[ test]
149+ fn read_reply_treats_eof_without_data_as_no_response ( ) {
150+ rt ( ) . block_on ( async {
151+ let ( a, mut b) = tokio:: io:: duplex ( 1024 ) ;
152+ drop ( a) ;
153+ let err = read_reply ( & mut b) . await . unwrap_err ( ) ;
154+ assert_eq ! ( err, "no response from printer" ) ;
155+ } ) ;
156+ }
157+
158+ #[ test]
159+ fn read_reply_times_out_on_trickle ( ) {
160+ rt ( ) . block_on ( async {
161+ let ( mut a, mut b) = tokio:: io:: duplex ( 1024 ) ;
162+ // A byte just inside every idle gap holds the reply open past the
163+ // overall deadline.
164+ tokio:: spawn ( async move {
165+ while a. write_all ( b"x" ) . await . is_ok ( ) {
166+ tokio:: time:: sleep ( Duration :: from_millis ( 1000 ) ) . await ;
167+ }
168+ } ) ;
169+ let err = read_reply ( & mut b) . await . unwrap_err ( ) ;
170+ assert_eq ! ( err, "read timed out" ) ;
171+ } ) ;
172+ }
173+ }
0 commit comments