配置文件规范
警告!此规范内容是不稳定版本,可能会发生破坏兼容性的更新。当无法保障向下兼容时,将会升级此文档的主版本号,如从“1.0”更新到“2.0”。反之,普通更新只会升级次版本号,如“1.0”更新到“1.1”,其对“1.0”版本向下兼容。请在使用前确认此文档的版本号,并为将来可能发生的兼容性变化做好准备。
- 维护者:zccrs zhangjide@uniontech.com
- 修改日期:2023.09.01
- 版本:1.0
- 议题:#3
引言
本文档规定了配置文件的存储格式和安装路径、配置文件的读写、配置中心的设计、开发库 API 接口等规范。
名词解释
- 配置描述文件:此类文件由安装包携带,类似于 gsettings 的 schemes 文件,用于描述配置项的元信息,以及携带配置项的默认值
- 配置存储文件:对于一些可修改的配置项,本文件用于保存程序运行过程中的改动
- $APP_ROOT:应用程序安装的根目录
- $appid:应用程序的唯一ID
- 应用无关配置: 与应用无关的通用配置,用于提供所有应用共享的配置
角色说明
- 应用程序:读写配置文件的实体,亦是配置文件的提供者,一个应用程序可提供多个配置文件
- 配置文件:包含一系列配置项的集合,存储了配置项相关的信息
- 配置中心:为程序提供读写配置项的 DBus 接口,实现配置项的 override 机制
- DTK:应用程序开发基础库,提供统一的配置文件读写工具类
配置描述文件
配置描述文件使用 json 格式提供,以下将“配置描述文件”简称为“描述文件”。
文件名:$appid.json,如:foo.example.json
安装路径:
- $APP_ROOT/configs/:用于放置应用程序所携带的描述文件
- $DSG_DATA_DIR/configs/:用于放置开发库所携带的描述文件和
应用无关配置
的描述文件,如 $DSG_DATA_DIR/configs/org.dtk.core.json,此目录下的配置描述文件为所有程序共享。如果安装到 $DSG_DATA_DIR/configs/ 下的配置文件与 $APP_ROOT/configs/ 中的同名(忽略子目录),则使用 $APP_ROOT/configs/ 下的文件
子目录:描述文件安装可包含子路径。
假设 foo.example.json 安装到 “$APP_ROOT/configs/A/B”,当程序读取配置文件时,可额外传入 subpath 参数,假设传入的参数为 “/A/B/C”,则查找 foo.example.json 时的优先级顺序是:
- $APP_ROOT/configs/A/B/C/foo.example.json
- $APP_ROOT/configs/A/B/foo.example.json
- $APP_ROOT/configs/A/foo.example.json
- $APP_ROOT/configs/foo.example.json
依次从上往下,直到找到一个存在的文件为止。当 subpath 为空时,直接读取 $APP_ROOT/configs/foo.example.json。安装到 $DSG_DATA_DIR/configs/ 下的配置文件同理。
描述文件包含下列属性:
- magic:此 json 文件的标识性信息,所有描述文件均标记为 “dsg.config.meta”
- version:此描述文件的内容格式的版本。版本号使用两位数字描述,首位数字不同的描述文件相互之间不兼容,第二位数字不同的描述文件需满足向下兼容。解析此描述文件的程序要根据版本进行内容分析,当遇到不兼容的版本时,需要立即终止解析,并向使用者报告错误信息,如 “1.0” 和 “2.0” 版本之间不兼容,如果解析程序最高只支持 1.0 版本,则遇到 2.0 版本的描述文件时应该报告错误,但是如果遇到 1.1 版本,则可以继续执行。
contents:配置项的内容,每一个配置项是一个 json 对象,配置项之间的相对顺序无意义,且不支持嵌套。每一项配置可包含下列属性:
- value:配置项的默认值,可使用 json 支持的各种数据类型,如字符串、数字、数组、对象等
- serial:单调递增的整数值,使用场景:假设程序 “A” 的配置项 “a” 记录其是否已经进行了初始化,之后 “A” 可能更改了初始化相关的代码,需要确保版本更新之后能重新进行初始化,则可以将配置项 “a” 的 “serial” 属性增加 1,则旧版 “A” 程序所记录的配置项 “a” 将失效,以此确保更新 “A” 之后能再次进行初始化工作。此配置项可以省略,无此项时读取配置存储文件将忽略 serial 字段。
- name:配置项的可显示名称,需国际化(使用DTK工具为其生成 ts 文件,ts 编译后的 qm 文件需要与配置描述文件同名同路径放置)。此名称可用于展示到用户界面,如当程序 A 请求通过配置中心读取程序 B 的某个配置项时,将提示用户“程序 A 请求获取程序 B 的"允许退出"配置项的值,是否允许?”,用户可选择拒绝程序 A 的请求,名称在这里的作用是利于用户理解此配置项的含义。
- description:描述此配置项的用途,需国际化(同 name)
- permissions:配置项的权限
- readonly:不允许修改,当程序读取此配置时,将直接使用默认值
- readwrite:可读可写,如果此值被修改过,则不再使用此处定义的默认值
- visibility:配置项的可见性
- private 仅限程序内部使用,对外不可见。此类配置项完全由程序自己读写,可随意增删改写其含义,无需做兼容性考虑
- public 外部程序可使用。此类配置项一旦发布,在兼容性版本的升级中,要保障此配置项向下兼容,简而言之,只允许在程序/库的大版本升级时才允许删除或修改此类配置项,当配置项的 permissions、visibility、flags 任意一个属性被修改则认为此配置项被修改,除此之外修改 value、name、description 属性时则不需要考虑兼容性
- flags:配置项的一些特性
- nooverride:存在此标记时,将表明则此配置项不可被覆盖(详见下述 override 机制)。反之,不存在此标记时表明此配置项允许被覆盖,对于此类配置项,如若其有界面设置入口,则当此项不可写时,应当隐藏或禁用界面的设置入口
- global:当读写此类配置时,将忽略用户身份,无论程序使用哪个用户身份执行,读操作都将获取到同样的数据,写操作将对所有用户都生效。但是,如果对应的配置存储目录不存在或无权限写入,则忽略此标志
$APP_ROOT/configs/foo.example.json 描述文件内容示例:
{
"magic": "dsg.config.meta",
"version": "1.0",
"contents": {
"key1": {
"value": value1,
"time": "2017-07-24T15:46:29",
"name": "允许退出",
"description": "xxxxxxxx",
"permissions": "readwrite",
"visibility": "private",
"flags": ["nooverride"]
}
}
}
{
"magic": "dsg.config.meta",
"version": "1.0",
"contents": {
"key1": {
"value": value1,
"time": "2017-07-24T15:46:29",
"name": "允许退出",
"description": "xxxxxxxx",
"permissions": "readwrite",
"visibility": "private",
"flags": ["nooverride"]
}
}
}
override 机制
以 foo.example.json 和 org.dtk.core.json 为例
override 文件放置路径(按优先级排序):
- /etc/dsg/configs/overrides/$appid/foo.example/
- $DSG_DATA_DIR/configs/overrides/$appid/foo.example/
- /etc/dsg/configs/overrides/org.dtk.core/
- $DSG_DATA_DIR/configs/overrides/org.dtk.core/
同配置描述文件一样支持子目录机制,忽略其描述文件所对应的子目录,从头按规则顺序查找 override 目录
对于第 3 和第 4 类路径,其省略了 $appid,放置在此目录下的文件对所有应用程序都生效,表示为所有应用程序提供对 org.dtk.core 配置的覆盖
$DSG_XDG_DATA/configs
下的路径用于放置安装包携带的文件/etc
下的路径用于放置动态创建的文件,比如用户手动添加,或者域管等程序在运行时创建文件名只允许使用拉丁字符,后缀为 ".json"。使用自然排序(如“a2”在“a11”之前)规则,按文件名排序,越靠后的配置文件优先级越高。
可以覆盖配置项的 "value"、"permissions" 属性
以 /etc/dsg/configs/overrides/foo.example/foo.example/oem1-override.json 为例,可包含以下属性:
- magic:此 json 文件的标识性信息,所有 override 文件均标记为 “dsg.config.override”
- version:此文件的内容格式的版本。版本号使用两位数字描述,首位数字不同的描述文件相互之间不兼容,第二位数字不同的描述文件需满足向下兼容。解析此描述文件的程序要根据版本进行内容分析,当遇到不兼容的版本时,需要立即终止解析,忽略此文件,并在程序日志中写入警告信息,如 “1.0” 和 “2.0” 版本之间不兼容,如果解析程序最高只支持 1.0 版本,则遇到 2.0 版本的描述文件时应该终止解析,但是如果遇到 1.1 版本,则可以继续执行。
- contents:覆盖的配置项的内容,每一项是一个 json 对象,项之间的相对顺序无意义。每一项可包含下列属性:
- value:覆盖配置项的默认值
- serial:覆盖配置项对应的 serial 属性
- comment:描述此 override 行为的注释内容
- permissions:覆盖配置项的权限
- readonly:将配置项覆盖为只读
- readwrite:将配置项覆盖为可读可写
{
"magic": "dsg.config.override",
"version": "1.0",
"contents": {
"key1": {
"value": value1,
"comment": "xxxxxxxx",
"permissions": "readonly"
}
}
}
{
"magic": "dsg.config.override",
"version": "1.0",
"contents": {
"key1": {
"value": value1,
"comment": "xxxxxxxx",
"permissions": "readonly"
}
}
}
配置存储文件
使用 json 作为配置文件的存储格式,以下称为“存储文件”。根据配置项是否携带 global 标志,将分开存储,以 foo.example.json 为例,分别存储在(下列路径用于存储程序的运行时修改的配置内容,请勿往此目录安装任何文件):
用户级别:$USER_CACHE_PREFIX/$appid/foo.example.json,推荐$USER_CACHE_PREFIX为/$HOME/.config/dsg/configs,对于多用户场景,$USER_CACHE_PREFIX需要区分不同用户的配置存储目录
global 级别:$GLOBAL_CACHE_PREFIX/foo.example.json,推荐$GLOBAL_CACHE_PREFIX为$DSG_APP_DATA/configs
如果设置 subpath,则只从 subpath 中查找保存的配置文件,不 fallback 到上级目录
foo.example.json 文件格式如下:
- magic:此 json 文件的标识性信息,所有存储文件均标记为 “dsg.config.cache”
- version:此文件的内容格式的版本。版本号使用两位数字描述,首位数字不同的描述文件相互之间不兼容,第二位数字不同的描述文件需满足向下兼容。读取此描述文件的程序要根据版本进行内容分析,当遇到不兼容的版本时,需要立即终止解析,忽略此文件,并在程序日志中写入警告信息,如 “1.0” 和 “2.0” 版本之间不兼容,如果解析程序最高只支持 1.0 版本,则遇到 2.0 版本的描述文件时应该终止解析,但是如果遇到 1.1 版本,则可以继续执行。写入此描述文件时,遇到不兼容的版本时,需要先清空当前内容再写入,每次写入皆需更新此字段。
- contents:保存的配置项的内容,每一项是一个 json 对象,项之间的相对顺序无意义。每一项可包含下列属性:
- value:保存修改后的值
- serial:在使用此配置项的存储值之前,应当先与配置描述文件中定义的 serial 属性做比较(需遵守 override 机制),如果此值不等于配置表述文件中记录的值,否则忽略此处存储的值
- time:记录值的修改时间,使用 UTC 时间,采用 ISO 8601 表示方法
- user:记录修改此项设置的用户名称
- appid:记录修改此项设置的应用程序id,如无法正常获取程序id,需记录二进制文件路径
{
"magic": "dsg.config.cache",
"version": "1.0",
"contents": {
"key1": {
"value": value1,
"time": "2017-07-24T15:46:29",
"user": "user name",
"appid": "foo.example",
"serial": 0
}
}
}
{
"magic": "dsg.config.cache",
"version": "1.0",
"contents": {
"key1": {
"value": value1,
"time": "2017-07-24T15:46:29",
"user": "user name",
"appid": "foo.example",
"serial": 0
}
}
}
假设对于
应用无关配置
的配置文件foo.example.json 安装到“$APP_ROOT/configs”目录。获取 foo.example 所提供的配置项值的优先级顺序是:
- 某个具体应用程序的cache值
- 应用无关配置的cache值
- 某个具体应用程序的默认值
- 应用无关配置的默认值
- 具体某个应用程序获取foo.example所提供的配置项值时,查找路径依次为1, 2, 3, 4
应用无关配置
程序获取foo.example所提供的配置项值时,查找路径依次为2, 4
如果某个
应用无关配置
的配置项值发生变化,应当通知其它所有需要监听此配置项的程序。
配置中心
配置中心为上述配置文件的管理服务,对外提供配置项的读写接口。基于 DBus 服务实现,关于 DBus 的规范请查看:https://dbus.freedesktop.org/doc/dbus-specification.html。
配置中心需要监听配置描述文件
和配置override目录
的变化,当描述文件被修改或override目录新增、移除、修改文件时,需要重新解析对应的文件内容,但是不需要监听配置存储文件
的变化。
与程序的关系
当配置中心的 DBus 服务存在时(需要注意,仅需要它存在,不要求它一定处于运行状态),所有配置项的读写皆要通过此服务进行。反之,可直接基于文件进行配置项的读写操作,无需考虑文件竞争的情况,且无需在修改配置项时通知其他进程。
配置中心的 DBus 接口
- 服务名:org.desktopspec.ConfigManager
- 路 径:/org/desktopspec/ConfigManager
<interface name='org.desktopspec.ConfigManager'>
<method name='acquireManager'>
<arg type='s' name='appid' direction='in'/>
<arg type='s' name='name' direction='in'/>
<arg type='s' name='subpath' direction='in'/>
<arg type='o' name='path' direction='out'/>
</method>
</interface>
<interface name='org.desktopspec.ConfigManager.Manager'>
<property access="read" type="s" name="version"/>
<property access="read" type="as" name="keyList"/>
<method name='value'>
<arg type='s' name='key' direction='in'/>
<arg type='v' name='value' direction='out'/>
</method>
<method name='setValue'>
<arg type='s' name='key' direction='in'/>
<arg type='v' name='value' direction='in'/>
</method>
<method name='name'>
<arg type='s' name='key' direction='in'/>
<arg type='s' name='language' direction='in'/>
<arg type='s' name='name' direction='out'/>
</method>
<method name='description'>
<arg type='s' name='key' direction='in'/>
<arg type='s' name='language' direction='in'/>
<arg type='s' name='description' direction='out'/>
</method>
<method name='visibility'>
<arg type='s' name='key' direction='in'/>
<arg type='s' name='visibility' direction='out'/>
</method>
<!--采用引用计数的方式,引用为 0 时才真正的销毁-->
<method name='release'>
</method>
<signal name="valueChanged">
<arg name="key" type="s" direction="out"/>'
</signal>
</interface>
<interface name='org.desktopspec.ConfigManager'>
<method name='acquireManager'>
<arg type='s' name='appid' direction='in'/>
<arg type='s' name='name' direction='in'/>
<arg type='s' name='subpath' direction='in'/>
<arg type='o' name='path' direction='out'/>
</method>
</interface>
<interface name='org.desktopspec.ConfigManager.Manager'>
<property access="read" type="s" name="version"/>
<property access="read" type="as" name="keyList"/>
<method name='value'>
<arg type='s' name='key' direction='in'/>
<arg type='v' name='value' direction='out'/>
</method>
<method name='setValue'>
<arg type='s' name='key' direction='in'/>
<arg type='v' name='value' direction='in'/>
</method>
<method name='name'>
<arg type='s' name='key' direction='in'/>
<arg type='s' name='language' direction='in'/>
<arg type='s' name='name' direction='out'/>
</method>
<method name='description'>
<arg type='s' name='key' direction='in'/>
<arg type='s' name='language' direction='in'/>
<arg type='s' name='description' direction='out'/>
</method>
<method name='visibility'>
<arg type='s' name='key' direction='in'/>
<arg type='s' name='visibility' direction='out'/>
</method>
<!--采用引用计数的方式,引用为 0 时才真正的销毁-->
<method name='release'>
</method>
<signal name="valueChanged">
<arg name="key" type="s" direction="out"/>'
</signal>
</interface>
API 接口规范
此接口规范定义了开发库所提供的关于配置文件读写的相关接口,如果应用程序所使用的开发库实现了此规范,则程序应当优先使用开发库提供的接口。
接口的伪代码
class DConfig {
DConfig(string name, string subpath);
property list<string> keyList;
signal valueChanged(string key);
variant value(string key);
void setValue(string key, variant value);
};
class DConfig {
DConfig(string name, string subpath);
property list<string> keyList;
signal valueChanged(string key);
variant value(string key);
void setValue(string key, variant value);
};