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

LUCI界面

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

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

完整的界面文件结构

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

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 <admin@ikghx.com>
# 软件许可信息,可选
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 脚本界面的运行规则了,下面介绍一些常用的其它脚本规则。

获取网络接口的选项

'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 = 'uinteger';//只允许输入正整数。
o.datatype = 'range(-20,19)';//只允许输入范围内的数值,(只允许输入-20至19的数字)
o.datatype = 'list(string)';//限制此选项的格式为列表值,一般用于配合动态列表。
o.datatype = 'directory';//只允许输入路径格式。
o.datatype = 'string';//允许任意字符组合。(此为默认值,即如果不指定数据类型就默认是这个。)
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 = 'max(1024)';//限制参数最大值。


界面语言翻译文件

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

#
# OpenWrt-Life <admin@ikghx.com>, 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 安装完毕后自动执行命令,当然也可以不用它,目前建议至少用于重载 rpcd 服务,以便新安装的界面能顺利显示,否则就必须关闭浏览器重新登录才能显示新安装的界面菜单。

#!/bin/sh

/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 配置” 里的软件源地址。

然后就可以开始使用了。



文章来源:https://iyzm.net/openwrt/624.html

本文章由作者:佐须之男 整理编辑,原文地址: OpenWrt达人教程之 LuCI2开发入门指南
本站的文章和资源来自互联网或者站长的原创,按照 CC BY -NC -SA 3.0 CN协议发布和共享,转载或引用本站文章应遵循相同协议。如果有侵犯版权的资 源请尽快联系站长,我们会在24h内删除有争议的资源。欢迎大家多多交流,期待共同学习进步。

相关推荐