OpenWrt达人教程之开发人员入门指南

OpenWrt 作为一款基于 Linux 的嵌入式系统,很多功能特性其实与 Linux 一样,软件开发的难点在于解决交叉编译时出现的问题,为了新入门的开发人员更快的上手,特在此汇总一些最常用的开发说明,当然你也可以直接访问 OpenWrt 官方网站获取更全面的开发人员指南。OpenWrt Project: Developer guide

遵纪守法

虽然软件、网络等技术产物是虚拟的,但最终是作用于现实生活中,与大众的日常生活息息相关,所以必然要接受各种法律法规的管控,各类技术人员首先需要学习并牢记当地的法律法规,以免误入歧途,玩火自焚。

在中国工作和生活的技术人员请学习如下法律法规:
《中华人民共和国网络安全法》《计算机信息网络国际联网安全保护管理办法》《计算机软件保护条例》《中华人民共和国计算机信息系统安全保护条例》

LUCI 界面

OpenWrt 的界面其实就是网页界面,默认是由 uhttpd 服务器承载,之所以叫做 LUCI ,因为这是使用 Lua 脚本编写的控制界面,全称 Lua Unified Configuration Interface,当然目前已经不再使用 Lua 脚本了,从 OpenWrt 19.07.4 版开始,界面已经切换为使用 JavaScript 脚本来编写,其拥有更便利的页面控件,页面自由度也大大提高,因为脚本交由客户端运行,页面流畅度自然也比 Lua 界面高出不少。

下面仅介绍 OpenWrt 的 JavaScript 脚本界面规范。

LUCI 网页界面的意义:各类程序的配置文件直接手动配置也是完全没问题的,但提供网页界面可以更直观的管理各项配置参数,并且可以通过界面对输入的参数进行一些限制,防止因手误输入错误的参数,具备一定程度的防呆功能,这对于复杂的各类软件功能是很有帮助的。

完整的界面文件结构

以源代码目录的文件结构为例,一个基本的界面程序应当具备如下所示的目录文件结构。

openwrt
┕feeds
  ┕luci
    ┕applications
      ┕luci-app-name # 界面程序的主目录
        ┕htdocs
        ┊ ┕luci-static
        ┊   ┕resources
        ┊     ┕view
        ┊       ┕name.js # JavaScript 脚本界面文件。
        ┕po
        ┊ ┕zh_Hans # 此目录名称对应简体中文。
        ┊   ┕name.po # 界面语言翻译文件。
        ┕root
        ┊ ┕etc
        ┊ ┊ ┕uci-defaults
        ┊ ┊   ┕luci-app-name # 软件安装完毕后,默认执行的脚本(一次性脚本),可选。
        ┊ ┕usr
        ┊   ┕share
        ┊     ┕luci
        ┊     ┊ ┕menu.d
        ┊     ┊   ┕luci-app-name.json # 界面菜单,在系统菜单中的名称、顺序等。
        ┊     ┕rpcd
        ┊       ┕acl.d
        ┊         ┕luci-app-name.json # 权限控制文件,管控界面能执行的各类操作。
        ┕Makefile # 编译文件。

界面程序的 Makefile 编写指南

#
# Copyright (C) 2020 OpenWrt.org
#
# This is free software, licensed under the GNU General Public License v2.
# See /LICENSE for more information.
# 注释信息,可选。

# 所有的可选项,不需要时可以直接不写。

# 加载相关规则文件,必需。
include $(TOPDIR)/rules.mk

# 在 OpenWrt 编译菜单中显示的标题,必需
LUCI_TITLE:=My package - LuCI interface
# 依赖关系,可选
LUCI_DEPENDS:=+luci-mod-admin-full
# 是否要限制硬件平台,可选
LUCI_PKGARCH:=all
# 版本号,可选
PKG_VERSION:=1.0
# 修订版本号,可选
PKG_RELEASE:=1
# 标记日期,可选
PKG_DATE:=20201130


# 作者信息,可选
PKG_MAINTAINER:=OpenWrt-Life <[email protected]>
# 软件许可信息,可选
PKG_LICENSE:=Apache-2.0

# 加载相关规则文件,必需。
include ../../luci.mk

