Merge "aconfig: introduce new aflags CLI" into main

This commit is contained in:
Ted Bauer
2024-02-21 22:04:01 +00:00
committed by Gerrit Code Review
7 changed files with 382 additions and 0 deletions

View File

@@ -18,6 +18,7 @@
PRODUCT_PACKAGES += \
abx \
adbd_system_api \
aflags \
am \
android.hidl.base-V1.0-java \
android.hidl.manager-V1.0-java \

View File

@@ -4,6 +4,7 @@ members = [
"aconfig",
"aconfig_protos",
"aconfig_storage_file",
"aflags",
"printflags"
]

View File

@@ -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"
}
]
}

View File

@@ -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"],
}

View File

@@ -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" }

View File

@@ -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<Vec<Flag>> {
let mut flags: BTreeMap<String, Flag> = 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<HashMap<String, String>> {
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<String> {
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<HashMap<String, String>> {
let list_output = read_device_config_output("list")?;
parse_device_config(&list_output)
}
fn reconcile(pb_flags: &[Flag], dc_flags: HashMap<String, String>) -> Vec<Flag> {
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<Vec<Flag>> {
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);
}
}

View File

@@ -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<Vec<Flag>>;
}
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<String> {
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),
}
}