- submit: by j4ger on 2023-08-14
- change: by j4ger on 2023-08-20
- change: by j4ger on 2023-08-25
- change: by j4ger on 2023-09-02
unit_parser
库提供了可以解析 Systemd Unit File 和部分 XDG Desktop Entry 文件的宏和函数,只需使用 #[derive]
宏为指定的 Unit Spec 定义 Struct 实现核心 Trait,即可直接在程序逻辑中调用解析函数得到对应 Struct 结果。
对单个服务/挂载点等操作时使用,如启动一个服务时。
应提供全部搜索路径(寻找模板等)和目标文件名。
fn load_named<P: AsRef<Path>, S: AsRef<str>>(paths: Vec<P>, name: S, root: bool) -> Result<Self, Error>;
接受参数:
- 搜索路径:Unit 存在的全部合法路径,包括 Template 和 drop-in 目录;
- Unit 名称;
- 是否工作于 root 权限下:会影响 Specifier 解析的结果。
将会在每个搜索路径搜索:
- Unit 本身;
-
<name>.d
等 drop-in 目录; -
subdir
Attribute 指定的<name>.<subdir>
目录作为subdir
Entry 的附加内容; - 若文件为 Instance,搜索其 Template。
首先,我们将一个 Systemd Unit File 的结构总结为 Section 和 Entry,其中:
- Section 从一个由中括号包裹的 Section 名开头,到下一个 Section 头部或文件尾结束;
- Section 体部分由 Entry 组成,具体形式为以
=
分隔的键值对。
以下文档以此 Unit File 为例:
# /usr/lib/systemd/system/sddm.service
[Unit]
Description=Simple Desktop Display Manager
Documentation=man:sddm(1) man:sddm.conf(5)
Conflicts[email protected]
After=systemd-user-sessions.service [email protected] plymouth-quit.service systemd-logind.service
PartOf=graphical.target
StartLimitIntervalSec=30
StartLimitBurst=2
[Service]
ExecStart=/usr/bin/sddm
Restart=always
[Install]
Alias=display-manager.service
该文件包含三个 Section:Unit Service 和 Install,以及内部的多条 Entry。
unit_parser
的核心 Trait 对应了 Unit File 的三个部分:
-
UnitConfig
:对应一类 Unit File,其中每个 Field 对应一个 Section,应当为实现了UnitSection
的 Struct; -
UnitSection
:对应一类 Section,其中每个 Field 对应一个 Entry,应当为实现了UnitEntry
的类型; -
UnitEntry
:对应一个 Entry 键值对,其类型应当实现UnitEntry
Trait,本质与std::str::FromStr
相同,库中已对默认实现std::str::FromStr
的类型实现了UnitEntry
,详见下一部分。
使用时,首先引入核心类型:
use unit_parser::prelude::*;
由此,针对上述 Unit File,编写的 Struct 定义如下:
#![allow(non_snake_case)]
use unit_parser::prelude::*;
#[derive(UnitConfig, Debug, Clone)]
#[unit(suffix = "service")]
struct ServiceUnit {
#[section(must)]
Unit: UnitSection,
#[section(must)]
Service: ServiceSection,
Install: Option<InstallSection>
}
#[derive(UnitSection, Debug, Clone)]
struct UnitSection {
#[entry(must)]
Description: String,
Documentation: Option<String>,
#[entry(multiple)]
Conflicts: Vec<String>,
#[entry(multiple)]
After: Vec<String>,
#[entry(multiple)]
PartOf: Vec<String>,
StartLimitIntervalSec: Option<u32>,
StartLimitBurst: Option<u32>,
}
#[derive(UnitSection, Debug, Clone)]
struct ServiceSection {
#[entry(must)]
ExecStart: String,
Restart: Option<RestartStrategy>,
}
#[derive(UnitEntry, Debug, Clone)]
enum RestartStrategy {
always, never,
}
#[derive(UnitSection, Debug, Clone)]
struct InstallSection {
#[entry(multiple)]
Alias: Vec<String>,
}
在主程序逻辑中,只需使用 UnitConfig
结构体上的 load_named
方法读取,如下:
let unit = ServiceUnit::load_named(vec!["/usr/lib/systemd/system/"], "sddm", true)?;
在结构体定义中,可以使用特殊的 Attribute 来改变解析时的行为。
所有 Unit Attribute 应用在 UnitConfig
结构体外部,使用 #[unit()]
作为外标记。
指定 Unit 文件的后缀名,若指定则解析目录时只会解析匹配后缀名的文件。
#[derive(UnitConfig, Debug, Clone)]
#[unit(suffix = "service")]
struct Unit {
#[section(default, must)]
Section: Section,
}
所有 Section Attribute 应用在 UnitConfig
结构体中的 Field 上,使用 #[section()]
作为外标记。
指定对应 Section 的默认值,若文件中未找到该 Section 则使用默认值。对应 Section 结构体需要实现 std::default::Default
,即其默认值。
#[derive(UnitConfig, Debug, Clone)]
struct Unit {
#[section(default, must)]
Section: Section,
}
#[derive(UnitSection, Debug, Clone)]
struct Section {
#[entry(must)]
Entry: u64,
}
impl Default for Section {
fn default() -> Self {
Self { Entry: 0 }
}
}
指定对应 Section 的键名。默认使用 Field 名作为 Section 名在文件中寻找 Section 定义,可使用 str
形式的键名覆盖这一行为。
#[derive(UnitConfig, Debug, Clone)]
struct Unit {
#[section(key = "AlternativeName", must)]
Section: Section,
}
指定对应的 Section 为必填。若不指定 must
,对应 Field 必须为 Option
。
指定 must
的 Field 会在解析错误时报错,在未找到该 Section 时也会报错。
#[derive(UnitConfig, Debug, Clone)]
struct Unit {
#[section(must)]
Section: Section,
OptionalSection: Option<OptionalSection>,
}
所有 Entry Attribute 应用在 UnitSection
结构体中的 Field 上,使用 #[entry()]
作为外标记。
指定对应 Entry 的默认值,若文件中未找到该 Entry 则使用默认值。需要传入一个 Expr
表达式。
#[derive(UnitSection, Debug, Clone)]
struct Section {
#[entry(default = 114, must)]
Entry: u64,
}
指定对应 Entry 的键名。默认使用 Field 名作为 Entry 名在文件中寻找 Entry 定义,可使用 str
形式的键名覆盖这一行为。
#[derive(UnitSection, Debug, Clone)]
struct Section {
#[entry(key = "AltKey", must)]
Entry: u64,
}
指定对应的 Entry 为必填。若不指定 must
或 multiple
,则对应 Field 必须为 Option
。
指定 must
的 Field 会在解析错误时报错,在未找到该 Entry 的时候也会报错。
#[derive(UnitSection, Debug, Clone)]
struct Section {
#[entry(must)]
Entry: u64,
OptionalEntry: Option<u64>,
}
在未找到值(或值解析出错)时的行为 | 有 must |
无 must |
---|---|---|
有 default |
不合法的设置(编译期报错) | 使用默认值 |
无 default |
运行时报错 | 必须为 Option ,结果为 None |
指定对应的 Entry 允许出现多次。默认情况下,最后一次出现的值会覆盖之前的值。指定 multiple
后,每次出现 Entry 时其值会被加入最终的 Vec
中。此外,每次解析字符串时,会首先按照空格分割字符串,再解析每一段,从而可以解析空格分隔的数组值。multiple
Field 必须为 Vec
。
#[derive(UnitSection, Debug, Clone)]
struct Section {
#[entry(multiple)]
Entry: Vec<u64>,
}
指定对应的 Entry 值可以由名为 <file name>.<subdir name>
目录下的文件名构成。解析时,将在所有搜索路径下查找对应格式的目录,并将其中所有文件名加入该 Entry 的值。subdir
Field 必须为 Vec
。
#[derive(UnitSection, Debug, Clone)]
struct Service {
#[entry(subdir = "wants", multiple)]
Wants: Vec<String>,
}
如当前读取文件名为 multi-user.target,解析时会尝试寻找 multi-user.target.wants 目录,并将其下所有文件名加入该数组。
UnitEntry
Trait 已为所有实现 std::str::FromStr
的类型完成实现,此外特殊实现包括:
-
bool
:根据 systemd.syntax 中的定义,yes
1
on
true
都被认为是true
,no
0
off
false
都被认为是false
; -
chrono::Duration
:根据 systemd.time 中的定义解析; -
chrono::DateTime<Utc>
:根据 systemd.time 中的定义解析; -
Enum
:自定义的枚举类型,可以使用#[derive(UnitEntry)]
自动实现UnitEntry
。
首先,通过编写 PEG 语法文件和使用 pest
库进行预解析,首先验证 Unit File 的基本结构是否合法。
解析后的内部状态被包装为 UnitParser
和 SectionParser
,其求值是惰性的,也不会产生额外的复制开销。UnitParser
是一个返回 SectionParser
的迭代器,SectionParser
是一个返回 (String, String)
键值对的迭代器。
unit_parser_macro
库使用 syn
库解析 #[derive()]
修饰的结构体定义,处理其键名、类型和所加的 Attribute。对每个 Unit 的解析逻辑(其上生成的 __parse_unit()
方法)如下:
- 对每个键初始化同名变量,值为
None
。 - 调用
unit_parser
函数获得UnitParser
,迭代其中每一个SectionParser
,用match
语句匹配其键名(或default
Attribute 指定的键名),并调用对应 Section 结构体的__parse_section
方法解析SectionParser
。根据 Attribute 定义的行为对解析结果进行操作。 - 判断每个 Field 是否有值,并根据定义的行为进行操作。
- 构造并返回
Self
。
类似地,每个 Section 的解析逻辑(其上生成的 __parse_section
方法)如下:
- 对每个键初始化同名变量,值为
None
,若为multiple
,则初始化为Vec::new()
。 - 迭代
SectionParser
其中每一个(String, String)
键值对,用match
语句匹配其键名(或default
Attribute 指定的键名),并调用对应 Entry 结构体的parse_from_str
方法解析。根据 Attribute 定义的行为对解析结果进行操作。 - 根据 Attribute 定义的行为对解析结果进行操作。
- 构造并返回
Self
。
为了实现 drop-in patching,每次解析时可选地传入一个已有的 Self Struct 以供修补,而非全部从头开始构造。
为了实现 Specifier 解析,需要传入是否工作于 root 模式,详见 systemd.unit 规范。在前期 PEG 解析时捕获所有形同 %x
的标记,并在解析时完成替换。
为了实现模板解析,读取之前需要判断文件类型,若为实例则提取模板进行解析,并通过 Specifier 解析将实例信息替换进去。
- XDG Desktop Entry 规范中定义了 locale 功能,在此并未实现;
- 全部上述功能
- 错误处理(单个文件错误不应影响其他)
- Calender Event 解析
- 英文文档
- 注释
- freedesktop.org - systemd.unit
- freedesktop.org - systemd.syntax
- freedesktop.org - systemd.time
- freedesktop.org - Desktop Entry Specification