# 下面一行是 Luci 界面专用调用标识,必需,如果缺失会导致不会被加入 OpenWrt 的编译菜单中。
# call BuildPackage - OpenWrt buildroot signature

JavaScript 脚本界面

至于如何编写完整的 JavaScript 脚本界面,不在本指南范围内,你应该先去学会通用的 JavaScript 编写知识,这里只额外说明 OpenWrt 平台上特有的一些接口或注意事项。

以源代码中已有的 Samba4 的 JavaScript 脚本界面为例。

'use strict';
//使用紧凑格式,编译时会自动压缩此脚本。
'require view';
'require fs';
'require form';
'require tools.widgets as widgets';
//上面这些都是声明的接口调用,OpenWrt 下有效。

return view.extend({
	load: function() {
		return Promise.all([
			L.resolveDefault(fs.stat('/sbin/block'), null),
//读取文件状态。
			L.resolveDefault(fs.stat('/etc/config/fstab'), null),
			L.resolveDefault(fs.stat('/usr/sbin/nmbd'), {}),
			L.resolveDefault(fs.stat('/usr/sbin/samba'), {}),
			L.resolveDefault(fs.stat('/usr/sbin/winbindd'), {}),
			L.resolveDefault(fs.exec('/usr/sbin/smbd', ['-V']), null),
//执行命令,获取版本号。
		]);
	},
	render: function(stats) {
		var m, s, o, v;
		v = '';
		
		m = new form.Map('samba4', _('Network Shares-samba4'));
//关联配置文件/etc/config/samba4,括号内为页面标题名称。

		if (stats[5] && stats[5].code === 0) {
			v = stats[5].stdout.trim();
		}
		s = m.section(form.TypedSection, 'samba', 'Samba ' + v);
//配置名为 samba 的子配置节点,此处也为页面说明,这里仅用于显示 samba 的版本号了。
		s.anonymous = true;
//隐藏配置文件中的 Section 节点名称。

		s.tab('general',  _('General Settings'));
//子菜单,选项卡界面。
		s.tab('template', _('Edit Template'));

		s.taboption('general', form.Flag, 'enable', _('Enable'));
//复选框选项,此选项位于'general'子菜单下。

		s.taboption('general', widgets.NetworkSelect, 'interface', _('Interface'),
//这是菜单名称
			_('Listen only on the given interface or, if unspecified, on lan'));//这是菜单注释,详细说明。

		o = s.taboption('general', form.Value, 'workgroup', _('Workgroup'));
		o.placeholder = 'WORKGROUP';
//占位符,用于提示用户应该输入什么样的字符。

		o = s.taboption('general', form.Value, 'description', _('Description'));
		o.placeholder = 'Samba4 on OpenWrt';

		s.taboption('general', form.Flag, 'enable_extra_tuning', _('Enable extra Tuning'),
			_('Enable some community driven tuning parameters, that may improve write speeds and better operation via WiFi.'));

		s.taboption('general', form.Flag, 'allow_legacy_protocols', _('Allow legacy protocols'),
			_('Allow connection using smb(v1) protocol.'));

		s.taboption('general', form.Flag, 'disable_async_io', _('Force synchronous I/O'),
			_('On lower-end devices may increase speeds, by forceing synchronous I/O instead of the default asynchronous.'));

		s.taboption('general', form.Flag, 'macos', _('Enable macOS compatible shares'),
			_('Enables Apple\'s AAPL extension globally and adds macOS compatibility options to all shares.'));

		o = s.taboption('general', form.Value, 'nice', _('Scheduling priority'),
			_('Set the scheduling priority of the spawned process.'));
		o.datatype = 'range(-20,19)';
//限制此输入框的格式,只允许输入-20至19的数字。
		o.default = '0';
//此选项的默认值。
		o.rmempty = false;
//是否允许为空值。false则表示否,不允许为空值。

		if (stats[2].type === 'file') {
			s.taboption('general', form.Flag, 'disable_netbios', _('Disable Netbios')) 
		}
//符合判断条件才会显示出来的菜单。
		if (stats[3].type === 'file') {
			s.taboption('general', form.Flag, 'disable_ad_dc', _('Disable Active Directory Domain Controller')) 
		}
		if (stats[4].type === 'file') {
			s.taboption('general', form.Flag, 'disable_winbind', _('Disable Winbind')) 
		}
		
		o = s.taboption('template', form.TextValue, '_tmpl',
			_(''),
			_("This is the content of the file '/etc/samba/smb.conf.template' from which your samba configuration will be generated. "
			+ "Values enclosed by pipe symbols ('|') should not be changed. They get their values from the 'General Settings' tab."));
//这句话太长,不方便阅读,可以切断,用 + 号连接,这样程序仍然会认为这是一句话。
		o.rows = 20;
//行高
		o.cfgvalue = function(section_id) {
			return fs.trimmed('/etc/samba/smb.conf.template');
		};
//读取指定的文件。
		o.write = function(section_id, formvalue) {
			return fs.write('/etc/samba/smb.conf.template', formvalue.trim().replace(/\r\n/g, '\n') + '\n');
		};
//写入数据到指定的文件。


		s = m.section(form.TableSection, 'sambashare', _('Shared Directories'),
			_('Please add directories to share. Each directory refers to a folder on a mounted device.'));//配置名为 sambashare 的子配置节点
		s.anonymous = true;
		s.addremove = true;
//允许添加或删除此配置节点

		s.option(form.Value, 'name', _('Name'));
		o = s.option(form.Value, 'path', _('Path'));
		if (stats[0] && stats[1]) {
			o.titleref = L.url('admin', 'system', 'mounts');
		}

		o = s.option(form.Flag, 'browseable', _('Browse-able'));
		o.enabled = 'yes';
//使复选框选项使用指定的参数,勾选则写入参数 yes
		o.disabled = 'no';
//使复选框选项使用指定的参数,不勾选则写入参数 no
		o.default = 'yes';

		o = s.option(form.Flag, 'read_only', _('Read-only'));
		o.enabled = 'yes';
		o.disabled = 'no';
		o.default = 'no'; // smb.conf default is 'yes'
		o.rmempty = false;

		s.option(form.Flag, 'force_root', _('Force Root'));

		o = s.option(form.Value, 'users', _('Allowed users'));
		o.rmempty = true;

		o = s.option(form.Flag, 'guest_ok', _('Allow guests'));
		o.enabled = 'yes';
		o.disabled = 'no';
		o.default = 'yes'; // smb.conf default is 'no'
		o.rmempty = false;

		o = s.option(form.Flag, 'guest_only', _('Guests only'));
		o.enabled = 'yes';
		o.disabled = 'no';
		o.default = 'no';
		
		o = s.option(form.Flag, 'inherit_owner', _('Inherit owner'));
		o.enabled = 'yes';
		o.disabled = 'no';
		o.default = 'no';

		o = s.option(form.Value, 'create_mask', _('Create mask'));
		o.maxlength = 4;
//限制字符长度,4表示最多只允许4个英文字符长度。
		o.default = '0666'; // smb.conf default is '0744'
		o.placeholder = '0666';
		o.rmempty = false;

		o = s.option(form.Value, 'dir_mask', _('Directory mask'));
		o.maxlength = 4;
		o.default = '0777'; // smb.conf default is '0755'
		o.placeholder = '0777';
		o.rmempty = false;
		
		o = s.option(form.Value, 'vfs_objects', _('Vfs objects'));
		o.rmempty = true;
		
		s.option(form.Flag, 'timemachine', _('Apple Time-machine share'));
		
		o = s.option(form.Value, 'timemachine_maxsize', _('Time-machine size in GB'));
		o.rmempty = true;
		o.maxlength = 5;

		return m.render();
	}
});

