@@ -10,10 +10,9 @@ use libc::pid_t;
10
10
use serde:: Deserialize ;
11
11
use std:: {
12
12
collections:: { HashMap , HashSet } ,
13
- future:: Future ,
14
13
io,
15
14
path:: PathBuf ,
16
- process:: { ExitStatus , Stdio } ,
15
+ process:: Stdio ,
17
16
sync:: { Arc , LazyLock , Mutex } ,
18
17
time:: Duration ,
19
18
} ;
@@ -110,8 +109,7 @@ pub async fn has_full_disk_access() -> bool {
110
109
let stderr = proc. stderr . take ( ) . unwrap ( ) ;
111
110
drop ( proc. stdin . take ( ) ) ;
112
111
113
- let has_full_disk_access =
114
- parse_logger_status ( proc. wait ( ) , stdout, stderr) . await == NeedFda :: No ;
112
+ let has_full_disk_access = parse_logger_status ( stdout, stderr) . await == NeedFda :: No ;
115
113
Ok :: < bool , Error > ( has_full_disk_access)
116
114
} )
117
115
. await
@@ -127,7 +125,6 @@ enum NeedFda {
127
125
/// Return whether `proc` reports that full-disk access is unavailable based on its output
128
126
/// If it cannot be determined that access is available, it is assumed to be available
129
127
async fn parse_logger_status (
130
- proc : impl Future < Output = io:: Result < ExitStatus > > ,
131
128
stdout : impl AsyncRead + Unpin + Send + ' static ,
132
129
stderr : impl AsyncRead + Unpin + Send + ' static ,
133
130
) -> NeedFda {
@@ -155,21 +152,15 @@ async fn parse_logger_status(
155
152
}
156
153
} ) ;
157
154
158
- let proc = tokio:: time:: timeout ( EARLY_FAIL_TIMEOUT , proc ) ;
155
+ let deadline = tokio:: time:: sleep ( EARLY_FAIL_TIMEOUT ) ;
159
156
160
157
tokio:: select! {
161
158
// Received standard err/out
162
159
biased; need_full_disk_access = & mut need_full_disk_access => {
163
160
need_full_disk_access. unwrap_or( NeedFda :: No )
164
161
}
165
- proc_result = proc => {
166
- if let Ok ( Ok ( _exit_status) ) = proc_result {
167
- // Process exited
168
- return need_full_disk_access. await . unwrap_or( NeedFda :: No ) ;
169
- }
170
- // Timeout or `Child::wait`` returned an error
171
- NeedFda :: No
172
- }
162
+ // Timed out while checking for full-disk access
163
+ _ = deadline => NeedFda :: No ,
173
164
}
174
165
}
175
166
@@ -562,12 +553,28 @@ fn check_os_version_support_inner(version: MacosVersion) -> Result<(), Error> {
562
553
563
554
#[ cfg( test) ]
564
555
mod test {
565
- use super :: {
566
- check_os_version_support_inner, parse_logger_status, NeedFda , EARLY_FAIL_TIMEOUT ,
567
- MIN_OS_VERSION ,
568
- } ;
569
- use std:: { process:: ExitStatus , time:: Duration } ;
556
+ use super :: * ;
557
+
558
+ use std:: time:: Duration ;
570
559
use talpid_platform_metadata:: MacosVersion ;
560
+ use tokio:: io:: { simplex, AsyncWriteExt } ;
561
+
562
+ /// A mock-version of std{out,err}. [tokio::io::SimplexStream] implements [AsyncRead], so it can be used to test
563
+ /// [parse_logger_status].
564
+ fn output ( msg : & ' static str , lag : Duration ) -> impl AsyncRead + Unpin + Send + ' static {
565
+ // Ensure that 'msg' contains a newline to prevent user errors
566
+ assert ! (
567
+ msg. contains( '\n' ) ,
568
+ "Message does not contain a newline!! Make sure to add a newline to '{msg}'"
569
+ ) ;
570
+ let ( stdout_read, mut stdout_write) = simplex ( msg. as_bytes ( ) . len ( ) ) ;
571
+ // "print" to "stdout" after `duration`.
572
+ tokio:: spawn ( async move {
573
+ tokio:: time:: sleep ( lag) . await ;
574
+ stdout_write. write_all ( msg. as_bytes ( ) ) . await . unwrap ( ) ;
575
+ } ) ;
576
+ stdout_read
577
+ }
571
578
572
579
#[ test]
573
580
fn test_min_os_version ( ) {
@@ -590,7 +597,6 @@ mod test {
590
597
#[ tokio:: test]
591
598
async fn test_parse_logger_status_missing_access ( ) {
592
599
let need_fda = parse_logger_status (
593
- async { Ok ( ExitStatus :: default ( ) ) } ,
594
600
& [ ] [ ..] ,
595
601
b"ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED\n " . as_slice ( ) ,
596
602
)
@@ -603,15 +609,11 @@ mod test {
603
609
) ;
604
610
}
605
611
606
- /// If the check times out and 'ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED' is not in stderr , assume
607
- /// full-disk access is available.
612
+ /// If process exits without 'ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED', assume full-disk access
613
+ /// is available.
608
614
#[ tokio:: test]
609
- async fn test_parse_logger_status_timeout ( ) {
615
+ async fn test_parse_logger_status_immediate_exit ( ) {
610
616
let need_fda = parse_logger_status (
611
- async {
612
- tokio:: time:: sleep ( EARLY_FAIL_TIMEOUT + Duration :: from_secs ( 10 ) ) . await ;
613
- Ok ( ExitStatus :: default ( ) )
614
- } ,
615
617
b"nothing to see here\n " . as_slice ( ) ,
616
618
b"nothing to see here\n " . as_slice ( ) ,
617
619
)
@@ -620,25 +622,49 @@ mod test {
620
622
assert_eq ! (
621
623
need_fda,
622
624
NeedFda :: No ,
623
- "expected 'NeedFda::No' when ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED wasn't present"
625
+ "expected 'NeedFda::No' on immediate exit" ,
624
626
) ;
625
627
}
626
628
627
- /// If process exits without 'ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED', assume full-disk access
628
- /// is available.
629
+ /// Check that [parse_logger_status] returns within a reasonable timeframe.
630
+ /// "Reasonable" being within [EARLY_FAIL_TIMEOUT].
631
+ #[ tokio:: test( start_paused = true ) ]
632
+ async fn test_parse_logger_status_responsive ( ) {
633
+ let start = tokio:: time:: Instant :: now ( ) ;
634
+ let stdout = output ( "This will never be printed\n " , Duration :: MAX ) ;
635
+ let stderr = output (
636
+ "ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED\n " ,
637
+ EARLY_FAIL_TIMEOUT / 2 ,
638
+ ) ;
639
+ tokio:: time:: resume ( ) ;
640
+
641
+ let need_fda = parse_logger_status ( stdout, stderr) . await ;
642
+
643
+ tokio:: time:: pause ( ) ;
644
+
645
+ assert_eq ! (
646
+ need_fda,
647
+ NeedFda :: Yes ,
648
+ "expected 'NeedFda::Yes' when ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED was eventually printed to stderr"
649
+ ) ;
650
+
651
+ // Assert that we did not spend more time waiting than we should
652
+ assert ! ( start. elapsed( ) < EARLY_FAIL_TIMEOUT ) ;
653
+ }
654
+
655
+ /// Check that [parse_logger_status] doesn't get stuck because nothing is ever output
656
+ /// to std{out,err}. It should time out with the assumption that full-disk access is available.
629
657
#[ tokio:: test]
630
- async fn test_parse_logger_status_immediate_exit ( ) {
631
- let need_fda = parse_logger_status (
632
- async { Ok ( ExitStatus :: default ( ) ) } ,
633
- b"nothing to see here\n " . as_slice ( ) ,
634
- b"nothing to see here\n " . as_slice ( ) ,
635
- )
636
- . await ;
658
+ async fn test_parse_logger_status_timeout ( ) {
659
+ let stdout = output ( "This will never be printed\n " , Duration :: MAX ) ;
660
+ let stderr = output ( "This will never be printed\n " , Duration :: MAX ) ;
661
+
662
+ let need_fda = parse_logger_status ( stdout, stderr) . await ;
637
663
638
664
assert_eq ! (
639
665
need_fda,
640
666
NeedFda :: No ,
641
- "expected 'NeedFda::No' on immediate exit" ,
667
+ "expected 'NeedFda::No' when nothing was ever printed to stdout or stderr"
642
668
) ;
643
669
}
644
670
}
0 commit comments