宝塔管理服务器比较方便,管理网站和数据库,安装各种插件,比较灵活。如果能用宝塔安装云SRS,那可以在很多云和虚拟机都可能安装了。
说干就干,先去下载资料,宝塔插件的官方手册和官方论坛,还有官方Demo,可以直接下载下来后在你的宝塔中安装。
我跟着操作了一遍,发现还是有些地方没有说清楚,而官方Demo比较复杂,写得比较隐晦,就重新写了个教程。
Note: 目前插件开发,需要企业认证,认证通过后,才能开发第三方应用插件。
Develop Mode
记得一定要打开开发者模式,否则修改代码不生效:
Note: 开启开发者模式后,比如修改srs_cloud_main.py或者index.html后,会立刻生效,不用清理缓存或重启服务。
开启开发者模式后,我们就可以开始开发插件了。
HelloWorld
我们先做一个最简单的插件,只实现安装和卸载,请参考bt-plugin-example-install,下面是详细的解读。
宝塔插件的安装如下,假设插件为srs_cloud,请替换成你的插件名称:
- 安装时,会调用install.sh脚本,参数是install,需要定义如何安装。
- ZIP文件会解压到/www/server/panel/plugin/srs_cloud插件目录,调用脚本时文件已经存在了,可以使用插件自己的这些文件。
- 会获取info.json的信息,关于插件的描述信息。
- 卸载时,会调用install.sh脚本,参数是uninstall,需要定义如何卸载。
我们写一个简单的插件,目录为srs_cloud。插件描述信息info.json文件,内容如下:
{
"title": "SRS音视频服务器",
"name": "srs_cloud",
"ps": "直播和WebRTC音视频能力,支持RTMP、WebRTC、HLS、HTTP- FLV和SRT等常用协议",
"versions": "4.4",
"checks": "/www/server/panel/plugin/srs_cloud",
"author": "Winlin",
"home": "https://github.com/ossrs/srs"
}
然后是install.sh脚本,安装只打印一条消息并等待3秒,卸载时直接删除插件目录:
#!/bin/bash
install_path=/www/server/panel/plugin/srs_cloud
Install() {
echo 'Installing'; sleep 2;
echo 'Install OK'; sleep 2;
}
Uninstall() {
rm -rf $install_path
}
if [ "${1}" == 'install' ];then
Install
elif [ "${1}" == 'uninstall' ];then
Uninstall
else
echo 'Error!'; exit 1;
fi
将目录srs_cloud打包成srs_cloud.zip,可以在第三方应用 > 导入插件,上传压缩包安装。安装过程会显示消息:
Note: 安装后,可以直接scp本地文件到服务器的插件目录,这样不用每次上传压缩包,直接刷新页面就可以看到效果,开发调试的周期很短。
安装后,就可以看到这个插件,可以卸载,如下图所示:
安装后,如果点设置发现会有错误提示,接下来我们开发插件的设置和配置面板。
Settings
接下来我们实现插件的设置面板,只显示一个简单的信息,请参考bt-plugin-example-settings,下面是详细的解读。
用户点击插件的设置按钮,或者点插件名称,会打开插件设置面板。定义如下:
- 需要index.html页面,默认的模板页面。
我们先实现一个简单的UI,新增文件index.html我们先写一个非常简单的插件(更多样式可以参考demo插件),只显示一个导航和内容:
<div class="bt-form">
<div class="bt-w-main">
<div class="bt-w-menu"><p class="bgw">概览</p></div>
<div class="bt-w-con pd15"><div class="plugin_body"></div></div>
</div>
</div>
<script type="text/javascript">
$('.plugin_body').html("<h1>Hello World!</h1>");
</script>
这样用户点击就会打开一个页面:
这是最简单的插件页面了,只是静态页面而已。我们接下来,添加后端API。
Backend Server
接下来我们实现插件的后端服务,前端调用后端服务并展示结果,请参考bt-plugin-example-backend,下面是详细的解读。
我们一般需要页面从后端服务请求数据,比如查看我们的服务器是否正常等。定义如下:
- 需要srs_cloud_main.py,每个函数就是API。
新建文件srs_cloud_main.py,新增一个简单的helloworld函数,它的代码如下:
#!/usr/bin/python
# coding: utf-8
import sys, os, json
os.chdir("/www/server/panel")
sys.path.append("class/")
import public
class srs_cloud_main:
def __init__(self):
pass
def helloworld(self, args):
return public.returnMsg(True, "Hello API World!")
关于后端API,请注意:
- 必须返回固定的格式,我们使用public.returnMsg函数返回,一个是status,一个是msg两个字段,是不能随便返回的,会报错。
- 我们这里没有传参数,args为空,注意不能传action、s、name、a等参数,会报错。
现在我们有了后端API,在前端index.html中就可以调用这个API了,同样也必须是固定的格式,我们修改index.html代码如下:
<div class="bt-form">
<div class="bt-w-main">
<div class="bt-w-menu"><p class="bgw">概览</p></div>
<div class="bt-w-con pd15"><div class="plugin_body"></div></div>
</div>
</div>
<script type="text/javascript">
function plugin_request(plugin_name, function_name, args, callback, timeout) {
$.ajax({
type:'POST',
url: '/plugin?action=a&s=' + function_name + '&name=' + plugin_name,
data: args,
timeout:timeout || 3600 * 1000,
success: function(res) {
if (!callback) return layer.msg(res.msg, { icon: res.status ? 1 : 2 });
return callback(res);
}
});
}
plugin_request('srs_cloud', 'helloworld', { p: 100}, function (res) {
$('.plugin_body').html(`<pre>${JSON.stringify(res, null, 2)}</pre>`);
});
</script>
打开插件的设置页面,就会调用这个API,从后端AP获取数据:
这样,我们就可以写非常复杂的功能了。接下来,我们传参数和解析响应值。
Request Args
接下来我们实现请求的参数,以及复杂的响应解析,请参考bt-plugin-example-args,下面是详细的解读。
我们先让msg变成一个JSON对象,修改srs_cloud_main.py如下:
def helloworld(self, args):
return public.returnMsg(True, json.dumps({'r0': 100, 'r1': 'Hello API World!'}))
同时,我们在前端收到响应时,再解析一次msg对象:
return callback({...res, msg: JSON.parse(res.msg)});
这样我们获取到的就是整个JSON对象了:
我们可以在args中传一些参数,比如我们加了born和category两个参数:
plugin_request('srs_cloud', 'helloworld', {born:2013, category:'media'}, function (res) {
$('.plugin_body').html(`<pre>${JSON.stringify(res, null, 2)}</pre>`);
});
同时修改后端的代码,读取这两个参数并返回:
def helloworld(self, args):
return public.returnMsg(True, json.dumps({
'r0': 100, 'r1': args.born, 'r2': args.category,
}))
运行结果如下:
注意:插件的后端API,不能随便传参数,有几个参数是不能传的,比如action、s、name、a,会出各种奇怪的错误,比如我们传个name:
plugin_request('srs_cloud', 'helloworld', { name: 'srs'}, function (res) {
$('.plugin_body').html(`<h3>${JSON.stringify(res)}</h3>`);
});
会发现调用错误,显示的错误是:
Traceback (most recent call last):
File "class/panelPlugin.py", line 2683, in a
p = Plugin(get.name)
File "class/pluginAuth.py", line 80, in panel.class.pluginAuth.Plugin.__init__
File "class/pluginAuth.py", line 229, in panel.class.pluginAuth.Plugin.__l1Ol11l0O001
File "class/public.py", line 3411, in get_plugin_main_object
return plugin_obj,is_php
UnboundLocalError: local variable 'plugin_obj' referenced before assignment
看起来是插件对象问题,实际上是name这个参数不能传。
后面就是如何定义更多的API,实现自己的复杂业务逻辑了。下面是一些其他话题,如果感兴趣也可以参考。
Request Promise
我们把plugin_request修改下,改成现代js的写法,用promise的方式实现:
async function srs_cloud_request(function_name, args, timeout) {
return new Promise((resolve, reject) => {
$.ajax({
type: 'POST',
url: `/plugin?action=a&s=${function_name}&name=srs_cloud`,
data: args,
timeout: timeout || 3600 * 1000,
success: function (res, status, xhr) {
return resolve(JSON.parse(res.msg));
},
error: reject,
});
});
}
后续我们用到这个函数时,都使用Promise方式的函数,会让整个逻辑非常简单。
Pycharm Development
宝塔定义了Python可以用到一些API,在/www/server/panel/class/public.py中,因此可以看到srs_cloud_main.py中导入这个文件:
os.chdir("/www/server/panel")
sys.path.append("class/")
import public
如果在本机开发,这些函数就会提示错误,所以我们可以把服务器的文件下载下来,配置Pycharm的开发环境,这样就更容易知道可以调用哪些API了。
我们在服务器上把整个宝塔都打包下载下来,然后在Pycharm的设置中,添加这个路径就可以:
我们在Pycharm中打开demo,可以看到public的函数都可以找到了:
这样开发效率是很高的。
Template
Demo中的demo_main.py,定义了一些函数,比如demo_main.index和demo_main.get_logs,然后他们的备注说是模板。
这个是指,可以在宝塔中打开对应插件的页面,这样会请求对应的API,并且绑定到对应的模板中。比如,安装demo插件后,打开:
http://your_server_ip:8888/demo/index.html
展示的就是一个由demo_main.index和templates/index.html渲染出来的页面:
注意官方手册中有个说明:
Note: 插件动态路由不继承面板会话权限,需开发者自己在插件(_check 方法)中做好访问权限的控制,请谨慎使用。
这个弄得很晕,其实并不是必要的,很多插件包括官方的插件,都没有用这种方式。
Config
在Demo中还有个config.json的操作函数,具体没看到怎么用法,猜测可能是有便捷的方式能让插件读取和写入配置吧。
Install
关于install.sh特别说明几点:
- 就算exit 1也会提示安装成功,所以不能依赖脚本来做复杂的任务,比如终止安装和回滚等。
- 如果要检测依赖,比如需要nginx,那可以在html和py中检测,加一个API,如果没有就提示用户安装。
- 依赖的文件一般都放在插件的压缩包中,除非是一些公共的比如git/make/docker等,可以直接安装了。
宝塔的管理逻辑是简化安装,重点是在设置页面再处理。
比如PostgreSQL管理器这个插件,就会在插件配置中的版本管理安装数据库。第一次打开它时,会提示没有数据库,要安装,这时候才去安装和管理不同的版本。如下图所示:
这样可以做比较复杂的安装和回滚逻辑,比如选择不同的版本安装。
JS API: Message
前端也有一些API可以使用,比如弹一个消息出来等等。
下面是一个例子,在index.html中写如下代码:
<div class="bt-form">
<div class="bt-w-main"></div>
</div>
<script type="text/javascript">
layer.msg('HelloWorld', {icon: 1});
</script>
这样会弹出消息,过几秒就会消失,如下图所示:
这个API的定义如下:
layer.msg(msg: String, {icon: Integer});
- msg: 弹出的消息内容。
- icon: 整形数字,定义如下:
- 0是警告,
- 1是成功消息,
- 2是失败消息,
- 3是黄色问号,
- 4一把灰色的锁,
- 5是红色哭脸,
- 6是绿色笑脸,
- 7是黄色感叹号,
- 16是转圈圈的图标
例如,弹出成功消息:
layer.msg('HelloWorld', {icon: 1});
有时候需要弹窗确认,还有个弹窗的API可用。
JS API: Dialog
比如我们要用户确认操作,可以弹一个确认窗口出来。
下面是一个例子,在index.html中写如下代码:
<div class="bt-form">
<div class="bt-w-main"></div>
</div>
<script type="text/javascript">
const confirm = layer.open({
icon: 0,
closeBtn: 2,
title: '提示',
area: '400px',
btn: ['确定', '取消'],
content: '<div class="bt_conter bt-form">是否继续操作?</div>',
yes: function (index, layero) {
layer.close(confirm);
}
});
</script>
这样就会弹出一个确认窗口,如下图所示:
这个API的定义如下:
layer.open({
icon: Integer, closeBtn: Integer, title: String, area: String,
btn: [String], Content: String, yes: Function,
});
- icon: 具体定义参考JS API: Message中的详细说明。
- btn: 按钮数组,0是确认,1是取消,如果为空则不显示任何按钮。
- closeBtn: 0不显示关闭按钮,2显示关闭按钮。
有时候我们会依赖其他的基础软件,比如NGINX。我们可以在后端py中检测依赖的软件路径,若不存在则返回错误给前端,前端提示用户安装。
JS API: Close
弹出消息Message和对话框Dialog都是可以关闭的,接口是:layer.close(id)或者layer.closeAll()。
比如,我们在调用接口前弹出一个消息框,然后接口返回后我们关闭提示:
const tips = layer.msg('检测服务状态', {icon: 16});
const services = await srs_cloud_request('serviceStatus');
console.log(`services status is ${JSON.stringify(services)}`);
layer.close(tips);
或者,我们如果要安装别的依赖软件,弹出来依赖的安装对话框,但是我们想把自己的安装窗口关闭掉,那么就可以关闭所有的窗口:
layer.closeAll();
await install_dependence();
这样就能避免在一个逻辑中做过多的判断了。
JS API: Software
宝塔提供了函数,可以安装特定的软件,比如NGINX,我们在前端index.html中写如下代码:
<div class="bt-form">
<div class="bt-w-main">
<div class="bt-w-con pd15">
<div class="plugin_body">
<a href="javascript:void(0);" class="btlink" onclick="bt.soft.install('nginx')">
安装NGINX
</a>
</div>
</div>
</div>
</div>
则会显示一个安装NGINX的链接,点击后会弹出来安装NGINX的对话框:
如果是强依赖NGINX,安装前不能点其他操作,可以配合前面的Confirm来弹出一个对话框,HTML如下:
<div class="bt-form">
<div class="bt-w-main"></div>
</div>
<script type="text/javascript">
const confirm = layer.open({
icon: 0,
closeBtn: 0,
title: '请安装依赖',
area: '400px',
btn: [],
content: `
<a href="javascript:void(0);" class="btlink" οnclick="bt.soft.install('nginx')">
安装NGINX
</a>
`,
yes: function (index, layero) {
layer.close(confirm);
}
});
</script>
我们配置了Confirm没有取消和关闭,效果如下:
这样用户做其他操作前,必须先安装依赖。
Example: Logging
有时候我们需要起一个任务,然后显示日志,并且会不断滚动刷新最新的日志,如下图所示:
用一个简单的例子,我们执行个脚本do_setup.sh,不断的打印日志:
#!/bin/bash
for ((;;)); do
echo "installing $(date)"
sleep 3
done
我们可以加一个API,如果没有启动脚本就启动这个脚本,并返回日志:
class srs_cloud_main:
__plugin_path = "/www/server/panel/plugin/srs_cloud"
def installSrs(self, args):
if 'start' not in args: args.start = 0
if 'end' not in args: args.end = 0
srs = public.ExecShell('ls /usr/lib/systemd/system/srs-cloud.service >/dev/null 2>&1 && echo -n ok')[0]
running = public.ExecShell('ps aux |grep -v grep |grep srs_cloud |grep do_setup >/dev/null 2>&1 && echo -n ok')[0]
[tail, wc] = ['', 0]
if running != 'ok' and srs != 'ok':
public.ExecShell('nohup bash {}/do_setup.sh >/tmp/srs_cloud_install.log 2>&1 &'.format(self.__plugin_path))
elif os.path.exists('/tmp/srs_cloud_install.log'):
tail = public.ExecShell('sed -n "{},{}p" /tmp/srs_cloud_install.log'.format(args.start, args.end))[0]
wc = public.ExecShell('wc -l /tmp/srs_cloud_install.log')[0]
return public.returnMsg(True, json.dumps({'srs': srs, 'running': running, 'wc': wc, 'tail': tail}))
- 我们使用nohup来启动脚本,并使用ps查看脚本是否存在,若没有运行则运行这个脚本。
- 我们将脚本的日志打印到了/tmp/srs_cloud_install.log,这样可以供前端查询。
- 我们使用args.start和args.end,1代表第1行,前端可以从第一行开始获取,不断读取日志。
- 我们会检测是否安装完成,也就是srs已经有了,避免脚本退出后不断重启脚本。
我们在前端展示这个日志,给它加上黑黢黢的一个样式:
<style>
.tomcat_log {
margin: 0px;
width: 100%;
height: 370px;
background-color: #424251;
overflow: auto;
line-height: 22px;
color: #fff;
padding-left: 10px;
font-family: arial;
margin-top: 10px;
outline: none;
}
</style>
<textarea class="install_log tomcat_log"></textarea>
然后使用脚本调用API,启动任务和获取最新日志:
<script type="text/javascript">
const installing = async () => {
let pos = 1;
for (; ;) {
const pid = await srs_cloud_request('installSrs', {start: pos, end: pos + 100});
if (pid.srs) return;
pos += pid.tail ? (pid.tail.match(/\n/g)?.length || 0) : 0;
if (pid.tail) {
const elem = $('.install_log').val($('.install_log').val() + pid.tail);
elem.get(0).scrollTop = elem.get(0).scrollHeight;
}
await new Promise(r0 => setTimeout(r0, 3000));
}
};
installing();
</script>
Note: 函数srs_cloud_request的定义,请参考之前的说明。
每隔一定时间会重复获取日志,这样就实现了日志的刷新。由于我们使用start和end准确读取日志的文件,所以也不会漏掉日志。
Example: Website
若我们插件需要创建Web站点,可以调用panelSite的函数创建,比如SRS自动创建了一个站点,如下图所示:
除了/www/server/panel/class/public.py,宝塔还提供了/www/server/panel/class/panelSite.py,以及其他的函数可以直接调用。比如,创建站点:
如果猜不到函数名是什么,则可以在创建站点的页面中,看页面发起了什么请求,比如:
后端的Python函数可以这么写:
class srs_cloud_main:
__site = 'srs.cloud.local'
def createSrsSite(self, args):
site = panelSite().AddSite(Params(
webname = json.dumps({"domain": self.__site, "domainlist": [], "count": 0}),
type = 'PHP',
port = '80',
ps = self.__site,
path = os.path.join(public.get_site_path(), self.__site),
type_id = 0,
version = '00',
ftp = 'false',
sql = 'false',
codeing = 'utf8',
))
if 'status' in site:
return site
return public.returnMsg(True, json.dumps(site))
class Params(object):
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
def __iter__(self):
return self.__dict__.__iter__();
def __getitem__(self, y):
return self.__dict__.__getitem__(y)
其中有几个地方要注意的:
- 我们定义了一个Params的对象,将字典转换成对象,因为panelSite.AddSite(get)的参数是一个对象而不是字典。
- 参数的类型别弄错了,几乎都是字符串的,特别是webname是一个JSON字符串。
- 网站的路径,可以通过public.get_site_path()获取,不要写死了,可能会变的。
- 注意返回值若创建失败是有status,而成功则自己再需要打包下。
还可以调用防火墙对象,开放对应端口。