界面脚本与配置文件是对应关系,将关联的配置文件 /etc/config/samba4 内容贴出来,两相对照才能更准确的理解各个参数的意义。


config samba
	option name 'OpenWrt'
	option workgroup 'WORKGROUP'
	option description 'Samba on OpenWrt'
	option charset 'UTF-8'
	option enable '1'
	option disable_ad_dc '1'
	option disable_winbind '1'
	option interface 'lan'
	option nice '0'

config sambashare
	option name 'sda'
	option path '/mnt/sda3'
	option read_only 'no'
	option force_root '1'
	option guest_ok 'yes'
	option create_mask '0666'
	option dir_mask '0777'

通过上面的实例,你应该已经初步知晓了, OpenWrt 当前的 JavaScript 脚本界面的运行规则了,下面介绍一些常用的其它脚本规则。

提示:源码目录自带开发文档,请查阅 cd openwrt/feeds/luci/docs

获取网络接口的选项

'require tools.widgets as widgets';//申明调用接口

//获取接口的网络名称,例如 lan wan wan6
s.option('general', widgets.NetworkSelect, 'interface', _('Interface'));
'require tools.widgets as widgets';//申明调用接口

//获取接口的设备名称,例如 eth0 br-lan
s.option('general', widgets.DeviceSelect, 'interface', _('Interface name'));

