diff --git a/target/product/base_system.mk b/target/product/base_system.mk index 277223e987..7d0222d980 100644 --- a/target/product/base_system.mk +++ b/target/product/base_system.mk @@ -18,6 +18,7 @@ PRODUCT_PACKAGES += \ abx \ adbd_system_api \ + aflags \ am \ android.hidl.base-V1.0-java \ android.hidl.manager-V1.0-java \ diff --git a/tools/aconfig/Cargo.toml b/tools/aconfig/Cargo.toml index 970fdcf1c1..95f1215766 100644 --- a/tools/aconfig/Cargo.toml +++ b/tools/aconfig/Cargo.toml @@ -4,6 +4,7 @@ members = [ "aconfig", "aconfig_protos", "aconfig_storage_file", + "aflags", "printflags" ] diff --git a/tools/aconfig/TEST_MAPPING b/tools/aconfig/TEST_MAPPING index 1a3f79ac92..e42b5d3a1f 100644 --- a/tools/aconfig/TEST_MAPPING +++ b/tools/aconfig/TEST_MAPPING @@ -74,6 +74,11 @@ { // aconfig_storage read api cpp integration tests "name": "aconfig_storage.test.cpp" + }, + { + // aflags CLI unit tests + // TODO(b/326062088): add to presubmit once proven in postsubmit. + "name": "aflags.test" } ] } diff --git a/tools/aconfig/aflags/Android.bp b/tools/aconfig/aflags/Android.bp new file mode 100644 index 0000000000..c65da97089 --- /dev/null +++ b/tools/aconfig/aflags/Android.bp @@ -0,0 +1,29 @@ +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +rust_defaults { + name: "aflags.defaults", + edition: "2021", + clippy_lints: "android", + lints: "android", + srcs: ["src/main.rs"], + rustlibs: [ + "libaconfig_protos", + "libanyhow", + "libclap", + "libprotobuf", + "libregex", + ], +} + +rust_binary { + name: "aflags", + defaults: ["aflags.defaults"], +} + +rust_test_host { + name: "aflags.test", + defaults: ["aflags.defaults"], + test_suites: ["general-tests"], +} diff --git a/tools/aconfig/aflags/Cargo.toml b/tools/aconfig/aflags/Cargo.toml new file mode 100644 index 0000000000..3350a6cd52 --- /dev/null +++ b/tools/aconfig/aflags/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "aflags" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0.69" +paste = "1.0.11" +clap = { version = "4", features = ["derive"] } +protobuf = "3.2.0" +regex = "1.10.3" +aconfig_protos = { path = "../aconfig_protos" } diff --git a/tools/aconfig/aflags/src/device_config_source.rs b/tools/aconfig/aflags/src/device_config_source.rs new file mode 100644 index 0000000000..12a62cfe8f --- /dev/null +++ b/tools/aconfig/aflags/src/device_config_source.rs @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use crate::{Flag, FlagPermission, FlagSource, ValuePickedFrom}; +use aconfig_protos::ProtoFlagPermission as ProtoPermission; +use aconfig_protos::ProtoFlagState as ProtoState; +use aconfig_protos::ProtoParsedFlag; +use aconfig_protos::ProtoParsedFlags; +use anyhow::{anyhow, bail, Result}; +use regex::Regex; +use std::collections::BTreeMap; +use std::collections::HashMap; +use std::process::Command; +use std::{fs, str}; + +pub struct DeviceConfigSource {} + +fn convert_parsed_flag(flag: &ProtoParsedFlag) -> Flag { + let namespace = flag.namespace().to_string(); + let package = flag.package().to_string(); + let name = flag.name().to_string(); + + let container = if flag.container().is_empty() { + "system".to_string() + } else { + flag.container().to_string() + }; + + let value = match flag.state() { + ProtoState::ENABLED => "true", + ProtoState::DISABLED => "false", + } + .to_string(); + + let permission = match flag.permission() { + ProtoPermission::READ_ONLY => FlagPermission::ReadOnly, + ProtoPermission::READ_WRITE => FlagPermission::ReadWrite, + }; + + Flag { + namespace, + package, + name, + container, + value, + permission, + value_picked_from: ValuePickedFrom::Default, + } +} + +fn read_pb_files() -> Result> { + let mut flags: BTreeMap = BTreeMap::new(); + for partition in ["system", "system_ext", "product", "vendor"] { + let path = format!("/{}/etc/aconfig_flags.pb", partition); + let Ok(bytes) = fs::read(&path) else { + eprintln!("warning: failed to read {}", path); + continue; + }; + let parsed_flags: ProtoParsedFlags = protobuf::Message::parse_from_bytes(&bytes)?; + for flag in parsed_flags.parsed_flag { + let key = format!("{}.{}", flag.package(), flag.name()); + let container = if flag.container().is_empty() { + "system".to_string() + } else { + flag.container().to_string() + }; + + if container.eq(partition) { + flags.insert(key, convert_parsed_flag(&flag)); + } + } + } + Ok(flags.values().cloned().collect()) +} + +fn parse_device_config(raw: &str) -> Result> { + let mut flags = HashMap::new(); + let regex = Regex::new(r"(?m)^([[[:alnum:]]_]+/[[[:alnum:]]_\.]+)=(true|false)$")?; + for capture in regex.captures_iter(raw) { + let key = + capture.get(1).ok_or(anyhow!("invalid device_config output"))?.as_str().to_string(); + let value = capture.get(2).ok_or(anyhow!("invalid device_config output"))?.as_str(); + flags.insert(key, value.to_string()); + } + Ok(flags) +} + +fn read_device_config_output(command: &str) -> Result { + let output = Command::new("/system/bin/device_config").arg(command).output()?; + if !output.status.success() { + let reason = match output.status.code() { + Some(code) => format!("exit code {}", code), + None => "terminated by signal".to_string(), + }; + bail!("failed to execute device_config: {}", reason); + } + Ok(str::from_utf8(&output.stdout)?.to_string()) +} + +fn read_device_config_flags() -> Result> { + let list_output = read_device_config_output("list")?; + parse_device_config(&list_output) +} + +fn reconcile(pb_flags: &[Flag], dc_flags: HashMap) -> Vec { + pb_flags + .iter() + .map(|f| { + dc_flags + .get(&format!("{}/{}.{}", f.namespace, f.package, f.name)) + .map(|value| { + if value.eq(&f.value) { + Flag { value_picked_from: ValuePickedFrom::Default, ..f.clone() } + } else { + Flag { + value_picked_from: ValuePickedFrom::Server, + value: value.to_string(), + ..f.clone() + } + } + }) + .unwrap_or(f.clone()) + }) + .collect() +} + +impl FlagSource for DeviceConfigSource { + fn list_flags() -> Result> { + let pb_flags = read_pb_files()?; + let dc_flags = read_device_config_flags()?; + + let flags = reconcile(&pb_flags, dc_flags); + Ok(flags) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_device_config() { + let input = r#" +namespace_one/com.foo.bar.flag_one=true +namespace_one/com.foo.bar.flag_two=false +random_noise; +namespace_two/android.flag_one=true +namespace_two/android.flag_two=nonsense +"#; + let expected = HashMap::from([ + ("namespace_one/com.foo.bar.flag_one".to_string(), "true".to_string()), + ("namespace_one/com.foo.bar.flag_two".to_string(), "false".to_string()), + ("namespace_two/android.flag_one".to_string(), "true".to_string()), + ]); + let actual = parse_device_config(input).unwrap(); + assert_eq!(expected, actual); + } +} diff --git a/tools/aconfig/aflags/src/main.rs b/tools/aconfig/aflags/src/main.rs new file mode 100644 index 0000000000..1e2a7a00a8 --- /dev/null +++ b/tools/aconfig/aflags/src/main.rs @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//! `aflags` is a device binary to read and write aconfig flags. + +use anyhow::Result; +use clap::Parser; + +mod device_config_source; +use device_config_source::DeviceConfigSource; + +#[derive(Clone)] +enum FlagPermission { + ReadOnly, + ReadWrite, +} + +impl ToString for FlagPermission { + fn to_string(&self) -> String { + match &self { + Self::ReadOnly => "read-only".into(), + Self::ReadWrite => "read-write".into(), + } + } +} + +#[derive(Clone)] +enum ValuePickedFrom { + Default, + Server, +} + +impl ToString for ValuePickedFrom { + fn to_string(&self) -> String { + match &self { + Self::Default => "default".into(), + Self::Server => "server".into(), + } + } +} + +#[derive(Clone)] +struct Flag { + namespace: String, + name: String, + package: String, + container: String, + value: String, + permission: FlagPermission, + value_picked_from: ValuePickedFrom, +} + +trait FlagSource { + fn list_flags() -> Result>; +} + +const ABOUT_TEXT: &str = "Tool for reading and writing flags. + +Rows in the table from the `list` command follow this format: + + package flag_name value provenance permission container + + * `package`: package set for this flag in its .aconfig definition. + * `flag_name`: flag name, also set in definition. + * `value`: the value read from the flag. + * `provenance`: one of: + + `default`: the flag value comes from its build-time default. + + `server`: the flag value comes from a server override. + * `permission`: read-write or read-only. + * `container`: the container for the flag, configured in its definition. +"; + +#[derive(Parser, Debug)] +#[clap(long_about=ABOUT_TEXT)] +struct Cli { + #[clap(subcommand)] + command: Command, +} + +#[derive(Parser, Debug)] +enum Command { + /// List all aconfig flags on this device. + List, +} + +struct PaddingInfo { + longest_package_col: usize, + longest_name_col: usize, + longest_val_col: usize, + longest_value_picked_from_col: usize, + longest_permission_col: usize, +} + +fn format_flag_row(flag: &Flag, info: &PaddingInfo) -> String { + let pkg = &flag.package; + let p0 = info.longest_package_col + 1; + + let name = &flag.name; + let p1 = info.longest_name_col + 1; + + let val = flag.value.to_string(); + let p2 = info.longest_val_col + 1; + + let value_picked_from = flag.value_picked_from.to_string(); + let p3 = info.longest_value_picked_from_col + 1; + + let perm = flag.permission.to_string(); + let p4 = info.longest_permission_col + 1; + + let container = &flag.container; + + format!("{pkg:p0$}{name:p1$}{val:p2$}{value_picked_from:p3$}{perm:p4$}{container}\n") +} + +fn list() -> Result { + let flags = DeviceConfigSource::list_flags()?; + let padding_info = PaddingInfo { + longest_package_col: flags.iter().map(|f| f.package.len()).max().unwrap_or(0), + longest_name_col: flags.iter().map(|f| f.name.len()).max().unwrap_or(0), + longest_val_col: flags.iter().map(|f| f.value.to_string().len()).max().unwrap_or(0), + longest_value_picked_from_col: flags + .iter() + .map(|f| f.value_picked_from.to_string().len()) + .max() + .unwrap_or(0), + longest_permission_col: flags + .iter() + .map(|f| f.permission.to_string().len()) + .max() + .unwrap_or(0), + }; + + let mut result = String::from(""); + for flag in flags { + let row = format_flag_row(&flag, &padding_info); + result.push_str(&row); + } + Ok(result) +} + +fn main() { + let cli = Cli::parse(); + let output = match cli.command { + Command::List => list(), + }; + match output { + Ok(text) => println!("{text}"), + Err(msg) => println!("Error: {}", msg), + } +}