Skip to content

配置文件规范

警告!此规范内容是不稳定版本,可能会发生破坏兼容性的更新。当无法保障向下兼容时,将会升级此文档的主版本号,如从“1.0”更新到“2.0”。反之,普通更新只会升级次版本号,如“1.0”更新到“1.1”,其对“1.0”版本向下兼容。请在使用前确认此文档的版本号,并为将来可能发生的兼容性变化做好准备。

引言

本文档规定了配置文件的存储格式和安装路径、配置文件的读写、配置中心的设计、开发库 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 时的优先级顺序是:

    1. $APP_ROOT/configs/A/B/C/foo.example.json
    2. $APP_ROOT/configs/A/B/foo.example.json
    3. $APP_ROOT/configs/A/foo.example.json
    4. $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 描述文件内容示例:

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 文件放置路径(按优先级排序):

    1. /etc/dsg/configs/overrides/$appid/foo.example/
    2. $DSG_DATA_DIR/configs/overrides/$appid/foo.example/
    3. /etc/dsg/configs/overrides/org.dtk.core/
    4. $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:将配置项覆盖为可读可写
json
{
    "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,需记录二进制文件路径
json
{
    "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 所提供的配置项值的优先级顺序是:

      1. 某个具体应用程序的cache值
      2. 应用无关配置的cache值
      3. 某个具体应用程序的默认值
      4. 应用无关配置的默认值
      • 具体某个应用程序获取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
xml
<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 接口规范

此接口规范定义了开发库所提供的关于配置文件读写的相关接口,如果应用程序所使用的开发库实现了此规范,则程序应当优先使用开发库提供的接口。

接口的伪代码

cpp
class DConfig {
    DConfig(string namestring subpath);

    property list<string> keyList;

    signal valueChanged(string key);

    variant value(string key);
    void setValue(string key, variant value);
};
class DConfig {
    DConfig(string namestring subpath);

    property list<string> keyList;

    signal valueChanged(string key);

    variant value(string key);
    void setValue(string key, variant value);
};