获取系统用户列表

'require tools.widgets as widgets';//申明调用接口

//获取操作系统内的用户列表。
s.option('general', widgets.UserSelect, 'user', _('Run daemon as user'));

配置一个下拉列表框,只允许用户选择预设的参数。

'require form';//申明调用接口

o = s.option(form.ListValue, 'leasetime', _('Lease time'));
o.value('1h', _('One hour'));
//下拉列表框将显示预设的选项名称,此名称可以使用翻译文件进行转换。
o.value('2h');
//此选项将直接显示为 2h
o.value('1d', _('One day'));
o.value('7d', _('A week'));
o.rmempty = true;//允许此选项为空值,即允许此选项不存在。

配置一个下拉列表框,允许用户选择预设的参数,也允许用户自行输入参数。

'require form';//申明调用接口

o = s.option(form.Value, 'leasetime', _('Lease time'));
o.value('1h', _('One hour'));
//下拉列表框将显示预设的选项名称,此名称可以使用翻译文件进行转换。
o.value('2h');
//此选项将直接显示为 2h
o.value('1d', _('One day'));
o.value('7d', _('A week'));
o.rmempty = true;//允许此选项为空值,即允许此选项不存在。

配置一个动态列表,允许自由添加删除多个参数,展示一个实例样板。

'require form';//申明调用接口

o = s.taboption('general', form.DynamicList, 'address', _('Static address'),
    _('List of domains to force to an IP address'));

o.optional = true;//表示此选项为可选属性。
o.placeholder = '/openwrt.xyz/192.168.9.1';
//占位符,用于提示用户应该输入什么样的字符。

配置含多个子菜单的选项卡界面

'require form';//申明调用接口

o = s.tab("general", _("General Settings"));
o = s.tab("advanced", _('Advanced Settings'));

o = s.taboption('general', form.Flag, "enabled", _("Enabled"));

o = s.taboption('advanced', form.Flag, "enable", _("Enable"));

配置选项关联另外的某个选项,当关联的选项为指定的值时才显示。

o.depends("advanced", "1");
//关联选项 advanced 参数为 1 时,此选项才会显示。

限制选项参数格式的常用类型

o.datatype = 'string';//允许任意字符组合。(此为默认值,即如果不指定数据类型就默认是这个。)
o.datatype = 'uinteger';//只允许输入正整数。
o.datatype = 'range(-20,19)';//只允许输入范围内的数值。(只允许输入-20至19的数字)
o.datatype = 'list(string)';//限制此选项的格式为列表值,一般用于配合动态列表。
o.datatype = 'directory';//只允许输入路径格式,且此目录必须已经存在。
o.datatype = 'file';//只允许输入文件路径,且此文件必须已经存在。
o.datatype = 'hostname';//限制参数为主机名。
o.datatype = 'host';//限制参数为网站域名。
o.datatype = 'port';//只允许输入端口号。
o.datatype = 'portrange';//限制参数为端口范围,即允许的书写格式如: 1025-65535
o.datatype = 'ipaddr';//限制参数为ip地址。
o.datatype = 'ip4addr';//限制参数为ipv4地址。
o.datatype = 'ip6addr';//限制参数为ipv6地址。
o.datatype = 'ipaddrport';//限制参数为ip地址加端口,书写格式如:127.0.0.1:80
o.datatype = 'or(ipaddr,hostname)';//限制参数为IP地址或者主机名。
o.datatype = 'max(1024)';//限制参数最大值。
o.datatype = 'min(60)';//限制参数最小值。

