use std::io::{prelude::*, IoSlice};
use std::net::Shutdown;
use std::os::unix::net::UnixStream;
use std::sync::OnceLock;
use std::time::{Duration, Instant};
use std::{
env,
path::{Path, PathBuf},
};
use zerocopy::IntoBytes;
use zerocopy_derive::*;
use crate::errors::*;
const DEFAULT_SOCKET_DIR: &str = "/dev/socket";
pub const PROPERTY_SERVICE_SOCKET_NAME: &str = "property_service";
pub const PROPERTY_SERVICE_FOR_SYSTEM_SOCKET_NAME: &str = "property_service_for_system";
use crate::wire::{
PROP_MSG_SETPROP, PROP_MSG_SETPROP2, PROP_NAME_MAX, PROP_SUCCESS, PROP_VALUE_MAX,
};
static SOCKET_DIR: OnceLock<PathBuf> = OnceLock::new();
pub(crate) fn set_socket_dir<P: AsRef<Path>>(dir: P) -> bool {
let dir_path = dir.as_ref().to_path_buf();
SOCKET_DIR.set(dir_path).is_ok()
}
pub(crate) fn socket_dir_is_set() -> bool {
SOCKET_DIR.get().is_some()
}
pub fn socket_dir() -> &'static Path {
if let Some(dir) = SOCKET_DIR.get() {
return dir.as_path();
}
let _guard = crate::lock_global_dirs();
SOCKET_DIR
.get_or_init(|| {
env::var_os("PROPERTY_SERVICE_SOCKET_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(DEFAULT_SOCKET_DIR))
})
.as_path()
}
fn get_property_service_socket() -> PathBuf {
socket_dir().join(PROPERTY_SERVICE_SOCKET_NAME)
}
fn get_property_service_for_system_socket() -> PathBuf {
socket_dir().join(PROPERTY_SERVICE_FOR_SYSTEM_SOCKET_NAME)
}
const SERVICE_IO_TIMEOUT: Duration = Duration::from_secs(2);
fn map_timeout_err(e: std::io::Error, doing: &str) -> Error {
if matches!(
e.kind(),
std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
) {
Error::Io(std::io::Error::new(
std::io::ErrorKind::TimedOut,
format!("timed out {doing} ({SERVICE_IO_TIMEOUT:?}): {e}"),
))
} else {
Error::Io(e)
}
}
fn connect_with_timeout(path: &Path, timeout: Duration) -> std::io::Result<UnixStream> {
use rustix::event::{poll, PollFd, PollFlags};
use rustix::io::Errno;
use rustix::net as rnet;
let timed_out = || {
std::io::Error::new(
std::io::ErrorKind::TimedOut,
format!("timed out connecting to {path:?} ({timeout:?})"),
)
};
let addr = rnet::SocketAddrUnix::new(path)?;
#[cfg(not(target_os = "macos"))]
let fd = rnet::socket_with(
rnet::AddressFamily::UNIX,
rnet::SocketType::STREAM,
rnet::SocketFlags::CLOEXEC,
None,
)?;
#[cfg(target_os = "macos")]
let fd = {
let fd = rnet::socket(rnet::AddressFamily::UNIX, rnet::SocketType::STREAM, None)?;
rustix::io::fcntl_setfd(&fd, rustix::io::FdFlags::CLOEXEC)?;
rnet::sockopt::set_socket_nosigpipe(&fd, true)?;
fd
};
rustix::io::ioctl_fionbio(&fd, true)?;
let deadline = Instant::now() + timeout;
loop {
match rnet::connect(&fd, &addr) {
Ok(()) => break,
Err(Errno::AGAIN) => {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return Err(timed_out());
}
std::thread::sleep(Duration::from_millis(10).min(remaining));
}
Err(Errno::INPROGRESS) => {
loop {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return Err(timed_out());
}
let timespec = rustix::event::Timespec::try_from(remaining).unwrap_or(
rustix::event::Timespec {
tv_sec: i64::MAX,
tv_nsec: 0,
},
);
let mut fds = [PollFd::new(&fd, PollFlags::OUT)];
match poll(&mut fds, Some(×pec)) {
Ok(0) => return Err(timed_out()),
Ok(_) => break,
Err(Errno::INTR) => continue,
Err(e) => return Err(e.into()),
}
}
rnet::sockopt::socket_error(&fd)??;
break;
}
Err(e) => return Err(e.into()),
}
}
rustix::io::ioctl_fionbio(&fd, false)?;
Ok(UnixStream::from(fd))
}
struct ServiceConnection {
stream: UnixStream,
}
impl ServiceConnection {
fn new(name: &str) -> Result<Self> {
let property_service_socket = get_property_service_socket();
let stream = if name == "sys.powerctl" {
let system_socket = get_property_service_for_system_socket();
connect_with_timeout(&system_socket, SERVICE_IO_TIMEOUT)
.or_else(|first_err| {
log::warn!(
"Connect to {system_socket:?} failed ({first_err}); falling back to {property_service_socket:?}"
);
connect_with_timeout(&property_service_socket, SERVICE_IO_TIMEOUT)
})?
} else {
connect_with_timeout(&property_service_socket, SERVICE_IO_TIMEOUT)?
};
stream.set_read_timeout(Some(SERVICE_IO_TIMEOUT))?;
stream.set_write_timeout(Some(SERVICE_IO_TIMEOUT))?;
Ok(Self { stream })
}
fn recv_i32(&mut self) -> Result<i32> {
let deadline = Instant::now() + SERVICE_IO_TIMEOUT;
let mut buf = [0u8; 4];
let mut filled = 0usize;
while filled < buf.len() {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return Err(Error::Io(std::io::Error::new(
std::io::ErrorKind::TimedOut,
format!(
"timed out waiting for property service response \
({SERVICE_IO_TIMEOUT:?} total, {filled}/4 bytes received)"
),
)));
}
let _ = self.stream.set_read_timeout(Some(remaining));
match self.stream.read(&mut buf[filled..]) {
Ok(0) => {
return Err(Error::Io(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"property service closed before sending a full response",
)))
}
Ok(n) => filled += n,
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
Err(e) => return Err(map_timeout_err(e, "waiting for property service response")),
}
}
Ok(i32::from_ne_bytes(buf))
}
}
enum WireBuf<'a> {
Borrowed(&'a [u8]),
Word([u8; 4]),
}
impl WireBuf<'_> {
fn as_slice(&self) -> &[u8] {
match self {
WireBuf::Borrowed(b) => b,
WireBuf::Word(w) => w,
}
}
}
struct ServiceWriter<'a> {
buffers: Vec<WireBuf<'a>>,
}
impl<'a> ServiceWriter<'a> {
fn new() -> Self {
Self {
buffers: Vec::with_capacity(4),
}
}
fn write_str(mut self, value: &'a str) -> Result<Self> {
let len = u32::try_from(value.len()).map_err(|_| {
Error::InvalidArgument(format!("string too long for wire: {} bytes", value.len()))
})?;
self.buffers.push(WireBuf::Word(len.to_ne_bytes()));
self.buffers.push(WireBuf::Borrowed(value.as_bytes()));
Ok(self)
}
fn write_u32(mut self, value: u32) -> Self {
self.buffers.push(WireBuf::Word(value.to_ne_bytes()));
self
}
fn write_bytes(mut self, value: &'a [u8]) -> Self {
self.buffers.push(WireBuf::Borrowed(value));
self
}
fn send(self, conn: &mut ServiceConnection) -> Result<()> {
let deadline = Instant::now() + SERVICE_IO_TIMEOUT;
let total: usize = self.buffers.iter().map(|b| b.as_slice().len()).sum();
let mut written = 0usize;
while written < total {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return Err(Error::Io(std::io::Error::new(
std::io::ErrorKind::TimedOut,
format!(
"timed out sending property service request \
({SERVICE_IO_TIMEOUT:?} total, {written}/{total} bytes sent)"
),
)));
}
let _ = conn.stream.set_write_timeout(Some(remaining));
let mut skip = written;
let mut slices: Vec<IoSlice<'_>> = Vec::with_capacity(self.buffers.len());
for buf in &self.buffers {
let buf = buf.as_slice();
if skip >= buf.len() {
skip -= buf.len();
continue;
}
slices.push(IoSlice::new(&buf[skip..]));
skip = 0;
}
match conn.stream.write_vectored(&slices) {
Ok(0) => {
return Err(Error::Io(std::io::Error::new(
std::io::ErrorKind::WriteZero,
"property service socket closed mid-write",
)))
}
Ok(n) => written += n,
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
Err(e) => return Err(map_timeout_err(e, "sending property service request")),
}
}
conn.stream.flush()?;
Ok(())
}
}
#[derive(Clone, Copy)]
enum ProtocolVersion {
V1 = 1,
V2 = 2,
}
#[derive(Immutable, IntoBytes)]
#[repr(C)]
struct PropertyMessage {
cmd: u32,
name: [u8; PROP_NAME_MAX],
value: [u8; PROP_VALUE_MAX],
}
impl PropertyMessage {
fn new(cmd: u32, name: &str, value: &str) -> Result<Self> {
let name_bytes = name.as_bytes();
let value_bytes = value.as_bytes();
if name_bytes.len() >= PROP_NAME_MAX {
return Err(Error::InvalidArgument(format!(
"Property name length {} exceeds PROP_NAME_MAX - 1 = {}",
name_bytes.len(),
PROP_NAME_MAX - 1
)));
}
if value_bytes.len() >= PROP_VALUE_MAX {
return Err(Error::InvalidArgument(format!(
"Property value length {} exceeds PROP_VALUE_MAX - 1 = {}",
value_bytes.len(),
PROP_VALUE_MAX - 1
)));
}
let mut name_buf = [0u8; PROP_NAME_MAX];
let mut value_buf = [0u8; PROP_VALUE_MAX];
name_buf[..name_bytes.len()].copy_from_slice(name_bytes);
value_buf[..value_bytes.len()].copy_from_slice(value_bytes);
Ok(Self {
cmd,
name: name_buf,
value: value_buf,
})
}
}
fn protocol_version() -> ProtocolVersion {
static PROTOCOL_VERSION: OnceLock<ProtocolVersion> = OnceLock::new();
if let Some(v) = PROTOCOL_VERSION.get() {
return *v;
}
let env_or_default = || match env::var("PROPERTY_SERVICE_VERSION") {
Ok(v) => match v.trim().parse::<u32>() {
Ok(n) if n >= 2 => ProtocolVersion::V2,
Ok(_) => ProtocolVersion::V1,
Err(_) => {
log::warn!("PROPERTY_SERVICE_VERSION={v:?} is not a number; assuming V1");
ProtocolVersion::V1
}
},
Err(_) => ProtocolVersion::V2,
};
match crate::system_properties_if_initialized() {
Some(sp) => *PROTOCOL_VERSION.get_or_init(|| {
sp.read_with("ro.property_service.version", |v| {
match v.trim().parse::<u32>() {
Ok(n) if n >= 2 => ProtocolVersion::V2,
_ => ProtocolVersion::V1,
}
})
.unwrap_or_else(|_| env_or_default())
}),
None => env_or_default(),
}
}
fn wait_for_socket_close(stream: &mut UnixStream, timeout: Duration) {
let _ = stream.shutdown(Shutdown::Write);
let original_timeout = stream.read_timeout().ok().flatten();
let started = Instant::now();
let mut buf = [0u8; 64];
loop {
let remaining = timeout.saturating_sub(started.elapsed());
if remaining.is_zero() {
log::warn!("wait_for_socket_close: timed out after {timeout:?}");
break;
}
if let Err(e) = stream.set_read_timeout(Some(remaining)) {
log::warn!("wait_for_socket_close: couldn't arm read timeout ({e}); skipping drain");
break;
}
match stream.read(&mut buf) {
Ok(0) => break, Ok(_) => {} Err(e)
if e.kind() == std::io::ErrorKind::WouldBlock
|| e.kind() == std::io::ErrorKind::TimedOut => {}
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
Err(e) => {
log::warn!("wait_for_socket_close: drain error ignored ({e})");
break;
}
}
}
let _ = stream.set_read_timeout(original_timeout);
}
pub(crate) fn set(name: &str, value: &str) -> Result<()> {
crate::wire::validate_property_name(name)
.inspect_err(|e| log::error!("setprop reject: {e}"))?;
crate::wire::validate_value_len(name, value)
.inspect_err(|e| log::error!("setprop reject: {e}"))?;
match protocol_version() {
ProtocolVersion::V1 => {
if name.len() >= PROP_NAME_MAX {
log::error!(
"Property name too long for V1 protocol: {} >= {}",
name.len(),
PROP_NAME_MAX
);
return Err(Error::InvalidArgument(format!(
"Property name is too long: {}",
name.len()
)));
}
if value.len() >= PROP_VALUE_MAX {
log::error!(
"Property value too long for V1 protocol: {} >= {}",
value.len(),
PROP_VALUE_MAX
);
return Err(Error::InvalidArgument(format!(
"Property value is too long: {}",
value.len()
)));
}
let mut conn = ServiceConnection::new(name)?;
let prop_msg = PropertyMessage::new(PROP_MSG_SETPROP, name, value)?;
ServiceWriter::new()
.write_bytes(prop_msg.as_bytes())
.send(&mut conn)?;
wait_for_socket_close(&mut conn.stream, Duration::from_millis(250));
}
ProtocolVersion::V2 => {
if name.len() > crate::wire::MAX_WIRE_NAME_LEN {
return Err(Error::InvalidArgument(format!(
"Property name exceeds the wire cap: {} > {}",
name.len(),
crate::wire::MAX_WIRE_NAME_LEN
)));
}
if value.len() > crate::wire::MAX_WIRE_VALUE_LEN {
return Err(Error::InvalidArgument(format!(
"Property value exceeds the wire cap: {} > {}",
value.len(),
crate::wire::MAX_WIRE_VALUE_LEN
)));
}
let mut conn = ServiceConnection::new(name)?;
ServiceWriter::new()
.write_u32(PROP_MSG_SETPROP2)
.write_str(name)?
.write_str(value)?
.send(&mut conn)?;
let res = conn.recv_i32()?;
if res != PROP_SUCCESS {
log::error!(
"Property service returned error for '{name}' (<{} bytes>): 0x{res:X}",
value.len()
);
return Err(Error::ServiceError {
name: name.to_owned(),
code: res,
});
}
}
}
Ok(())
}
#[cfg(all(test, not(target_os = "android")))]
mod tests {
use super::*;
#[test]
fn test_recv_i32_total_timeout_budget() {
let (client, mut server) = UnixStream::pair().unwrap();
let feeder = std::thread::spawn(move || {
for _ in 0..3 {
if server.write_all(&[0u8]).is_err() {
return;
}
std::thread::sleep(Duration::from_millis(800));
}
std::thread::sleep(Duration::from_millis(1500));
});
let mut conn = ServiceConnection { stream: client };
let start = Instant::now();
let err = conn
.recv_i32()
.expect_err("3/4 bytes must not satisfy recv_i32");
let elapsed = start.elapsed();
let msg = format!("{err}");
assert!(msg.contains("timed out"), "unexpected error: {msg}");
assert!(
elapsed >= Duration::from_millis(1800),
"budget expired early: {elapsed:?}"
);
assert!(
elapsed < Duration::from_millis(3200),
"total budget not enforced (per-syscall re-arm?): {elapsed:?}"
);
drop(conn);
let _ = feeder.join();
}
#[test]
fn test_recv_i32_early_close_is_eof_error() {
let (client, server) = UnixStream::pair().unwrap();
drop(server);
let mut conn = ServiceConnection { stream: client };
let err = conn.recv_i32().expect_err("closed socket must error");
assert!(
format!("{err}").contains("closed before"),
"unexpected error: {err}"
);
}
}