Compare commits

...

7 Commits

Author SHA1 Message Date
Andrew Gunnerson cf5ef13e47 Version 3.6.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-19 00:03:38 -04:00
Andrew Gunnerson 5fada419cb CHANGELOG.md: Add entry for PR #335
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-19 00:03:05 -04:00
Andrew Gunnerson eeea9f41b4 cli/args: Use tracing::Level directly
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-18 23:58:11 -04:00
Andrew Gunnerson 0bebf120c6 CHANGELOG.md: Add entry for PR #334
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-18 23:57:40 -04:00
Andrew Gunnerson 264c602fdb cli/ota: Remove unnecessary mutex
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-18 23:49:41 -04:00
Andrew Gunnerson 343e2e279c CHANGELOG.md: Add entry for PR #333
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-18 23:44:45 -04:00
Andrew Gunnerson 59ca759262 Add support for gzip VABC algorithm
Older devices, like the Pixel 4a 5G (bramble) use gzip instead of lz4.

This commit also reworks the CoW size estimate calculation to add the
same constant headroom that AOSP's delta_generator adds. Previously,
avbroot was already adding an additional 1% to account for differences
in compression ratios across compression library implementations. This
papered over the issue for large partitions, but small partitions could
still have a CoW size estimate that's too small. Adding the constant
headroom prevents ENOSPC when flashing those partitions.