界面语言翻译文件

在对应的语言目录下创建 po 翻译文件即可。

# 使用命令快速创建 po 翻译文件示例。
cd openwrt/feeds/luci
./build/i18n-scan.pl applications/luci-app-samba4/ > applications/luci-app-samba4/po/zh_Hans/samba4.po
#
# OpenWrt-Life <[email protected]>, 2020.
#
# msgid 为源语言文字。
# msgstr 为翻译后的文字。

# 这是可选注释,标记这个词句在源代码 JavaScript 脚本中的第几行。
#: applications/luci-app-samba4/htdocs/luci-static/resources/view/samba4.js:97
msgid "Allow guests"
msgstr "允许匿名用户"

# 对于比较长的语句,为了方便阅读源码,可以断句书写,见下面的实例。
# 但需要注意,不允许出现连续两个空格,否则会导致翻译失效。
#: applications/luci-app-samba4/htdocs/luci-static/resources/view/samba4.js:46
msgid ""
"Enables Apple's AAPL extension globally and adds macOS compatibility options "
"to all shares."
msgstr "全局启用 Apple 的 AAPL 扩展,并为所有共享添加 macOS 兼容性选项。"

#: applications/luci-app-samba4/htdocs/luci-static/resources/view/samba4.js:63
msgid ""
"This is the content of the file '/etc/samba/smb.conf.template' from which "
"your samba configuration will be generated. Values enclosed by pipe symbols "
"('|') should not be changed. They get their values from the 'General "
"Settings' tab."
msgstr ""
"这是将从其上生成 samba 配置的文件“/etc/samba/smb.conf.template”的内容。由管道"
"符(“|”)包围的值不应更改。它们将从“常规设置”标签中获取其值。"

# 翻译文件的冲突优先级。
# 当同一个源词句被翻译为不同的词句,则最后安装的会优先显示于所有界面。
# 例如单词 “Enable”,有 A、B、C 软件界面都含有这个源词语,但翻译词语却各不相同。
# 例如 A 翻译为“启用”,B 翻译为“打开”,C 翻译为“开启” 
#  A 安装后,“Enable”显示为“启用”
#  B 安装后,所有界面的“Enable”将显示为“打开”
#  C 安装后,所有界面的“Enable”将显示为“开启”
# 所以为了避免出现这种翻译混乱,请各位开发者用词准确且专业规范。

# 翻译文件的复用。
# 当一个界面所含的源词句与系统中已安装的翻译文件相同,
# 则即使这个界面不提供任何翻译文件,界面的相关词句仍然能显示已有的翻译词句。

默认执行脚本

在 uci-defaults 目录下可以放置脚本文件,用于在 ipk 安装完毕后自动执行命令,一般用于为软件预配置运行环境等,当然也可以不用它。

#!/bin/sh

# 表示重载 rpcd 脚本,用于重新读取 ACL 权限规则文件。
/etc/init.d/rpcd reload

# 表示执行完毕后退出此脚本。
exit 0

界面菜单

在 menu.d 目录下的 json 文件为软件的界面菜单文件,用于控制软件在系统菜单中的名称、顺序等。

下面用几个实例介绍 json 界面菜单文件的书写规范。

