Skip to content

Commit c065a5f

Browse files
Merge branch 'fda-bikeshedding'
2 parents f4c59f4 + 0a8ea83 commit c065a5f

File tree

2 files changed

+68
-38
lines changed

2 files changed

+68
-38
lines changed

talpid-core/Cargo.toml

+4
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,7 @@ features = [
103103

104104
[build-dependencies]
105105
tonic-build = { workspace = true, default-features = false, features = ["transport", "prost"] }
106+
107+
108+
[dev-dependencies]
109+
tokio = { workspace = true, features = [ "io-util", "test-util", "time" ] }

talpid-core/src/split_tunnel/macos/process.rs

+64-38
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,9 @@ use libc::pid_t;
1010
use serde::Deserialize;
1111
use std::{
1212
collections::{HashMap, HashSet},
13-
future::Future,
1413
io,
1514
path::PathBuf,
16-
process::{ExitStatus, Stdio},
15+
process::Stdio,
1716
sync::{Arc, LazyLock, Mutex},
1817
time::Duration,
1918
};
@@ -110,8 +109,7 @@ pub async fn has_full_disk_access() -> bool {
110109
let stderr = proc.stderr.take().unwrap();
111110
drop(proc.stdin.take());
112111

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;
115113
Ok::<bool, Error>(has_full_disk_access)
116114
})
117115
.await
@@ -127,7 +125,6 @@ enum NeedFda {
127125
/// Return whether `proc` reports that full-disk access is unavailable based on its output
128126
/// If it cannot be determined that access is available, it is assumed to be available
129127
async fn parse_logger_status(
130-
proc: impl Future<Output = io::Result<ExitStatus>>,
131128
stdout: impl AsyncRead + Unpin + Send + 'static,
132129
stderr: impl AsyncRead + Unpin + Send + 'static,
133130
) -> NeedFda {
@@ -155,21 +152,15 @@ async fn parse_logger_status(
155152
}
156153
});
157154

158-
let proc = tokio::time::timeout(EARLY_FAIL_TIMEOUT, proc);
155+
let deadline = tokio::time::sleep(EARLY_FAIL_TIMEOUT);
159156

160157
tokio::select! {
161158
// Received standard err/out
162159
biased; need_full_disk_access = &mut need_full_disk_access => {
163160
need_full_disk_access.unwrap_or(NeedFda::No)
164161
}
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,
173164
}
174165
}
175166

@@ -562,12 +553,28 @@ fn check_os_version_support_inner(version: MacosVersion) -> Result<(), Error> {
562553

563554
#[cfg(test)]
564555
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;
570559
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+
}
571578

572579
#[test]
573580
fn test_min_os_version() {
@@ -590,7 +597,6 @@ mod test {
590597
#[tokio::test]
591598
async fn test_parse_logger_status_missing_access() {
592599
let need_fda = parse_logger_status(
593-
async { Ok(ExitStatus::default()) },
594600
&[][..],
595601
b"ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED\n".as_slice(),
596602
)
@@ -603,15 +609,11 @@ mod test {
603609
);
604610
}
605611

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.
608614
#[tokio::test]
609-
async fn test_parse_logger_status_timeout() {
615+
async fn test_parse_logger_status_immediate_exit() {
610616
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-
},
615617
b"nothing to see here\n".as_slice(),
616618
b"nothing to see here\n".as_slice(),
617619
)
@@ -620,25 +622,49 @@ mod test {
620622
assert_eq!(
621623
need_fda,
622624
NeedFda::No,
623-
"expected 'NeedFda::No' when ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED wasn't present"
625+
"expected 'NeedFda::No' on immediate exit",
624626
);
625627
}
626628

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.
629657
#[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;
637663

638664
assert_eq!(
639665
need_fda,
640666
NeedFda::No,
641-
"expected 'NeedFda::No' on immediate exit",
667+
"expected 'NeedFda::No' when nothing was ever printed to stdout or stderr"
642668
);
643669
}
644670
}

0 commit comments

Comments
 (0)