Compare commits

..

5 Commits

Author SHA1 Message Date
langhuihui
f96bc11ddb 增强对实例的控制 2020-02-15 21:07:47 +08:00
langhuihui
5563ddc0d2 增强对实例的控制 2020-02-14 09:54:53 +08:00
langhuihui
95657bd6df 增强对实例的控制 2020-02-13 17:41:39 +08:00
langhuihui
b9e19e75c8 增强对实例的控制 2020-02-13 10:43:32 +08:00
langhuihui
eac623639d 界面增加重启和升级 2020-02-11 21:59:31 +08:00
23 changed files with 657 additions and 303 deletions

160
main.go
View File

@@ -6,6 +6,7 @@ import (
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"mime"
@@ -14,6 +15,7 @@ import (
"os/exec"
"os/user"
"path"
"regexp"
"runtime"
"strings"
@@ -29,7 +31,7 @@ type InstanceDesc struct {
Config string
}
var instances map[string]*InstanceDesc
var instances = make(map[string]*InstanceDesc)
var instancesDir string
func main() {
@@ -44,10 +46,12 @@ func main() {
}
addr := flag.String("port", "8000", "http server port")
flag.Parse()
http.HandleFunc("/list", listInstance)
http.HandleFunc("/create", initInstance)
http.HandleFunc("/upgrade/engine", upgradeEngine)
http.HandleFunc("/restart/instance", restartInstance)
http.HandleFunc("/instance/import", importInstance)
http.HandleFunc("/instance/updateConfig", updateConfig)
http.HandleFunc("/instance/list", listInstance)
http.HandleFunc("/instance/create", initInstance)
http.HandleFunc("/instance/restart", restartInstance)
http.HandleFunc("/instance/shutdown", shutdownInstance)
http.HandleFunc("/", website)
fmt.Printf("start listen at %s", *addr)
if err := http.ListenAndServe(":"+*addr, nil); err != nil {
@@ -55,6 +59,70 @@ func main() {
}
}
func importInstance(w http.ResponseWriter, r *http.Request) {
var e error
defer func() {
result := "success"
if e != nil {
result = e.Error()
}
w.Write([]byte(result))
}()
name := r.URL.Query().Get("name")
if importPath := r.URL.Query().Get("path"); importPath != "" {
f, err := os.Open(importPath)
if e = err; err != nil {
return
}
children, err := f.Readdir(0)
if e = err; err == nil {
var hasMain, hasConfig, hasMod, hasRestart bool
for _, child := range children {
switch child.Name() {
case "main.go":
hasMain = true
case "config.toml":
hasConfig = true
case "go.mod":
hasMod = true
case "restart.sh":
hasRestart = true
}
}
if hasMain && hasConfig && hasMod && hasRestart {
if name == "" {
_, name = path.Split(importPath)
}
config, err := ioutil.ReadFile(path.Join(importPath, "config.toml"))
if e = err; err != nil {
return
}
mainGo, err := ioutil.ReadFile(path.Join(importPath, "main.go"))
if e = err; err != nil {
return
}
reg, err := regexp.Compile("_ \"(.+)\"")
if e = err; err != nil {
return
}
instances[name] = &InstanceDesc{
Name: name,
Path: importPath,
Plugins: nil,
Config: string(config),
}
for _, m := range reg.FindAllStringSubmatch(string(mainGo), -1) {
instances[name].Plugins = append(instances[name].Plugins, m[1])
}
} else {
e = errors.New("路径中缺少文件")
}
}
} else {
w.Write([]byte("参数错误"))
}
}
func readInstances() error {
if homeDir, err := Home(); err == nil {
instancesDir = path.Join(homeDir, ".monibuca")
@@ -140,31 +208,50 @@ func initInstance(w http.ResponseWriter, r *http.Request) {
}
instances[instanceDesc.Name] = instanceDesc
}
func upgradeEngine(w http.ResponseWriter, r *http.Request) {
sse := util.NewSSE(w, r.Context())
cmd := exec.Command("go", "get", "-u", "github.com/langhuihui/monibuca/monica")
func shutdownInstance(w http.ResponseWriter, r *http.Request) {
instanceName := r.URL.Query().Get("instance")
cmd.Dir = instances[instanceName].Path
err := sse.WriteExec(cmd)
if err != nil {
sse.Write([]byte(err.Error()))
if instance, ok := instances[instanceName]; ok {
if err := instance.command("kill", "-9", "`cat pid`").Run(); err == nil {
w.Write([]byte("success"))
} else {
w.Write([]byte(err.Error()))
}
} else {
w.Write([]byte("no such instance"))
}
}
func restartInstance(w http.ResponseWriter, r *http.Request) {
sse := util.NewSSE(w, r.Context())
instanceName := r.URL.Query().Get("instance")
cmd := exec.Command("sh", "restart.sh")
cmd.Dir = path.Join(instancesDir, instanceName)
cmd.Stderr = sse
cmd.Stdout = sse
err := cmd.Start()
if err != nil {
sse.Write([]byte(err.Error()))
needUpdate := r.URL.Query().Get("update") != ""
needBuild := r.URL.Query().Get("build") != ""
if instance, ok := instances[instanceName]; ok {
if needUpdate {
if err := sse.WriteExec(instance.command("go", "get", "-u")); err != nil {
sse.WriteEvent("failed", []byte(err.Error()))
return
}
}
if needBuild {
if err := sse.WriteExec(instance.command("go", "build")); err != nil {
sse.WriteEvent("failed", []byte(err.Error()))
return
}
}
if err := sse.WriteExec(instance.command("sh", "restart.sh")); err != nil {
sse.WriteEvent("failed", []byte(err.Error()))
return
}
sse.Write([]byte("success"))
} else {
sse.WriteEvent("failed", []byte("no such instance"))
}
}
func (p *InstanceDesc) writeExecSSE(sse *util.SSE, cmd *exec.Cmd) error {
func (p *InstanceDesc) command(name string, args ...string) (cmd *exec.Cmd) {
cmd = exec.Command(name, args...)
cmd.Dir = p.Path
return sse.WriteExec(cmd)
return
}
func (p *InstanceDesc) createDir(sse *util.SSE, clearDir bool) (err error) {
if clearDir {
@@ -200,12 +287,12 @@ func main(){
return
}
sse.WriteEvent("step", []byte("3:文件创建成功!"))
err = p.writeExecSSE(sse, exec.Command("go", "mod", "init", p.Name))
err = sse.WriteExec(p.command("go", "mod", "init", p.Name))
if err != nil {
return
}
sse.WriteEvent("step", []byte("4:go mod 初始化完成!"))
err = p.writeExecSSE(sse, exec.Command("go", "build"))
err = sse.WriteExec(p.command("go", "build"))
if err != nil {
return
}
@@ -215,17 +302,30 @@ func main(){
binFile := strings.TrimSuffix(p.Path, "/")
_, binFile = path.Split(binFile)
build.WriteString(binFile)
build.WriteString(" > log.txt & echo $! > pid\n")
build.WriteString(" & echo $! > pid\n")
err = ioutil.WriteFile(path.Join(p.Path, "restart.sh"), build.Bytes(), 0777)
if err != nil {
return
}
cmd := exec.Command("sh", "restart.sh")
cmd.Dir = p.Path
cmd.Stderr = sse
cmd.Stdout = sse
err = cmd.Start()
return
return sse.WriteExec(p.command("sh", "restart.sh"))
}
func updateConfig(w http.ResponseWriter, r *http.Request) {
instanceName := r.URL.Query().Get("instance")
if instance, ok := instances[instanceName]; ok {
f, err := os.OpenFile(path.Join(instance.Path, "config.toml"), os.O_WRONLY|os.O_TRUNC, 0666)
if err != nil {
w.Write([]byte(err.Error()))
return
}
_, err = io.Copy(f, r.Body)
if err != nil {
w.Write([]byte(err.Error()))
return
}
w.Write([]byte("success"))
} else {
w.Write([]byte("no such instance"))
}
}
func Home() (string, error) {
user, err := user.Current()

View File

@@ -4,12 +4,17 @@ import (
"encoding/json"
"io/ioutil"
"log"
"time"
"github.com/BurntSushi/toml"
)
var ConfigRaw []byte
var Version = "0.2.1"
var Version = "0.2.6"
var EngineInfo = &struct {
Version string
StartTime time.Time
}{Version, time.Now()}
func Run(configFile string) (err error) {
log.Printf("start monibuca version:%s", Version)

View File

@@ -1,28 +1,27 @@
package QoS
import (
"strings"
. "github.com/langhuihui/monibuca/monica"
)
var (
selectMap = map[string][]string{
"low": {"low", "medium", "high"},
"medium": {"medium", "low", "high"},
"high": {"high", "medium", "low"},
}
)
// var (
// selectMap = map[string][]string{
// "low": {"low", "medium", "high"},
// "medium": {"medium", "low", "high"},
// "high": {"high", "medium", "low"},
// }
// )
func getQualityName(name string, qualityLevel string) string {
if qualityLevel == "" {
return name
}
for _, l := range selectMap[qualityLevel] {
if _, ok := AllRoom.Load(name + "/" + l); ok {
return name + "/" + l
}
}
return name + "/" + qualityLevel
}
// func getQualityName(name string, qualityLevel string) string {
// for _, l := range selectMap[qualityLevel] {
// if _, ok := AllRoom.Load(name + "/" + l); ok {
// return name + "/" + l
// }
// }
// return name + "/" + qualityLevel
// }
var config = struct {
Suffix []string
@@ -39,8 +38,23 @@ func init() {
func run() {
OnDropHooks.AddHook(func(s *OutputStream) {
if s.TotalDrop > s.TotalPacket>>2 {
//TODO
//s.Control<-&ChangeRoomCmd{s,AllRoom.Get()}
var newStreamPath = ""
for i, suf := range config.Suffix {
if strings.HasSuffix(s.StreamPath, suf) {
if i < len(config.Suffix)-1 {
newStreamPath = s.StreamPath + "/" + config.Suffix[i+1]
break
}
} else {
newStreamPath = s.StreamPath + "/" + suf
break
}
}
if newStreamPath != "" {
if _, ok := AllRoom.Load(newStreamPath); ok {
s.Control <- &ChangeRoomCmd{s, AllRoom.Get(newStreamPath)}
}
}
}
})
}

View File

@@ -15,8 +15,8 @@ import (
)
var (
config = new(ListenerConfig)
config = new(ListenerConfig)
startTime = time.Now()
dashboardPath string
)
@@ -99,7 +99,8 @@ func summary(w http.ResponseWriter, r *http.Request) {
}
}
func sysInfo(w http.ResponseWriter, r *http.Request) {
bytes, err := json.Marshal(&struct{ Version string }{Version: Version})
w.Header().Set("Access-Control-Allow-Origin", "*")
bytes, err := json.Marshal(EngineInfo)
if err == nil {
_, err = w.Write(bytes)
}

View File

@@ -0,0 +1,73 @@
package logrotate
import (
"fmt"
. "github.com/langhuihui/monibuca/monica"
"log"
"os"
"path"
"time"
)
var config = new(LogRotate)
type LogRotate struct {
Path string
Size int64
Days int
file *os.File
currentSize int64
createTime time.Time
hours float64
splitFunc func() bool
}
func init() {
InstallPlugin(&PluginConfig{
Name: "LogRotate",
Type: PLUGIN_HOOK,
Config: config,
Run: run,
})
}
func run() {
if config.Size > 0 {
config.splitFunc = config.splitBySize
} else {
if config.Days == 0 {
config.Days = 1
}
config.hours = float64(config.Days) * 24
config.splitFunc = config.splitByTime
}
config.createTime = time.Now()
file, err := os.OpenFile(path.Join(config.Path, fmt.Sprintf("%s.log", config.createTime.Format("2006-01-02T15:04:05"))), os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0666)
if err == nil {
config.file = file
stat, _ := file.Stat()
config.currentSize = stat.Size()
AddWriter(config)
} else {
log.Println(err)
}
}
func (l *LogRotate) splitBySize() bool {
return l.currentSize >= l.Size
}
func (l *LogRotate) splitByTime() bool {
return time.Since(l.createTime).Hours() > l.hours
}
func (l *LogRotate) Write(data []byte) (n int, err error) {
n, err = l.file.Write(data)
l.currentSize += int64(n)
if err == nil {
if l.splitFunc() {
l.createTime = time.Now()
if file, err := os.OpenFile(path.Join(l.Path, fmt.Sprintf("%s.log", l.createTime.Format("2006-01-02T15:04:05"))), os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0666); err == nil {
l.file = file
l.currentSize = 0
}
}
}
return
}

2
pm/dist/index.html vendored
View File

@@ -1 +1 @@
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1"><link rel=icon href=/favicon.ico><title>Monibuca Instance Manager</title><script src=ajax.js></script><link href=/css/app.200d2f8f.css rel=preload as=style><link href=/css/chunk-vendors.22ebf426.css rel=preload as=style><link href=/js/app.fd72a180.js rel=preload as=script><link href=/js/chunk-vendors.6b87e1b5.js rel=preload as=script><link href=/css/chunk-vendors.22ebf426.css rel=stylesheet><link href=/css/app.200d2f8f.css rel=stylesheet></head><body><noscript><strong>We're sorry but pm doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script src=/js/chunk-vendors.6b87e1b5.js></script><script src=/js/app.fd72a180.js></script></body></html>
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1"><link rel=icon href=/favicon.ico><title>Monibuca Instance Manager</title><script src=ajax.js></script><link href=/css/app.200d2f8f.css rel=preload as=style><link href=/css/chunk-vendors.22ebf426.css rel=preload as=style><link href=/js/app.f5cfbf83.js rel=preload as=script><link href=/js/chunk-vendors.470a264b.js rel=preload as=script><link href=/css/chunk-vendors.22ebf426.css rel=stylesheet><link href=/css/app.200d2f8f.css rel=stylesheet></head><body><noscript><strong>We're sorry but pm doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script src=/js/chunk-vendors.470a264b.js></script><script src=/js/app.f5cfbf83.js></script></body></html>

2
pm/dist/js/app.f5cfbf83.js vendored Normal file

File diff suppressed because one or more lines are too long

1
pm/dist/js/app.f5cfbf83.js.map vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

5
pm/package-lock.json generated
View File

@@ -979,6 +979,11 @@
"@hapi/hoek": "8.5.0"
}
},
"@iarna/toml": {
"version": "2.2.3",
"resolved": "https://registry.npm.taobao.org/@iarna/toml/download/@iarna/toml-2.2.3.tgz",
"integrity": "sha1-8GC/bqr65NVqfaxhiYCDiwaW4qs="
},
"@intervolga/optimize-cssnano-plugin": {
"version": "1.0.6",
"resolved": "https://registry.npm.taobao.org/@intervolga/optimize-cssnano-plugin/download/@intervolga/optimize-cssnano-plugin-1.0.6.tgz",

View File

@@ -8,6 +8,7 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
"@iarna/toml": "^2.2.3",
"core-js": "^3.4.4",
"view-design": "^4.0.0",
"vue": "^2.6.10",

View File

@@ -1,5 +1,5 @@
<template>
<Modal v-bind="$attrs" v-on="$listeners" :title="info.Path">
<Modal v-bind="$attrs" v-on="$listeners" :title="info && info.Path">
<Steps :current="currentStep" size="small" :status="status">
<Step title="解析请求"></Step>
<Step title="创建目录"></Step>
@@ -14,8 +14,7 @@
</div>
<div slot="footer">
<Checkbox v-model="clearDir">安装前清空目录</Checkbox>
<Button type="primary" @click="start">开始</Button>
<Button type="success" @click="close" v-if="status=='finish'">完成</Button>
<Button type="primary" @click="start" :loading="status=='process'">开始</Button>
</div>
</Modal>
</template>
@@ -27,36 +26,37 @@ export default {
props: {
info: Object
},
methods:{
start(){
eventSource = new EventSource(
"/create?info=" + JSON.stringify(this.info)+(this.clearDir?"&clear=true":"")
);
eventSource.onopen = () => (this.log = "");
eventSource.onmessage = evt => {
this.log += evt.data + "\n";
if (evt.data == "success") {
this.status = "finish";
eventSource.close();
}
};
eventSource.addEventListener("exception", evt => {
this.log += evt.data + "\n";
this.status = "error";
methods: {
start() {
this.status = "process";
eventSource = new EventSource(
"/instance/create?info=" +
JSON.stringify(this.info) +
(this.clearDir ? "&clear=true" : "")
);
eventSource.onopen = () => (this.log = "");
eventSource.onmessage = evt => {
this.log += evt.data + "\n";
if (evt.data == "success") {
this.status = "finish";
eventSource.close();
});
eventSource.addEventListener("step", evt => {
let [step, msg] = evt.data.split(":");
this.currentStep = step | 0;
this.log += msg + "\n";
});
},close(){
this.$Modal.remove()
}
}
};
eventSource.addEventListener("exception", evt => {
this.log += evt.data + "\n";
this.status = "error";
eventSource.close();
});
eventSource.addEventListener("step", evt => {
let [step, msg] = evt.data.split(":");
this.currentStep = step | 0;
this.log += msg + "\n";
});
},
},
data() {
return { clearDir: true, currentStep: 0, log: "", status: "process" };
return { clearDir: true, currentStep: 0, log: "", status: "wait" };
}
};
</script>

View File

@@ -1,60 +0,0 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="less">
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

View File

@@ -0,0 +1,41 @@
<template>
<div>
<i-input v-model="instanceName" :placeholder="defaultInstanceName">
<span slot="prepend">实例名称</span>
</i-input>
<i-input prefix="ios-home" v-model="instancePath" placeholder="输入实例所在的路径" search enter-button="Import" @on-search="doImport">
</i-input>
</div>
</template>
<script>
export default {
name: "ImportInstance",
data(){
return {
instancePath:"",
instanceName:""
}
},
computed:{
defaultInstanceName(){
return this.instancePath.replace(/\\/g,"/").split("/").pop()
}
},
methods:{
doImport(){
window.ajax.get("/instance/import?path="+this.instancePath+"&name="+this.instanceName).then(x=>{
if(x=="success"){
this.$Message.success("导入成功!")
}else{
this.$Message.error(x)
}
})
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,156 @@
<template>
<div>
<List border>
<ListItem v-for="item in instances" :key="item.Name">
<ListItemMeta :title="item.Name" :description="item.Path"></ListItemMeta>
<template v-if="item.Info.StartTime">
引擎版本{{item.Info.Version}} <br>启动时间
<StartTime :value="item.Info.StartTime"></StartTime>
</template>
<template v-else>{{item.Info}}</template>
<template slot="action">
<li @click="changeConfig(item)">
<Icon type="ios-settings"/>
修改配置
</li>
<li v-if="hasGateway(item)" @click="openGateway(item)">
<Icon type="md-browsers"/>
管理界面
</li>
<li @click="currentItem=item,showRestart=true">
<Icon type="ios-refresh"/>
重启
</li>
<li @click="shutdown(item)">
<Icon type="ios-power"/>
关闭
</li>
</template>
</ListItem>
</List>
<Modal v-model="showRestart" title="重启选项" @on-ok="restart">
<Checkbox v-model="update">go get -u</Checkbox>
<Checkbox v-model="build">go build</Checkbox>
</Modal>
<Modal v-model="showConfig" title="修改实例配置" @on-ok="submitConfigChange">
<i-input type="textarea" v-model="currentConfig" :rows="20"></i-input>
</Modal>
</div>
</template>
<script>
import toml from "@iarna/toml"
import StartTime from "./StartTime"
export default {
name: "InstanceList",
components: {StartTime},
data() {
return {
instances: [],
showRestart: false,
update: false,
build: false,
showConfig: false,
currentItem: null,
currentConfig: ""
}
},
mounted() {
window.ajax.getJSON("/instance/list").then(x => {
for (let name in x) {
let instance = x[name]
instance.Config = toml.parse(instance.Config)
if (this.hasGateway(instance)) {
window.ajax.getJSON(this.gateWayHref(instance) + "/api/sysInfo").then(x => {
instance.Info = x
}).catch(() => {
instance.Info = "无法访问实例"
})
} else {
instance.Info = "实例未配置网关插件"
}
this.instances.push(instance)
}
// this.instances = x;
});
},
methods: {
changeConfig(item) {
this.showConfig = true
this.currentItem = item
this.currentConfig = toml.stringify(item.Config)
},
submitConfigChange() {
try {
this.currentItem.Config = toml.parse(this.currentConfig)
window.ajax.post("/instance/updateConfig?instance=" + this.currentItem.Name, this.currentConfig).then(x => {
if (x == "success") {
this.$Message.success("更新成功!")
} else {
this.$Message.error(x)
}
}).catch(e => {
this.$Message.error(e)
})
} catch (e) {
this.$Message.error(e)
}
},
openGateway(item) {
window.open(this.gateWayHref(item), '_blank')
},
hasGateway(item) {
return item.Config.Plugins.hasOwnProperty("GateWay")
},
gateWayHref(item) {
return "http://" + location.hostname + ":" + item.Config.Plugins.GateWay.ListenAddr.split(":").pop()
},
restart() {
let item = this.currentItem
const msg = this.$Message.loading({
content: 'restart ' + item.Name + '...',
duration: 0
});
let arg = item.Name
if (this.update) {
arg += "&update=true"
}
if (this.build) {
arg += "&build=true"
}
const es = new EventSource("/instance/restart?instance=" + arg)
es.onmessage = evt => {
if (evt.data == "success") {
this.$Message.success("重启成功!")
msg()
} else {
this.$Message.info(evt.data)
}
}
es.addEventListener("failed", evt => {
this.$Message.error(evt.data)
msg()
})
es.onerror = e => {
if (e && e.toString()) this.$Message.error(e);
msg()
es.close()
}
},
shutdown(item) {
window.ajax.get("/instance/shutdown?instance=" + item.Name).then(x => {
if (x == "success") {
this.$Message.success("已关闭实例")
} else {
this.$Message.error(x)
}
})
},
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,18 @@
<template>
<Poptip trigger="hover" :content="'⌚️'+ new Date(value).toLocaleString()">
<Time :time="new Date(value)"></Time>
</Poptip>
</template>
<script>
export default {
name: "StartTime",
props:{
value:String
}
}
</script>
<style scoped>
</style>

View File

@@ -1,10 +1,40 @@
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
defaultPlugins:{
GateWay:[
"gateway",'ListenAddr = ":8081"',"网关插件提供各种API服务包括信息采集和控制等控制台页面展示静态资源服务器"
],
LogRotate:[
"logrotate",`Path = "log"
Size = 0
Days = 1`,"日志分割插件Size 代表按照字节数分割0代表采用时间分割"
],
Jessica:[
"jessica",'ListenAddr = ":8080"',"WebSocket协议订阅采用私有协议搭配Jessibuca播放器实现低延时播放"
],
Cluster:[
"cluster",'Master = "localhost:2019"\nListenAddr = ":2019"',"集群插件可以实现级联转发功能Master代表上游服务器ListenAdder代表源服务器监听端口可只配置一项"
],
RTMP:[
"rtmp",'ListenAddr = ":1935"',"rtmp协议实现基本发布和订阅功能"
],
RecordFlv:[
"record",'Path="./resource"',"录制视频流到flv文件"
],
HDL:[
"HDL",'ListenAddr = ":2020"',"Http-flv格式实现可以对接CDN厂商进行回源拉流"
],
Auth:[
"auth",'Key = "www.monibuca.com"',"一个鉴权验证模块"
],
Qos:[
"QoS",'Suffix = ["high","medium","low"]',"质量控制插件,可以动态改变订阅的不同的质量的流"
]
}
},
mutations: {
},

View File

@@ -1,18 +0,0 @@
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</div>
</template>
<script>
// @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue'
export default {
name: 'home',
components: {
HelloWorld
}
}
</script>

View File

@@ -4,11 +4,7 @@
<Content class="content">
<Tabs value="name1">
<TabPane label="实例" name="name1">
<List border>
<ListItem v-for="item in instances" :key="item.Name">
<ListItemMeta :title="item.Name" :description="item.Path"></ListItemMeta>
</ListItem>
</List>
<InstanceList></InstanceList>
</TabPane>
<TabPane label="创建" name="name2">
<Steps :current="createStep">
@@ -26,7 +22,8 @@
{{item.Config}}
<template slot="action">
<li @click="removePlugin(name)">
<Icon type="ios-trash" />移除
<Icon type="ios-trash"/>
移除
</li>
</template>
</ListItem>
@@ -34,8 +31,8 @@
<div v-else>
<h3>实例名称</h3>
<i-input
v-model="instanceName"
:placeholder="createPath.split('/').pop()"
v-model="instanceName"
:placeholder="createPath.split('/').pop()"
></i-input>
<h4>安装路径</h4>
<div>
@@ -52,27 +49,28 @@
</div>
<ButtonGroup style="display:table;margin:50px auto;">
<Button
size="large"
type="primary"
@click="createStep--"
v-if="createStep!=0"
size="large"
type="primary"
@click="createStep--"
v-if="createStep!=0"
>
<Icon type="ios-arrow-back"></Icon>上一步
<Icon type="ios-arrow-back"></Icon>
上一步
</Button>
<Button
size="large"
type="success"
@click="showAddPlugin=true"
v-if="createStep==1"
size="large"
type="success"
@click="showAddPlugin=true"
v-if="createStep==1"
>
+
添加插件
</Button>
<Button
size="large"
type="primary"
@click="createStep++"
v-if="createStep!=2"
size="large"
type="primary"
@click="createStep++"
v-if="createStep!=2"
>
下一步
<Icon type="ios-arrow-forward"></Icon>
@@ -81,7 +79,9 @@
</ButtonGroup>
</div>
</TabPane>
<TabPane label="导入" name="name3"></TabPane>
<TabPane label="导入" name="name3">
<ImportInstance></ImportInstance>
</TabPane>
</Tabs>
</Content>
<Modal v-model="showAddPlugin" title="添加Plugin" @on-ok="addPlugin">
@@ -94,9 +94,9 @@
<Button slot="append" @click="showBuiltinPlugin=true">内置插件</Button>
</i-input>
</FormItem>
<Alert
type="show-icon"
v-if="!Object.values(builtinPlugins).includes(formPlugin.Path)"
<Alert show-icon
type="warning"
v-if="!isBuiltInPlugin(formPlugin.Path)"
>
如果该插件是私有仓库请到服务器上输入echo "machine {{privateHost}} login 用户名 password 密码" >> ~/.netrc
并且添加环境变量GOPRIVATE={{privateHost}}
@@ -108,11 +108,12 @@
</Modal>
<Modal v-model="showBuiltinPlugin">
<List>
<ListItem v-for="(item,name) in builtinPlugins" :key="name">
<ListItemMeta :title="name" :description="item"></ListItemMeta>
<ListItem v-for="(item,name) in $store.state.defaultPlugins" :key="name">
<ListItemMeta :title="name" :description="item[2]"></ListItemMeta>
<template slot="action">
<li @click="addBuiltin(name,item)">
<Icon type="ios-add" />添加
<Icon type="ios-add"/>
添加
</li>
</template>
</ListItem>
@@ -123,116 +124,96 @@
</template>
<script>
import CreateInstance from "../components/CreateInstance";
import CreateInstance from "../components/CreateInstance";
import InstanceList from "../components/InstanceList";
import ImportInstance from "../components/ImportInstance";
export default {
components: {
CreateInstance
},
data() {
return {
instanceName: "",
createStep: 0,
showCreate: false,
createInfo: null,
createPath: "/opt/monibuca",
instances: {},
plugins: {},
showAddPlugin: false,
formPlugin: {},
showBuiltinPlugin: false,
builtinPlugins: {
Auth: "github.com/langhuihui/monibuca/plugins/auth",
Cluster: "github.com/langhuihui/monibuca/plugins/cluster",
GateWay: "github.com/langhuihui/monibuca/plugins/gateway",
HDL: "github.com/langhuihui/monibuca/plugins/HDL",
Jessica: "github.com/langhuihui/monibuca/plugins/jessica",
QoS: "github.com/langhuihui/monibuca/plugins/QoS",
RecordFlv: "github.com/langhuihui/monibuca/plugins/record",
RTMP: "github.com/langhuihui/monibuca/plugins/rtmp"
},
defaultConfig: {
Auth: 'Key = "www.monibuca.com"',
RecordFlv: 'Path="./resource"',
QoS: 'Suffix = ["high","medium","low"]',
Cluster: 'Master = "localhost:2019"\nListenAddr = ":2019"',
GateWay: 'ListenAddr = ":8081"',
RTMP: 'ListenAddr = ":1935"',
Jessica: 'ListenAddr = ":8080"',
HDL: 'ListenAddr = ":2020"'
}
};
},
computed: {
pluginStr() {
return Object.values(this.plugins)
.map(x => x.Path)
.join("\n");
export default {
components: {
CreateInstance,InstanceList,ImportInstance
},
configStr() {
return Object.values(this.plugins)
.map(
x => `[Plugins.${x.Name}]
${x.Config || ""}`
)
.join("\n");
},
privateHost() {
return (
(this.formPlugin.Path && this.formPlugin.Path.split("/")[0]) ||
"仓库域名"
);
}
},
mounted() {
window.ajax.getJSON("/list").then(x => {
this.instances = x;
});
},
methods: {
goUp() {
let paths = this.createPath.split("/");
paths.pop();
this.createPath = paths.join("/");
},
createInstance() {
this.showCreate = true;
this.createInfo = {
Name: this.instanceName || this.createPath.split("/").pop(),
Path: this.createPath,
Plugins: Object.values(this.plugins).map(x => x.Path),
Config: this.configStr
data() {
return {
instanceName: "",
createStep: 0,
showCreate: false,
createInfo: null,
createPath: "/opt/monibuca",
plugins: {},
showAddPlugin: false,
formPlugin: {},
showBuiltinPlugin: false,
};
},
addPlugin() {
this.plugins[this.formPlugin.Name] = this.formPlugin;
this.formPlugin = {};
computed: {
pluginStr() {
return Object.values(this.plugins)
.map(x => x.Path)
.join("\n");
},
configStr() {
return Object.values(this.plugins)
.map(
x => `[Plugins.${x.Name}]
${x.Config || ""}`
)
.join("\n");
},
privateHost() {
return (
(this.formPlugin.Path && this.formPlugin.Path.split("/")[0]) ||
"仓库域名"
);
}
},
removePlugin(name) {
delete this.plugins[name];
this.$forceUpdate();
},
addBuiltin(name, item) {
this.formPlugin.Name = name;
this.formPlugin.Path = item;
this.formPlugin.Config = this.defaultConfig[name];
this.showBuiltinPlugin = false;
methods: {
isBuiltInPlugin(path){
return Object.values(this.$store.state.defaultPlugins).some(x=>"github.com/langhuihui/monibuca/plugins/"+x[0]==path)
},
goUp() {
let paths = this.createPath.split("/");
paths.pop();
this.createPath = paths.join("/");
},
createInstance() {
this.showCreate = true;
this.createInfo = {
Name: this.instanceName || this.createPath.split("/").pop(),
Path: this.createPath,
Plugins: Object.values(this.plugins).map(x => x.Path),
Config: this.configStr
};
},
addPlugin() {
this.plugins[this.formPlugin.Name] = this.formPlugin;
this.formPlugin = {};
},
removePlugin(name) {
delete this.plugins[name];
this.$forceUpdate();
},
addBuiltin(name, item) {
this.formPlugin.Name = name;
this.formPlugin.Path = "github.com/langhuihui/monibuca/plugins/"+item[0];
this.formPlugin.Config = item[1];
this.showBuiltinPlugin = false;
},
}
}
};
};
</script>
<style>
.content {
background: white;
}
.content {
background: white;
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
}
.ivu-tabs .ivu-tabs-tabpane {
padding: 20px;
}
.ivu-tabs .ivu-tabs-tabpane {
padding: 20px;
}
</style>