{
	"admin/services/samba4": {
//表示位于系统菜单的“服务”下。
		"title": "Network Shares-samba4",
//菜单标题,可以由翻译文件转换。
		"action": {
			"type": "view",
//类型view,表示默认的调用格式。
			"path": "samba4"
// JavaScript 脚本的路径,此处表示文件在 view 目录下,不用写后缀名。
		},
		"depends": {
//关联依赖项。
			"acl": [ "luci-app-samba4" ],
//权限控制文件的名称,不用写后缀名。
			"uci": { "samba4": true }
//表示关联 uci 配置文件 samba4,当配置文件存在才能显示界面菜单。(/etc/config/samba4)
		}
	}
}
{
	"admin/network/sqm": {
//表示位于系统菜单的“网络”下。
		"title": "SQM QoS",

		"order": 59,//菜单排序,数字越大排序越靠后,如果与其它界面数值相同,则以字母顺序分先后。
		"action": {
			"type": "view",
			"path": "network/sqm"

// JavaScript 脚本的路径,此处表示文件在 view/network 目录下。
		},
		"depends": {
			"acl": [ "luci-app-sqm" ]
		}
	}
}
{
	"admin/troubleshooting": {
//表示一级菜单。
		"title": "Troubleshooting",
		"order": 80,
		"action": {
			"type": "firstchild"
//表示此菜单不存在则自动创建。
		}
	},
	"admin/troubleshooting/packet_capture": {
//表示位于系统菜单的“troubleshooting”下。
		"title": "Packet Capture",
		"order": 1,
		"action": {
			"type": "view",
			"path": "packet_capture/tcpdump"
		},
		"depends" : {
			"acl": [ "luci-app-packet-capture" ],
			"uci": { "packet_capture": true },
			"fs": { "/usr/libexec/packet_capture": "executable",
//表示需要读取到这些文件存在且可执行。
				"/usr/libexec/packet_capture_start": "executable",
				"/usr/libexec/packet_capture_stop": "executable"
			}
		}
	}
}

权限控制文件

OpenWrt 为 JavaScript 脚本界面引入的权限控制机制,用于管制脚本界面能执行哪些系统操作。

{
	"luci-app-samba4": {
//这个权限控制文件的名称。
		"description": "Grant access to LuCI app samba4",
//描述,可由翻译文件转换。
		"read": {
//允许读取操作。
			"file": {
//表示关联对象为文件
				"/etc/samba/smb.conf.template": [ "read" ],
//表示允许读取这个文件。
				"/usr/sbin/smbd": [ "exec" ]
//表示允许执行此文件,只能读取数据。
			},
			"uci": [ "samba4" ]
//表示允许读取uci配置文件 samba4(/etc/config/samba4)
		},
		"write": {
//允许写入操作。
			"file": {
				"/etc/samba/smb.conf.template": [ "write" ]
//允许写入这个文件。
			},
			"uci": [ "samba4" ]
//表示允许写入uci配置文件 samba4
		}
	}
}
{
	"luci-app-sqm": {
		"description": "Grant UCI access for luci-app-sqm",
		"read": {
			"file": {
				"/var/run/sqm/available_qdiscs": [ "list" ],
//表示在这个目录下执行 list 命令。
				"/usr/lib/sqm/*.qos.help": [ "read" ]
			},
			"uci": [ "sqm" ],
			"ubus": {
//表示允许 ubus 命令调用
				"file": [ "read", "list" ],
//允许 file 项目调用 read 和 list 命令。
				"luci": [ "setInitAction" ]
//允许界面调用 setInitAction
			}
		},
		"write": {
			"uci": [ "sqm" ]
		}
	}
}
{
	"luci-app-packet-capture": {
		"description": "Grant access to tcpdump ubus object",
		"read": {
			"cgi-io": [ "download", "exec" ],
//允许执行的 cgi-io 操作。
			"ubus": {
				"tcpdump": [ "*" ],
//允许通过 ubus 调用 tcpdump 执行任意命令。
				"luci": [ "getProcessList" ]
//允许获取 luci 流程列表。
			},
			"uci": [ "packet_capture", "system" ],
			"file": {
				"/tmp/capture.pcap": [ "read" ]
			}
		},
		"write": {
			"uci": [ "packet_capture" ],
			"file": {
				"/usr/libexec/packet_capture_start": [ "exec" ],
//允许执行此文件,并可写入数据。
				"/usr/libexec/packet_capture_stop": [ "exec" ],
				"/usr/libexec/packet_capture": [ "exec" ],
				"/tmp/capture.pcap": [ "write" ]
//允许写入此文件。
			}
		}
	}
}

界面调试技巧

开发软件界面的时候,经常需要查看界面改动后的实际效果,难道必须要编译为 ipk 文件再安装吗?并不是,通过上面的学习,你应该了解到界面文件其实都是纯文本状态,编译过程只是压缩打包而已,并不是为了编译成二进制文件,所以只需要把文件放入对应的系统目录,然后刷新浏览器页面,或者清空浏览器缓存即可。

# 以 samba4 为例,将相关文件放入对应的系统目录即可。

# JavaScript 脚本界面
/www/luci-static/resources/view/samba4.js