Fixes: #332

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-18 23:33:43 -04:00
11 changed files with 164 additions and 106 deletions
+10
View File
@@ -7,6 +7,12 @@
to update the actual links at the bottom of the file.
-->
### Version 3.6.0
* Add support for gzip compression when computing CoW size estimates ([Issue #332], [PR #333])
* This allows `--replace` to successfully replace dynamic partitions on legacy devices, like the Pixel 4a 5G
* Minor code cleanup ([PR #334], [PR #335])
### Version 3.5.0
* Update all dependencies ([PR #329])
@@ -236,6 +242,7 @@ Behind-the-scenes changes:
[Issue #306]: https://github.com/chenxiaolong/avbroot/issues/306
[Issue #310]: https://github.com/chenxiaolong/avbroot/issues/310
[Issue #328]: https://github.com/chenxiaolong/avbroot/issues/328
[Issue #332]: https://github.com/chenxiaolong/avbroot/issues/332
[PR #130]: https://github.com/chenxiaolong/avbroot/pull/130
[PR #132]: https://github.com/chenxiaolong/avbroot/pull/132
[PR #133]: https://github.com/chenxiaolong/avbroot/pull/133
@@ -333,3 +340,6 @@ Behind-the-scenes changes:
[PR #323]: https://github.com/chenxiaolong/avbroot/pull/323
[PR #329]: https://github.com/chenxiaolong/avbroot/pull/329
[PR #331]: https://github.com/chenxiaolong/avbroot/pull/331
[PR #333]: https://github.com/chenxiaolong/avbroot/pull/333
[PR #334]: https://github.com/chenxiaolong/avbroot/pull/334
[PR #335]: https://github.com/chenxiaolong/avbroot/pull/335
Generated
+21 -5
View File
@@ -8,6 +8,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "adler2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "aes"
version = "0.8.4"
@@ -109,7 +115,7 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
[[package]]
name = "avbroot"
version = "3.5.0"
version = "3.6.0"
dependencies = [
"anyhow",
"assert_matches",
@@ -132,6 +138,7 @@ dependencies = [
"liblzma",
"lz4_flex",
"memchr",
"miniz_oxide 0.8.0",
"num-bigint-dig",
"num-traits",
"phf",
@@ -541,7 +548,7 @@ dependencies = [
[[package]]
name = "e2e"
version = "3.5.0"
version = "3.6.0"
dependencies = [
"anyhow",
"avbroot",
@@ -613,7 +620,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920"
dependencies = [
"crc32fast",
"miniz_oxide",
"miniz_oxide 0.7.4",
]
[[package]]
@@ -635,7 +642,7 @@ dependencies = [
[[package]]
name = "fuzz"
version = "3.5.0"
version = "3.6.0"
dependencies = [
"avbroot",
"honggfuzz",
@@ -953,6 +960,15 @@ dependencies = [
"adler",
]
[[package]]
name = "miniz_oxide"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
dependencies = [
"adler2",
]
[[package]]
name = "multimap"
version = "0.10.0"
@@ -2050,7 +2066,7 @@ dependencies = [
[[package]]
name = "xtask"
version = "3.5.0"
version = "3.6.0"
dependencies = [
"anyhow",
"clap",
+1 -1
View File
@@ -4,7 +4,7 @@ members = ["avbroot", "e2e", "fuzz", "xtask"]
resolver = "2"
[workspace.package]
version = "3.5.0"
version = "3.6.0"
license = "GPL-3.0-only"
edition = "2021"
repository = "https://github.com/chenxiaolong/avbroot"
+1
View File
@@ -27,6 +27,7 @@ hex = { version = "0.4.3", features = ["serde"] }
liblzma = "0.3.0"
lz4_flex = "0.11.1"
memchr = "2.6.0"
miniz_oxide = "0.8.0"
num-bigint-dig = "0.8.4"
num-traits = "0.2.16"
phf = { version = "0.11.2", features = ["macros"] }
+4 -37
View File
@@ -37,39 +37,6 @@ pub enum Command {
MagiskInfo(boot::MagiskInfoCli),
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
}
impl LogLevel {
fn as_level(self) -> Level {
match self {
Self::Trace => Level::TRACE,
Self::Debug => Level::DEBUG,
Self::Info => Level::INFO,
Self::Warn => Level::WARN,
Self::Error => Level::ERROR,
}
}
}
impl Default for LogLevel {
fn default() -> Self {
Self::Info
}
}
impl fmt::Display for LogLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.to_possible_value().ok_or(fmt::Error)?.get_name())
}
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum LogFormat {
Short,
@@ -96,8 +63,8 @@ pub struct Cli {
pub command: Command,
/// Lowest log message severity to output.
#[arg(long, global = true, value_name = "LEVEL", default_value_t)]
pub log_level: LogLevel,
#[arg(long, global = true, value_name = "LEVEL", default_value_t = Level::INFO)]
pub log_level: Level,
/// Output format for log messages.
#[arg(long, global = true, value_name = "FORMAT", default_value_t)]
@@ -124,11 +91,11 @@ impl FormatTime for ShortUptime {
}
}
pub fn init_logging(log_level: LogLevel, log_format: LogFormat) {
pub fn init_logging(log_level: Level, log_format: LogFormat) {
let builder = tracing_subscriber::fmt()
.with_writer(io::stderr)
.with_ansi(io::stderr().is_terminal())
.with_max_level(log_level.as_level());
.with_max_level(log_level);
match log_format {
LogFormat::Short => {
+21 -20
View File
@@ -34,7 +34,7 @@ use crate::{
avb::{self, Descriptor, Header},
ota::{self, SigningWriter, ZipEntry},
padding,
payload::{self, PayloadHeader, PayloadWriter},
payload::{self, PayloadHeader, PayloadWriter, VabcAlgo},
},
patch::{
boot::{
@@ -676,12 +676,11 @@ pub fn compress_image(
// Otherwise, compress the entire image. If VABC is enabled, we need to
// update the CoW size estimate or else the CoW block device may run out of
// space during flashing.
let need_cow = partition.estimate_cow_size.is_some();
if need_cow {
let vabc_algo = if partition.estimate_cow_size.is_some() {
info!("Needs updated CoW size estimate: {name}");
// Only CoW v2 + lz4 seems to exist in the wild currently, so that is
// all we support.
// Only CoW v2 seems to exist in the wild currently, so that is all we
// support.
let Some(dpm) = &header.manifest.dynamic_partition_metadata else {
bail!("Dynamic partition metadata is missing");
};
@@ -696,13 +695,17 @@ pub fn compress_image(
}
let compression = dpm.vabc_compression_param();
if compression != "lz4" {
let Some(vabc_algo) = VabcAlgo::new(compression) else {
bail!("Unsupported VABC compression: {compression}");
}
}
};
Some(vabc_algo)
} else {
None
};
let (partition_info, operations, cow_estimate) =
payload::compress_image(&*file, &writer, name, block_size, need_cow, cancel_signal)?;
payload::compress_image(&*file, &writer, name, block_size, vabc_algo, cancel_signal)?;
partition.new_partition_info = Some(partition_info);
partition.operations = operations;
@@ -726,15 +729,13 @@ fn patch_ota_payload(
cert_ota: &Certificate,
cancel_signal: &AtomicBool,
) -> Result<(String, u64)> {
let header = PayloadHeader::from_reader(payload.reopen_boxed()?)
let mut header = PayloadHeader::from_reader(payload.reopen_boxed()?)
.context("Failed to load OTA payload header")?;
if !header.is_full_ota() {
bail!("Payload is a delta OTA, not a full OTA");
}
let header = Mutex::new(header);
let mut header_locked = header.lock().unwrap();
let all_partitions = header_locked
let all_partitions = header
.manifest
.partitions
.iter()
@@ -752,7 +753,7 @@ fn patch_ota_payload(
// Determine what images need to be patched. For simplicity, we pre-read all
// vbmeta images since they're tiny. They're discarded later if the they
// don't need to be modified.
let required_images = RequiredImages::new(&header_locked.manifest);
let required_images = RequiredImages::new(&header.manifest);
let vbmeta_images = required_images.iter_vbmeta().collect::<HashSet<_>>();
// The set of source images to be inserted into the new payload, replacing
@@ -764,7 +765,7 @@ fn patch_ota_payload(
payload,
&required_images,
external_images,
&header_locked,
&header,
cancel_signal,
)?;
@@ -806,7 +807,7 @@ fn patch_ota_payload(
&mut vbmeta_order,
clear_vbmeta_flags,
key_avb,
header_locked.manifest.block_size().into(),
header.manifest.block_size().into(),
)?;
// Unmodified vbmeta images no longer need to be kept around either.
@@ -818,7 +819,7 @@ fn patch_ota_payload(
let modified_operations = compress_image(
&name,
&mut input_file.file,
&mut header_locked,
&mut header,
// We can only perform the optimization of avoiding
// recompression if the image came from the original payload.
if name == system_target && !external_images.contains_key(&name) {
@@ -836,7 +837,7 @@ fn patch_ota_payload(
info!("Generating new OTA payload");
let mut payload_writer = PayloadWriter::new(writer, header_locked.clone(), key_ota.clone())
let mut payload_writer = PayloadWriter::new(writer, header.clone(), key_ota.clone())
.context("Failed to write payload header")?;
let mut orig_payload_reader = payload.reopen_boxed().context("Failed to open payload")?;
@@ -854,7 +855,7 @@ fn patch_ota_payload(
let pi = payload_writer.partition_index().unwrap();
let oi = payload_writer.operation_index().unwrap();
let orig_partition = &header_locked.manifest.partitions[pi];
let orig_partition = &header.manifest.partitions[pi];
let orig_operation = &orig_partition.operations[oi];
let data_offset = orig_operation
.data_offset
@@ -884,7 +885,7 @@ fn patch_ota_payload(
// Otherwise, copy from the original payload.
let data_offset = data_offset
.checked_add(header_locked.blob_offset)
.checked_add(header.blob_offset)
.ok_or_else(|| anyhow!("data_offset overflow in partition #{pi} operation #{oi}"))?;
orig_payload_reader
+69 -23
View File
@@ -5,6 +5,7 @@
use std::{
collections::{HashMap, HashSet},
fmt,
io::{self, Cursor, Read, Seek, SeekFrom, Write},
ops::Range,
sync::atomic::AtomicBool,
@@ -887,19 +888,50 @@ fn compress_chunk(raw_data: &[u8], cancel_signal: &AtomicBool) -> Result<(Vec<u8
Ok((data, digest_compressed))
}
fn compress_cow_size(mut raw_data: &[u8], block_size: u32) -> u64 {
let mut total = 0;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub enum VabcAlgo {
Lz4,
Gzip,
}
while !raw_data.is_empty() {
let n = raw_data.len().min(block_size as usize);
let compressed = lz4_flex::block::compress(&raw_data[..n]);
total += compressed.len().min(n) as u64;
raw_data = &raw_data[n..];
impl VabcAlgo {
pub fn new(name: &str) -> Option<Self> {
match name {
"lz4" => Some(Self::Lz4),
"gz" => Some(Self::Gzip),
_ => None,
}
}
total
fn compressed_size(&self, mut raw_data: &[u8], block_size: u32) -> u64 {
let mut total = 0;
while !raw_data.is_empty() {
let n = raw_data.len().min(block_size as usize);
let compressed = match self {
Self::Lz4 => lz4_flex::block::compress(&raw_data[..n]),
// We use the miniz_oxide backend for flate2, but flate2 doesn't
// expose a nice function for compressing to a vec, so just use
// miniz_oxide directly.
Self::Gzip => miniz_oxide::deflate::compress_to_vec_zlib(&raw_data[..n], 9),
};
total += compressed.len().min(n) as u64;
raw_data = &raw_data[n..];
}
total
}
}
impl fmt::Display for VabcAlgo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Lz4 => f.write_str("lz4"),
Self::Gzip => f.write_str("gz"),
}
}
}
/// Compress the image and return the corresponding information to insert into
@@ -910,18 +942,18 @@ fn compress_cow_size(mut raw_data: &[u8], block_size: u32) -> u64 {
/// update [`InstallOperation::data_offset`] in each operation manually because
/// the initial values are relative to 0.
///
/// If `need_cow_estimate` is true, the VABC CoW v2 + lz4 size estimate will be
/// computed. The caller must update [`PartitionUpdate::estimate_cow_size`] with
/// this value or else update_engine may fail to flash the partition due to
/// running out of space on the CoW block device. CoW v2 + other algorithms and
/// also CoW v3 are currently unsupported because there currently are no known
/// OTAs that use those configurations.
/// If `vabc_algo` is set, the VABC CoW v2 size estimate will be computed. The
/// caller must update [`PartitionUpdate::estimate_cow_size`] with this value or
/// else update_engine may fail to flash the partition due to running out of
/// space on the CoW block device. CoW v2 + other algorithms and also CoW v3 are
/// currently unsupported because there currently are no known OTAs that use
/// those configurations.
pub fn compress_image(
input: &(dyn ReadSeekReopen + Sync),
output: &(dyn WriteSeekReopen + Sync),
partition_name: &str,
block_size: u32,
need_cow_estimate: bool,
vabc_algo: Option<VabcAlgo>,
cancel_signal: &AtomicBool,
) -> Result<(PartitionInfo, Vec<InstallOperation>, Option<u64>)> {
const CHUNK_SIZE: u64 = 2 * 1024 * 1024;
@@ -980,8 +1012,8 @@ pub fn compress_image(
.map(
|(raw_offset, raw_data)| -> Result<(Vec<u8>, InstallOperation, u64)> {
let (data, digest_compressed) = compress_chunk(&raw_data, cancel_signal)?;
let cow_size = if need_cow_estimate {
compress_cow_size(&raw_data, block_size)
let cow_size = if let Some(algo) = vabc_algo {
algo.compressed_size(&raw_data, block_size)
} else {
0
};
@@ -1028,11 +1060,25 @@ pub fn compress_image(
hash: Some(digest_uncompressed.as_ref().to_vec()),
};
let cow_estimate = if need_cow_estimate {
// Because lz4_flex compresses better than official lz4.
let fudge = cow_estimate / 100;
let cow_estimate = if vabc_algo.is_some() {
// lz4_flex and miniz_oxide usually compress better than the lz4 and
// zlib implementations used by libsnapshot_cow. Make up for this by
// adding percentage-based overhead.
cow_estimate += cow_estimate / 100;
Some(cow_estimate + fudge)
// We also need to account for constant overhead, especially with
// smaller partitions. We can match what delta_generator normally adds
// in CowWriterV2::InitPos() exactly. Since we only ever create full
// OTAs, we can assume that all CoW operations are kCowReplaceOp.
// sizeof(CowHeader).
cow_estimate += 38;
// header_.buffer_size (equal to BUFFER_REGION_DEFAULT_SIZE).
cow_estimate += 2 * 1024 * 1024;
// CowOptions::cluster_ops * sizeof(CowOperationV2).
cow_estimate += 200 * 20;
Some(cow_estimate)
} else {
None
};
+20 -8
View File
@@ -12,6 +12,9 @@ security_patch_level = "2024-01-01"
# Google Pixel 7 Pro
# What's unique: init_boot (boot v4) + vendor_boot (vendor v4)
[profile.pixel_v4_gki]
vabc_algo = "Lz4"
[profile.pixel_v4_gki.partitions.boot]
avb.signed = true
data.type = "boot"
@@ -46,12 +49,15 @@ data.version = "vendor_v4"
data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
[profile.pixel_v4_gki.hashes]
original = "6b140c378d21eae2fa4fc581bce13a689b21bd32f5fba865698d1fd322f2f8c6"
patched = "a68b3a44c0cf015a225f92837c05fd0dec6e159584c5880eff30d12daa7123ff"
original = "c00f891f941f3dddb28966f7b07f3acea773bee104dace82b37c2d1341f09422"
patched = "ce9d8ee97828d233809742a5d3f23aa27b042675b1935ca9e3df0592c55788fd"
# Google Pixel 6a
# What's unique: boot (boot v4, no ramdisk) + vendor_boot (vendor v4, 2 ramdisks)
[profile.pixel_v4_non_gki]
vabc_algo = "Lz4"
[profile.pixel_v4_non_gki.partitions.boot]
avb.signed = true
data.type = "boot"
@@ -80,12 +86,15 @@ data.version = "vendor_v4"
data.ramdisks = [["init", "otacerts", "first_stage", "dsu_key_dir"], ["dlkm"]]
[profile.pixel_v4_non_gki.hashes]
original = "31963e6f81986c6686111f50e36b89e4d85ee5c02bc8e5ecd560528bc98d6fe7"
patched = "a13173a006ad94b25db682bb14fde2e36891417727d6541bdf5f9bca57d26751"
original = "4d692bc777b568b0626d3c08d2e6f83f1b472db5ad903486daaec6a78d0cc26e"
patched = "e27673e4f30933710c11d51f0e73849068cbe9bc9f54e6076bdd93f9a5c8ea0a"
# Google Pixel 4a 5G
# What's unique: boot (boot v3) + vendor_boot (vendor v3)
[profile.pixel_v3]
vabc_algo = "Lz4"
[profile.pixel_v3.partitions.boot]
avb.signed = true
data.type = "boot"
@@ -115,12 +124,15 @@ data.version = "vendor_v3"
data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
[profile.pixel_v3.hashes]
original = "e684aacb54464098c1b8e3f499efe35dff10ea792e89d71a83404620d0108b3e"
patched = "49e810ae76154bccf2dc7d810b9eec19d23a5b8fa32381469660488d0a25121b"
original = "f432dc7931520feb238474aa707dd5299747562ffe6129f3f763b5f11ac473ab"
patched = "3850a2e73bd783a1ec4a70c59f37d2374e017c20df7ab4b591182b14d187c18e"
# Google Pixel 4a
# What's unique: boot (boot v2)
[profile.pixel_v2]
vabc_algo = "Gzip"
[profile.pixel_v2.partitions.boot]
avb.signed = false
data.type = "boot"
@@ -144,5 +156,5 @@ data.type = "vbmeta"
data.deps = ["system"]
[profile.pixel_v2.hashes]
original = "ee9568797d9195985f14753b89949d8ebb08c8863a32eceeeec6e8d94661b1cf"
patched = "e413f1f87ee5ba53d402edad03b0df8af451b5b0323202a9d32b8327d433d340"
original = "1b45235b58054009cc496f6c3ee11d3dc16ed5c388c861761e26a6fce83103a0"
patched = "193b2dc70dd465d686f35c7b7f74d2cc1b06a55e48cf5c2e4df0f667e03032fc"
+4 -3
View File
@@ -5,8 +5,9 @@
use std::{ffi::OsString, path::PathBuf};
use avbroot::cli::args::{LogFormat, LogLevel};
use avbroot::cli::args::LogFormat;
use clap::{Args, Parser, Subcommand, ValueEnum};
use tracing::Level;
#[derive(Debug, Args)]
pub struct ProfileGroup {
@@ -69,8 +70,8 @@ pub struct Cli {
pub command: Command,
/// Lowest log message severity to output.
#[arg(long, global = true, value_name = "LEVEL", default_value_t)]
pub log_level: LogLevel,
#[arg(long, global = true, value_name = "LEVEL", default_value_t = Level::INFO)]
pub log_level: Level,
/// Output format for log messages.
#[arg(long, global = true, value_name = "FORMAT", default_value_t = LogFormat::Medium)]
+2
View File
@@ -6,6 +6,7 @@
use std::{collections::BTreeMap, fs, path::Path};
use anyhow::{Context, Result};
use avbroot::format::payload::VabcAlgo;
use serde::{Deserialize, Serialize};
use toml_edit::DocumentMut;
@@ -109,6 +110,7 @@ pub struct Partition {
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Profile {
pub vabc_algo: Option<VabcAlgo>,
pub partitions: BTreeMap<String, Partition>,
pub hashes: Hashes,
}
+11 -9
View File
@@ -577,6 +577,7 @@ fn create_payload(
partitions: &BTreeMap<String, Partition>,
inputs: &BTreeMap<String, PSeekFile>,
ota_info: &OtaInfo,
profile: &Profile,
key_ota: &RsaSigningKey,
cancel_signal: &AtomicBool,
) -> Result<(String, u64)> {
@@ -594,14 +595,14 @@ fn create_payload(
.map(PSeekFile::new)
.with_context(|| format!("Failed to create temp file for: {name}"))?;
let (partition_info, operations, cow_estimate) = payload::compress_image(
file,
&writer,
name,
4096,
dynamic_partitions_names.contains(name),
cancel_signal,
)?;
let vabc_algo = if dynamic_partitions_names.contains(name) {
profile.vabc_algo
} else {
None
};
let (partition_info, operations, cow_estimate) =
payload::compress_image(file, &writer, name, 4096, vabc_algo, cancel_signal)?;
compressed.insert(name, writer);
@@ -645,7 +646,7 @@ fn create_payload(
}],
snapshot_enabled: Some(true),
vabc_enabled: Some(true),
vabc_compression_param: Some("lz4".to_owned()),
vabc_compression_param: profile.vabc_algo.map(|a| a.to_string()),
cow_version: Some(2),
vabc_feature_set: None,
}),
@@ -746,6 +747,7 @@ fn create_ota(
&profile.partitions,
&inputs,
ota_info,
profile,
key_ota,
cancel_signal,
)