OpenWrt Luci编写小技巧

技巧一:在luci页面中执行shell命令

方法一:获得标准输出流

luci.sys.exec("命令")

然后可以声明一个变量将标准输出内容保存起来,如下

local str = luci.sys.exec("netstat -nlp")

方法二:获得错误输出流

luci.sys.call("命令")
同方法一用法一样,区别在于用它可以获得错误输出流


技巧二:获取UCI记录

可以直接通过技巧一执行shell命令来达到目的,如下例获取路由器LAN口的IP地址

local router_ip = luci.sys.exec("uci get network.lan.ipaddr")
顺便再提一下uci shell命令的写法,如上面的例子就是获取 /etc/config/network 文件下面 option名为lan 下面的ipaddr属性,UCI配置应为如下格式

config interface 'loopback'
	option ifname 'lo'
	option proto 'static'
	option ipaddr '127.0.0.1'
	option netmask '255.0.0.0'

config globals 'globals'
	option ula_prefix 'fd25:5be0:231f::/48'

config interface 'lan'
	option ifname 'eth0.1'
	option force_link '1'
	option type 'bridge'
	option proto 'static'
	option ipaddr '192.168.1.1'
	option netmask '255.255.255.0'
	option ip6assign '60'
	option macaddr '00:88:a1:e4:f1:a3'

config interface 'wan'
	option ifname 'eth0.2'
	option proto 'dhcp'
	option macaddr '00:88:a1:e4:f1:a8'


但是对于这样的UCI文件需要加上下标才能获得到结果

config ssr
	option gfwlist 'china-banned'
	option safe_dns_tcp '0'
	option enabled '0'
	option server '1.2.3.4'
	option server_port '443'
	option password 'Alex666666'
	option method 'rc4-md5'
	option protocol 'origin'
	option obfs 'plain'

可以注意到 network的config行是两个参数,而上面这种格式只有一个参数,可以配置多个不同的config,所以要加上下标才能访问,如下

local vt_server_addr=`uci get ssr.@ssr[0].server`

同理我们可以获取列表型UCI记录的数据,如下

config main_server
	option server_weight '10'
	option server_ip '1.2.3.4'
	option server_port '443'
	option server_name 'JP1'

config backup_server
	option server_name 'JP2'
	option server_ip '2.2.2.2'
	option server_port '8038'

config backup_server
	option server_name 'JP3'
	option server_ip '3.3.3.3'
	option server_port '443'

config backup_server
	option server_name 'JP4'
	option server_ip '4.4.4.4'
	option server_port '443'
那么我们获得第二个备用服务器的ip就可以通过修改下标实现

local bk_server_2='uci get haproxy.@backup_server[1].server_ip'

 

技巧三:字符串拼接

在luci中字符串拼接不能使用C语言里的+或者%s,而要使用 字符串..变量..字符串 的形式

比如我从UCI记录中查询到了路由器内网的IP,我将这个IP保存为一个变量,然后我想把它显示在luci界面上,可以如下书写


local router_ip = luci.sys.exec("uci get network.lan.ipaddr")