# UCI 配置文件
/etc/config/samba4

# 界面菜单文件
/usr/share/luci/menu.d/luci-app-samba4.json

# 权限控制文件
/usr/share/rpcd/acl.d/luci-app-samba4.json

# 无法直接显示 po 翻译文件,需要转换为 lmo
/usr/lib/lua/luci/i18n/samba4.zh-cn.lmo

OpenWrt Procd 系统初始化和守护程序管理

在 OpenWrt 上要让一个程序正常运行,自然也需要一个启动脚本来提供服务,不然就只能使用命令行操控了,OpenWrt 下提供了一款类似于 systemd 的进程管理守护程序,称之为 Procd,其提供了非常强大的脚本功能,对于程序运行大有助益。

OpenWrt官网关于 Procd脚本的说明:OpenWrt Project: procd init script parameters

以程序 memcached 为例,演示传统启动脚本和 Procd 脚本的区别。

#!/bin/sh /etc/rc.common
# Copyright (C) 2010-2011 OpenWrt.org
# 这是传统启动脚本实例。

# 开机自启动时的顺序,1-99,数字越大,启动越晚。
START=80

start_instance () {
	local section="$1"

	config_get user "$section" 'user'
 # 获取配置文件参数
	config_get maxconn "$section" 'maxconn'
	config_get listen "$section" 'listen'
	config_get port "$section" 'port'
	config_get memory "$section" 'memory'
	config_get options "$section" 'options'

	service_start /usr/bin/memcached -d -u $user \
		-c $maxconn -l $listen \
		-p $port -m $memory $options
 # 启动参数
}

start() {
	config_load 'memcached'
	config_foreach start_instance 'memcached'
}

stop() {
	service_stop /usr/bin/memcached
}
#!/bin/sh /etc/rc.common
# Copyright (C) 2010-2011 OpenWrt.org
# 这是 Procd 脚本的实例。

START=80
STOP=10

# 使用 PROCD 的标识,必需。
USE_PROCD=1

PROG=/usr/bin/memcached

start_instance () {
	local section="$1"

	config_get user "$section" 'user' 
# 获取配置文件参数
	config_get maxconn "$section" 'maxconn'
	config_get listen "$section" 'listen'
	config_get port "$section" 'port'
	config_get memory "$section" 'memory'
	config_get options "$section" 'options'

	config_get_bool enable "$section" enable 0
	[ "$enable" -gt 0 ] || return 1
 # 获取 UCI 配置文件中的 enable 参数,以判断是否应该启动。

	procd_open_instance
	procd_set_param command "$PROG"
 # 程序可执行文件路径。
	procd_append_param command -u $user
 # 附加参数
	procd_append_param command -c $maxconn
	procd_append_param command -l $listen
	procd_append_param command -p $port
	procd_append_param command -m $memory
	procd_append_param command $options
	procd_set_param respawn
 # 守护进程,当进程出现故障未响应时自动重载。
	procd_set_param stdout 1
 # 将输出信息转发至系统日志。
	procd_set_param stderr 1
 # 将错误日志转发至系统日志。
	procd_close_instance
}

start_service() {
	config_load 'memcached'
	config_foreach start_instance 'memcached'
}

service_triggers() {
	procd_add_reload_trigger "memcached"
 # 监控 UCI 配置文件,当文件发生变化时自动重载程序。
 # 例如通过界面勾选启用,再点击保存并应用后,则程序就能自动运行。
}

搭建 OpenWrt 本地软件源

开发软件过程中,免不了要经常进行实机测试,难道要搭建一个 HTTP 服务器来提供安装源吗?并不需要,经验丰富的开发者应该已经想到了,那就是直接使用 OpenWrt 自带的 uhttpd 来作为 HTTP 服务器,并不需要任何额外设置,只需要将软件源目录软链接至 www 目录即可。

首先通过 FTP 或其它各种方式,将软件源目录上传至路由器磁盘空间。

然后使用软链接命令,将软件源目录链接至 www 目录即可。

ln -s /mnt/sda3/soft /www/soft

此时可以打开浏览器测试,看是否能成功访问。

接着再修改软件包的 “OPKG 配置” 里的软件源地址。

然后就可以开始使用了。