apps: return non-zero status code for incomplete requests

This should make it easier to use quiche-client in scripts.
diff --git a/tools/apps/src/bin/quiche-client.rs b/tools/apps/src/bin/quiche-client.rs
index 9d2c953..03c54ee 100644
--- a/tools/apps/src/bin/quiche-client.rs
+++ b/tools/apps/src/bin/quiche-client.rs
@@ -37,6 +37,9 @@
 
 const MAX_DATAGRAM_SIZE: usize = 1350;
 
+const HANDSHAKE_FAIL_STATUS: i32 = -1;
+const HTTP_FAIL_STATUS: i32 = -2;
+
 const USAGE: &str = "Usage:
   quiche-client [options] URL...
   quiche-client -h | --help
@@ -284,8 +287,19 @@
         if conn.is_closed() {
             info!("connection closed, {:?}", conn.stats());
 
+            if !conn.is_established() {
+                error!(
+                    "connection timed out after {:?}",
+                    app_data_start.elapsed(),
+                );
+
+                std::process::exit(HANDSHAKE_FAIL_STATUS);
+            }
+
             if let Some(h_conn) = http_conn {
-                h_conn.report_incomplete(&app_data_start);
+                if h_conn.report_incomplete(&app_data_start) {
+                    std::process::exit(HTTP_FAIL_STATUS);
+                }
             }
 
             if let Some(si_conn) = siduck_conn {
@@ -395,8 +409,23 @@
         if conn.is_closed() {
             info!("connection closed, {:?}", conn.stats());
 
+            if !conn.is_established() {
+                error!(
+                    "connection timed out after {:?}",
+                    app_data_start.elapsed(),
+                );
+
+                std::process::exit(HANDSHAKE_FAIL_STATUS);
+            }
+
             if let Some(h_conn) = http_conn {
-                h_conn.report_incomplete(&app_data_start);
+                if h_conn.report_incomplete(&app_data_start) {
+                    std::process::exit(HTTP_FAIL_STATUS);
+                }
+            }
+
+            if let Some(si_conn) = siduck_conn {
+                si_conn.report_incomplete(&app_data_start);
             }
 
             break;
diff --git a/tools/apps/src/lib.rs b/tools/apps/src/lib.rs
index 6c1b706..3f72fc9 100644
--- a/tools/apps/src/lib.rs
+++ b/tools/apps/src/lib.rs
@@ -347,7 +347,7 @@
         req_start: &std::time::Instant,
     );
 
-    fn report_incomplete(&self, start: &std::time::Instant);
+    fn report_incomplete(&self, start: &std::time::Instant) -> bool;
 
     fn handle_requests(
         &mut self, conn: &mut std::pin::Pin<Box<quiche::Connection>>,
@@ -501,7 +501,7 @@
         }
     }
 
-    pub fn report_incomplete(&self, start: &std::time::Instant) {
+    pub fn report_incomplete(&self, start: &std::time::Instant) -> bool {
         if self.quacks_acked != self.quacks_to_make {
             error!(
                 "connection timed out after {:?} and only received {}/{} quack-acks",
@@ -509,7 +509,11 @@
                 self.quacks_acked,
                 self.quacks_to_make
             );
+
+            return true;
         }
+
+        false
     }
 }
 
@@ -679,7 +683,7 @@
         }
     }
 
-    fn report_incomplete(&self, start: &std::time::Instant) {
+    fn report_incomplete(&self, start: &std::time::Instant) -> bool {
         if self.reqs_complete != self.reqs.len() {
             error!(
                 "connection timed out after {:?} and only completed {}/{} requests",
@@ -687,7 +691,11 @@
                 self.reqs_complete,
                 self.reqs.len()
             );
+
+            return true;
         }
+
+        false
     }
 
     fn handle_requests(
@@ -1260,7 +1268,7 @@
         }
     }
 
-    fn report_incomplete(&self, start: &std::time::Instant) {
+    fn report_incomplete(&self, start: &std::time::Instant) -> bool {
         if self.reqs_complete != self.reqs.len() {
             error!(
                 "connection timed out after {:?} and only completed {}/{} requests",
@@ -1272,7 +1280,11 @@
             if self.dump_json {
                 dump_json(&self.reqs);
             }
+
+            return true;
         }
+
+        false
     }
 
     fn handle_requests(