m=Map("haproxy",translate("HAProxy"),translate("HAProxy能够检测SS服务器的联通情况,从而实现负载均衡和高可用的功能") .. "

后台监控页面:" .. router_ip)



技巧四:判断某个服务是否在运行状态

方法一:通过查询服务名的pid,捕获错误输出来判断。原理就是根据服务的名字查询这个服务的pid,如果查不到就会输出错误流,否则输出标准流,可以通过检测是否有错误流输出判断该程序是否已经运行

local ssr_redir_on = (luci.sys.call("pidof ssr-redir > /dev/null") == 0)
if ssr_redir_on then	
	...
else
	...
end

方法二:查询相关端口是否被监听,适用于监听端口的程序,与方法一相反,这次是通过捕捉标准流输出是否有内容判断服务是否开启,如果想同时判断端口和服务名,只需执行两次grep即可,一次端口号,一次服务名

local red_on = string.len(luci.sys.exec("netstat -nlp | grep " .. listen_port))>0
if red_on then	
	...
else
	...
end


技巧五:通过HTML修改提示文字的颜色

我们经常需要在translate中填写菜单的名字或者提示语,可以通过普通的HTML标签来添加超链接,换行,字体颜色大小等等,如下


local state_msg = "" .. translate("Running") .. ""

m=Map("redsocks2",translate("Redsocks2 - General Settings"),
translatef("A modified version of redsocks.Beside the basic function of redsocks,it can redirect TCP connections which are blocked via proxy automatically without a blacklist.") .. "

状态 - " .. state_msg)


技巧六:在输入框的下面添加提示文字

OpenWrt Luci编写小技巧_第1张图片

只需要在声明空间的时候在第四个参数上使用translate就行了,如下

m=Map("redsocks2",translate("Redsocks2 - General Settings"),
translatef("A modified version of redsocks.Beside the basic function of redsocks,it can redirect TCP connections which are blocked via proxy automatically without a blacklist.") .. "

状态 - " .. state_msg) s=m:section(TypedSection,"redsocks2_redirect",translate("Redirector Settings")) o=s:option(Flag,"autoproxy",translate("Enable Auto Proxy"),translate("不推荐开启"))


技巧七:提交前自动检查格式规范

检查格式规范主要靠datatype属性,只要设置好就可以了如下

数字格式

o=s:option(Value,"dest_port",translate("Destination Port"))
o.datatype="uinteger"
o.placeholder = "53"

IP v4格式

o.datatype="ip4addr"

还有更多格式等待你去探索



技巧八:给输入框设置默认值

OpenWrt Luci编写小技巧_第2张图片

方式一:给输入框设置实体的默认值(可以直接用光标修改的默认值文字,上图中的“目标IP”)

首先数字型输入框可以这样书写,使用default属性

o=s:option(Value,"timeout",translate("Timeout"))
o.datatype="uinteger"
o.default=10

文本型

o=s:option(Value,"interface",translate("Outgoing interface"),translate("Outgoing interface for redsocks2."))
o.default='eth0.2'
布尔值型

o=s:option(Flag,"in_ttl",translate("修改入路由的TTL自动加1"))
o.rmempty=false


方式二:给输入框设置虚默认值(当输入框为空时显示的半透明文字,上图中的灰色字)

这种需要使用placeholder属性,单数注意第一种方式在“保存&应用”时会将默认值写入uci,但是这种方式却不会

数字型:

o=s:option(Value,"dest_port",translate("Destination Port"))
o.datatype="uinteger"
o.placeholder = "53"

文本型:

o=s:option(Value,"dest_ip2",translate("备用DNS服务器IP"))
o.datatype="ip4addr"
o.placeholder = "8.8.4.4"

由于使用placeholder不会将默认值写入uci,所以我们在读取uci时不要忘了添加非空判断,一般写法如下

local vt_server_addr=`uci get ssr.@ssr[0].server`
[ -z $vt_server_addr] && vt_server_addr="127.0.0.1"

技巧九:通过下拉框控制其他控件的显示和隐藏

下拉框里里面的不同选项往往代表着不同的类别,而不同的类别所需的控件往往也是不一样的,所以需要让一部分控件通过下拉框的内容来控制自身是否显示,效果如下图


OpenWrt Luci编写小技巧_第3张图片  OpenWrt Luci编写小技巧_第4张图片

在上面两张图中,通过选择“代理服务器类型”这个下拉菜单就可以控制下面许多控件的显示和隐藏,主要起作用的属性是depends:

首先是个很普通的下拉选择框

o=s:option(ListValue,"proxy_type",translate("Proxy Server Type"))
o:value("s",translate("Shadowsocks"))
o:value("socks5",translate("Socks5"))
o:value("direct",translate("转发流量至VPN"))
o:value("campus_router",translate("破解路由器限制"))

下面是被选择性隐藏的控件


o=s:option(Flag,"udp_relay",translate("启用UDP转发"),translate("注意不能与HAProxy配合使用"))
o:depends({proxy_type="s"})
o=s:option(Value,"username",translate("Username"),translate("Leave empty if your proxy server doesn't need authentication."))
o:depends({proxy_type="socks5"})
o=s:option(Value,"password",translate("Password"))
o:depends({proxy_type="s"})
o:depends({proxy_type="socks5"})
o.password=true
o=s:option(Value,"interface",translate("Outgoing interface"),translate("Outgoing interface for redsocks2."))
o:depends({proxy_type="direct"})
o:depends({proxy_type="campus_router"})
o.rmempty='eth0.2'
o=s:option(Flag,"out_ttl",translate("修改出路由的TTL为64"))
o:depends({proxy_type="campus_router"})
o.rmempty=false
o=s:option(Flag,"in_ttl",translate("修改入路由的TTL自动加1"))
o:depends({proxy_type="campus_router"})
o.rmempty=false
o=s:option(Flag,"autoproxy",translate("Enable Auto Proxy"),translate("不推荐开启"))
o.rmempty=false
o:depends({proxy_type="s"})
o:depends({proxy_type="socks5"})
o:depends({proxy_type="direct"})
o=s:option(Value,"timeout",translate("Timeout"))
o:depends({autoproxy=1})
o.datatype="uinteger"
o.rmempty=10

从上面的代码可以看出,下方的控件可以通过判断上方控件的值来控制自己是否显示,而且depends可以写不止一行,可以锚定多个下拉框的选项


技巧十:“更多选项”功能

如下图,勾选“更多选项”后在下面出现了很多控件

OpenWrt Luci编写小技巧_第5张图片

 展开后

OpenWrt Luci编写小技巧_第6张图片


同技巧九其实完全一样,都是靠depends这个属性,只不过值得判断变成了1和0

“显示更多”的单选框是个普通单选框

s:option(Flag, "more", translate("More Options"),
	translate("Options for advanced users"))

被隐藏的控件如下

safe_dns = s:option(Value, "safe_dns", translate("Safe DNS"),
	translate("推荐使用OpenDNS"))
safe_dns.datatype = "ip4addr"
safe_dns.optional = false
safe_dns.placeholder = "208.67.220.220"
safe_dns:depends("more", "1")

safe_dns_port = s:option(Value, "safe_dns_port", translate("Safe DNS Port"),
	translate("Foreign DNS on UDP port 53 might be polluted"))
safe_dns_port.datatype = "range(1,65535)"
safe_dns_port.placeholder = "443"
safe_dns_port.optional = false
safe_dns_port:depends("more", "1")

“1”代表单选框被勾上,“0”代表没有勾上


技巧十一:按钮控件

OpenWrt Luci编写小技巧_第7张图片

有时候经常需要在页面上放置几个按钮,点击时候可以执行一段shell脚本,写法如下

m = Map("gfwlist", translate("Domain Lists Settings"),translate("xxxx"))
s = m:section(TypedSection, "params", translate("Settings"))

button_update_gfwlist = s:option (Button, "_button_update_gfwlist", translate("更新GFWList")) 
local gfw_count = luci.sys.exec("grep -c '' /etc/gfwlist/china-banned")
button_update_gfwlist.inputtitle = translate ( "当前规则数目" .. gfw_count .. ",点击更新")
button_update_gfwlist.inputstyle = "apply" 
function button_update_gfwlist.write (self, section, value)
	luci.sys.call ( "/etc/update_gfwlist.sh > /dev/null")
end 

其中 grep -c '' 文件路径 的作用是返回一个文件的行数

/etc/update_gfwlist.sh就是我们要执行的shell脚本

在这里要特别注意,调用脚本之后,由于脚本内可能会有echo等语句导致执行脚本会有输出,而有输出的话会导致当前luci页面崩溃,所以我们必须把所有的输出去掉,方法就是 > /dev/null

该按钮在点击之后会呈现和点击了“保存&应用”一样的效果


技巧十二:多标签页

对于功能比较丰富的软件单个配置页面可能并不够用,需要多个页面来配置不同的业务,效果如下图

OpenWrt Luci编写小技巧_第8张图片

在处理这种配置页时,首先需要把所有页面的model的cbi放到同一个文件夹下,比如我有两个页面page1.lua和page2.lua,那么他们两个都应该放到/usr/lib/lua/luci/model/cbi/项目名/ 目录下,然后修改/usr/lib/lua/luci/controller/项目名.lua 如下

module("luci.controller.项目名", package.seeall)
function index()
		if not nixio.fs.access("/etc/config/项目名") then
		return
	end
	entry({"admin", "services", "项目名"},alias("admin", "services", "项目名","page1"),_("项目名")).dependent = true
	entry({"admin", "services", "项目名","page1"}, cbi("项目名/page1"),_("标签1标题"),10).leaf = true
	entry({"admin", "services", "项目名","page2"}, cbi("项目名/page2"),_("标签2标题"),20).leaf = true
end

其中alias表示的是该项目进去之后默认应该显示哪一页,dependent = true表示该行的model是一个独立的菜单,而 .leaf = true则表示该行的model是一个子页,依附于上面 dependent的model,而10,20这两个参数代表不通子页的左右位置,该数值越大越靠右。


技巧十三:HTML混合页面

如下图展示的就是一个HTML和lua混合开发后的页面,这种混合页面往往具有布局更为灵活,样式更加美观,并且可以自由使用背景和主题的优势。是非常提倡的一种luci开发方式。

OpenWrt Luci编写小技巧_第9张图片

下面把上面案例的model代码贴出来,其代码放置在/usr/lib/lua/luci/view下,是一个htm文件

<%-
    local sys  = require "luci.sys"
    local fs = require "nixio.fs"
    local uci = require "luci.model.uci".cursor()
    local ipkg = require "luci.model.ipkg"

    local kcptun = "kcptun"
    local enable_server = uci:get_first(kcptun, "general", "enable_server") == "1"
    local enable_monitor = uci:get_first(kcptun, "general", "enable_monitor") == "1"
    local enable_logging = uci:get_first(kcptun, "general", "enable_logging") == "1"
    local client_file = (uci:get_first("kcptun", "general", "client_file") or ""):trim()
    local package_info = ipkg.info("luci-app-kcptun") or ""

    local client_version, server_version

    if client_file ~= "" and fs.access(client_file, "rwx", "rx", "rx") then
        client_version = sys.exec(client_file .. " -v | awk '{printf $3}'")
    end
    if not client_version or client_version == "" then
        client_version = translate("Unknown")
    end

    if enable_server then
        local server_file = (uci:get_first("kcptun", "general", "server_file") or ""):trim()

        if server_file ~= "" and fs.access(server_file, "rwx", "rx", "rx") then
            server_version = sys.exec(server_file .. " -v | awk '{printf $3}'")
        end
        if not server_version or server_version == "" then
            server_version = translate("Unknown")
        end
    end

-%>



<%:Running Status%>
<% if enable_server then -%> <% end -%> <% if package_info ~= "" then -%> <% end -%>
<%:Client Version%><%=pcdata(client_version)%>
<%:Client Status%><%:Collecting data...%>
<%:Server Version%><%=pcdata(server_version)%>
<%:Server Status%><%:Collecting data...%>
<%:Luci Version%><%=pcdata(package_info["luci-app-kcptun"]["Version"])%>
<%:Luci Installation Time%><%=pcdata(os.date('%Y-%m-%d %H:%M:%S', package_info["luci-app-kcptun"]["Installed-Time"]))%>
<%:Author%>Index
<%:Blog%>https://blog.kuoruan.com
<% if enable_logging then -%>
<%:Client Log%>
<% if enable_server then -%>
<%:Server Log%>
<% end -%> <% if enable_monitor then -%>
<%:Monitor Log%>
<%- end -%> <%- end %>
仔细观察一下发现这种lua+html的混可页面其实很像PHP或者JSP页面,同样是页面主体结构使用HTML标签,然后使用<% lua代码 -%>的形式获取动态数据通过服务器程序填充到HTML页面中达到动态的效果,可以方便的把JS代码混合lua代码,shell命令进行开发,非常方便灵活。关于调用,我们在源model的cbi中按照如下填写

local uci = require "luci.model.uci".cursor()
local sys = require "luci.sys"
local fs = require "nixio.fs"

local m, s, o
local kcptun = "kcptun"

m = SimpleForm(kcptun, "%s - %s" %{translate("Kcptun"), translate("Overview")})
m:append(Template("kcptun/overview"))
m.reset = false
m.submit = false

return m

上面最关键的就是 m:append(Template(".htm文件位置"))这一句了。通过这一句就可以自由加载不同的html文件,其中这个文件位置是相对于/usr/lib/lua/luci/view目录来说的

技巧十四:将中文翻译打包进luci的ipk安装包中

OpenWrt luci的翻译模块使用的是i18n系统,用于翻译的文件位于/usr/lib/lua/luci/i18n/ 目录下所有的.lmo文件,问题是这个lmo文件是编译后的二进制文件而不是文本文件,所以没法像其他lua文件一样可以直接修改其源码。只能在编译ipk包之前进行修改并编译打包。

lmo文件的源是lo文件,lo文件的写法一般是这样的

msgid "Autoexpire, Default unit is seconds."
msgstr "自动重连间隔时间, 默认单位: 秒"

msgid "Blog"
msgstr "博客"

msgid "Check Kcptun process per minute."
msgstr "每分钟检查 Kcptun 进程"

msgid "Clear Client Log"
msgstr "清理客户端日志"

msgid "Clear Monitor Log"
msgstr "清理监控日志"

即msgis后面跟的是model页面中translate("")中的原版内容

msgstr是根据相关语言翻译后的内容。

一般lmo文件的文件名会是项目名.zh-cn.lmo这种形式,其中中间的这个.zh-cn就是代表着这个lmo将会在系统设置为何种语言时被加载,zh-cn就是汉语。

但是如何把.lo格式的文本文件编译成.lmo的二进制文件呢,又如何打包进ipk安装包并在安装时安装到OP系统中去呢,关键就在于Makefile文件

下面以SDK方式为例介绍如何编译翻译

首先把我们的lo源码找个位置放好,我这里选择 SDK/package/项目目录/i18n/zh-cn/ 

放好之后,我们需要一个编译工具来把lo编译成lmo,而这个编译工具源码github已经有人提供了,我们直接拷贝下来编译安装到系统中去即可,下载地址:https://github.com/AlexZhuo/luci-app-kcptun/tree/master/tools/po2lmo

有了这个编译工具之后,我们就可以在makefile中控制现编译,然后打包到ipk包中,最后安装到系统上,makefile的写法如下

首先prepare阶段加下面一句话

define Build/Prepare
	$(foreach po,$(wildcard ${CURDIR}/i18n/zh-cn/*.po), \
		po2lmo $(po) $(PKG_BUILD_DIR)/$(patsubst %.po,%.lmo,$(notdir $(po)));)
endef
上面的意思就是把/i18n/zh-cn/下面所有的.po文件都按照源文件名编译成* .lmo

然后在install阶段添加这样的话

define Package/$(PKG_NAME)/install
	$(INSTALL_DIR) $(1)/usr/lib/lua/luci/i18n
	$(INSTALL_DATA) $(PKG_BUILD_DIR)/*.lmo $(1)/usr/lib/lua/luci/i18n/
endef
意思就是把刚刚编译好的lmo文件全部安装到/usr/lib/lua/luci/i18n下面去,这样就完成了翻译文件的安装。

除了直接把翻译文件.lmo直接打包进luci的ipk内部之外,还可以制作单独的翻译ipk包减小luci包的体积

技巧十五:快速选择局域网客户端IP

在做访问控制的时候需要按照连如路由器设备的IP部署相应的访问控制规则,而如果让用户手动输入IP或者MAC不仅非常繁琐而且极容易出错,最好的办法是为用户提供一个下拉列表,用户可以直接通过点选的方式选择需要配置的设备IP和MAC。实现其来非常容易,luci代码如下

OpenWrt Luci编写小技巧_第10张图片

-- [[ LAN Hosts ]]--
s = m:section(TypedSection, "lan_hosts", translate("LAN Hosts"))
s.template = "cbi/tblsection"
s.addremove = true
s.anonymous = true

o = s:option(Value, "host", translate("Host"))
luci.sys.net.arptable(function(x)
	o:value(x["IP address"], "%s (%s)" %{x["IP address"], x["HW address"]})
end)
o.datatype = "ip4addr"
o.rmempty = false

o = s:option(ListValue, "type", translate("Proxy Type"))
o:value("direct", translate("Direct (No Proxy)"))
o:value("normal", translate("Normal"))
o:value("gfwlist", translate("GFW-List based auto-proxy"))
o.rmempty = false

o = s:option(Flag, "enable", translate("Enable"))
o.default = "1"
o.rmempty = false




你可能感兴趣的:(OpenWrt Luci编写小技巧)