Compare commits

...

7 Commits

Author SHA1 Message Date
langhuihui
709c2c6ac7 增加目录候选功能 2020-02-17 11:48:58 +08:00
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
langhuihui
fea6e98ca7 小功能增加 2020-02-11 17:27:05 +08:00
29 changed files with 1839 additions and 281 deletions

201
main.go
View File

@@ -6,9 +6,7 @@ import (
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"github.com/BurntSushi/toml" "io"
. "github.com/langhuihui/monibuca/monica"
"github.com/langhuihui/monibuca/monica/util"
"io/ioutil" "io/ioutil"
"log" "log"
"mime" "mime"
@@ -17,8 +15,14 @@ import (
"os/exec" "os/exec"
"os/user" "os/user"
"path" "path"
"path/filepath"
"regexp"
"runtime" "runtime"
"strings" "strings"
"github.com/BurntSushi/toml"
. "github.com/langhuihui/monibuca/monica"
"github.com/langhuihui/monibuca/monica/util"
) )
type InstanceDesc struct { type InstanceDesc struct {
@@ -28,7 +32,7 @@ type InstanceDesc struct {
Config string Config string
} }
var instances map[string]*InstanceDesc var instances = make(map[string]*InstanceDesc)
var instancesDir string var instancesDir string
func main() { func main() {
@@ -43,10 +47,13 @@ func main() {
} }
addr := flag.String("port", "8000", "http server port") addr := flag.String("port", "8000", "http server port")
flag.Parse() flag.Parse()
http.HandleFunc("/list", listInstance) http.HandleFunc("/instance/listDir", listDir)
http.HandleFunc("/create", initInstance) http.HandleFunc("/instance/import", importInstance)
http.HandleFunc("/upgrade/engine", upgradeEngine) http.HandleFunc("/instance/updateConfig", updateConfig)
http.HandleFunc("/restart/instance", restartInstance) http.HandleFunc("/instance/list", listInstance)
http.HandleFunc("/instance/create", initInstance)
http.HandleFunc("/instance/restart", restartInstance)
http.HandleFunc("/instance/shutdown", shutdownInstance)
http.HandleFunc("/", website) http.HandleFunc("/", website)
fmt.Printf("start listen at %s", *addr) fmt.Printf("start listen at %s", *addr)
if err := http.ListenAndServe(":"+*addr, nil); err != nil { if err := http.ListenAndServe(":"+*addr, nil); err != nil {
@@ -54,6 +61,88 @@ func main() {
} }
} }
func listDir(w http.ResponseWriter, r *http.Request) {
if input := r.URL.Query().Get("input"); input != "" {
if dir, err := os.Open(filepath.Dir(input)); err == nil {
var dirs []string
if infos, err := dir.Readdir(0); err == nil {
for _, info := range infos {
if info.IsDir() {
dirs = append(dirs, info.Name())
}
}
if bytes, err := json.Marshal(dirs); err == nil {
w.Write(bytes)
}
}
}
}
}
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 { func readInstances() error {
if homeDir, err := Home(); err == nil { if homeDir, err := Home(); err == nil {
instancesDir = path.Join(homeDir, ".monibuca") instancesDir = path.Join(homeDir, ".monibuca")
@@ -110,6 +199,7 @@ func initInstance(w http.ResponseWriter, r *http.Request) {
instanceDesc := new(InstanceDesc) instanceDesc := new(InstanceDesc)
sse := util.NewSSE(w, r.Context()) sse := util.NewSSE(w, r.Context())
err := json.Unmarshal([]byte(r.URL.Query().Get("info")), instanceDesc) err := json.Unmarshal([]byte(r.URL.Query().Get("info")), instanceDesc)
clearDir := r.URL.Query().Get("clear") != ""
defer func() { defer func() {
if err != nil { if err != nil {
sse.WriteEvent("exception", []byte(err.Error())) sse.WriteEvent("exception", []byte(err.Error()))
@@ -121,7 +211,7 @@ func initInstance(w http.ResponseWriter, r *http.Request) {
return return
} }
sse.WriteEvent("step", []byte("1:参数解析成功!")) sse.WriteEvent("step", []byte("1:参数解析成功!"))
err = instanceDesc.createDir(sse) err = instanceDesc.createDir(sse, clearDir)
if err != nil { if err != nil {
return return
} }
@@ -138,33 +228,55 @@ func initInstance(w http.ResponseWriter, r *http.Request) {
} }
instances[instanceDesc.Name] = instanceDesc instances[instanceDesc.Name] = instanceDesc
} }
func upgradeEngine(w http.ResponseWriter, r *http.Request) { func shutdownInstance(w http.ResponseWriter, r *http.Request) {
sse := util.NewSSE(w, r.Context())
cmd := exec.Command("go", "get", "-u", "github.com/langhuihui/monibuca/monica")
instanceName := r.URL.Query().Get("instance") instanceName := r.URL.Query().Get("instance")
cmd.Dir = instances[instanceName].Path if instance, ok := instances[instanceName]; ok {
err := sse.WriteExec(cmd) if err := instance.command("kill", "-9", "`cat pid`").Run(); err == nil {
if err != nil { w.Write([]byte("success"))
sse.Write([]byte(err.Error())) } else {
w.Write([]byte(err.Error()))
}
} else {
w.Write([]byte("no such instance"))
} }
} }
func restartInstance(w http.ResponseWriter, r *http.Request) { func restartInstance(w http.ResponseWriter, r *http.Request) {
sse := util.NewSSE(w, r.Context()) sse := util.NewSSE(w, r.Context())
instanceName := r.URL.Query().Get("instance") instanceName := r.URL.Query().Get("instance")
cmd := exec.Command("sh", "restart.sh") needUpdate := r.URL.Query().Get("update") != ""
cmd.Dir = path.Join(instancesDir, instanceName) needBuild := r.URL.Query().Get("build") != ""
cmd.Stderr = sse if instance, ok := instances[instanceName]; ok {
cmd.Stdout = sse if needUpdate {
err := cmd.Start() if err := sse.WriteExec(instance.command("go", "get", "-u")); err != nil {
if err != nil { sse.WriteEvent("failed", []byte(err.Error()))
sse.Write([]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 cmd.Dir = p.Path
return sse.WriteExec(cmd) return
} }
func (p *InstanceDesc) createDir(sse *util.SSE) (err error) { func (p *InstanceDesc) createDir(sse *util.SSE, clearDir bool) (err error) {
if clearDir {
os.RemoveAll(p.Path)
}
err = os.MkdirAll(p.Path, 0666) err = os.MkdirAll(p.Path, 0666)
if err != nil { if err != nil {
return return
@@ -195,30 +307,45 @@ func main(){
return return
} }
sse.WriteEvent("step", []byte("3:文件创建成功!")) 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 { if err != nil {
return return
} }
sse.WriteEvent("step", []byte("4:go mod 初始化完成!")) 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 { if err != nil {
return return
} }
sse.WriteEvent("step", []byte("5:go build 成功!")) sse.WriteEvent("step", []byte("5:go build 成功!"))
build.Reset() build.Reset()
build.WriteString("kill -9 `cat pid`\nnohup .") build.WriteString("kill -9 `cat pid`\nnohup ./")
build.WriteString(path.Dir(path.Join(p.Path, "main.go"))) binFile := strings.TrimSuffix(p.Path, "/")
build.WriteString(" > log.txt & echo $! > pid\n") _, binFile = path.Split(binFile)
build.WriteString(binFile)
build.WriteString(" & echo $! > pid\n")
err = ioutil.WriteFile(path.Join(p.Path, "restart.sh"), build.Bytes(), 0777) err = ioutil.WriteFile(path.Join(p.Path, "restart.sh"), build.Bytes(), 0777)
if err != nil { if err != nil {
return return
} }
cmd := exec.Command("sh", "restart.sh") return sse.WriteExec(p.command("sh", "restart.sh"))
cmd.Dir = p.Path }
cmd.Stderr = sse func updateConfig(w http.ResponseWriter, r *http.Request) {
cmd.Stdout = sse instanceName := r.URL.Query().Get("instance")
err = cmd.Start() if instance, ok := instances[instanceName]; ok {
return 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) { func Home() (string, error) {
user, err := user.Current() user, err := user.Current()

Binary file not shown.

View File

@@ -4,15 +4,22 @@ import (
"encoding/json" "encoding/json"
"io/ioutil" "io/ioutil"
"log" "log"
"time"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
) )
var ConfigRaw []byte var ConfigRaw []byte
var Version = "0.1.2" var Version = "0.2.6"
var EngineInfo = &struct {
Version string
StartTime time.Time
}{Version, time.Now()}
func Run(configFile string) (err error) { func Run(configFile string) (err error) {
log.Printf("start monibuca version:%s", Version)
if ConfigRaw, err = ioutil.ReadFile(configFile); err != nil { if ConfigRaw, err = ioutil.ReadFile(configFile); err != nil {
log.Printf("read config file error: %v", err)
return return
} }
go Summary.StartSummary() go Summary.StartSummary()
@@ -31,6 +38,8 @@ func Run(configFile string) (err error) {
go config.Run() go config.Run()
} }
} }
} else {
log.Printf("decode config file error: %v", err)
} }
return return
} }

View File

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

@@ -1,8 +1,7 @@
package gateway package gateway
import ( import (
. "github.com/langhuihui/monibuca/monica" "encoding/json"
. "github.com/langhuihui/monibuca/monica/util"
"io/ioutil" "io/ioutil"
"log" "log"
"mime" "mime"
@@ -10,11 +9,14 @@ import (
"path" "path"
"runtime" "runtime"
"time" "time"
. "github.com/langhuihui/monibuca/monica"
. "github.com/langhuihui/monibuca/monica/util"
) )
var ( var (
config = new(ListenerConfig) config = new(ListenerConfig)
startTime = time.Now()
dashboardPath string dashboardPath string
) )
@@ -30,6 +32,7 @@ func init() {
}) })
} }
func run() { func run() {
http.HandleFunc("/api/sysInfo", sysInfo)
http.HandleFunc("/api/stop", stopPublish) http.HandleFunc("/api/stop", stopPublish)
http.HandleFunc("/api/summary", summary) http.HandleFunc("/api/summary", summary)
http.HandleFunc("/api/logs", watchLogs) http.HandleFunc("/api/logs", watchLogs)
@@ -95,3 +98,10 @@ func summary(w http.ResponseWriter, r *http.Request) {
} }
} }
} }
func sysInfo(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
bytes, err := json.Marshal(EngineInfo)
if err == nil {
_, err = w.Write(bytes)
}
}

View File

@@ -0,0 +1,78 @@
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()
err := os.MkdirAll(config.Path, 0666)
config.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 {
stat, _ := config.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
}
//func (l *LogRotate) FindLog(grep string) string{
// cmd:=exec.Command("grep",fmt.Sprintf("\"%s\"",grep),l.Path)
// err:=cmd.Run()
//}

535
pm/dist/ajax.js vendored Normal file
View File

@@ -0,0 +1,535 @@
// a simple ajax
!(function () {
var jsonType = 'application/json';
var htmlType = 'text/html';
var xmlTypeRE = /^(?:text|application)\/xml/i;
var blankRE = /^\s*$/; // \s
/*
* default setting
* */
var _settings = {
type: "GET",
beforeSend: noop,
success: noop,
error: noop,
complete: noop,
context: null,
xhr: function () {
return new window.XMLHttpRequest();
},
accepts: {
json: jsonType,
xml: 'application/xml, text/xml',
html: htmlType,
text: 'text/plain'
},
crossDomain: false,
timeout: 0,
username: null,
password: null,
processData: true,
promise: noop
};
function noop() {
}
var ajax = function (options) {
//
var settings = extend({}, options || {});
//
for (var key in _settings) {
if (settings[key] === undefined) {
settings[key] = _settings[key];
}
}
//
try {
var q = {};
var promise = new Promise(function (resolve, reject) {
q.resolve = resolve;
q.reject = reject;
});
promise.resolve = q.resolve;
promise.reject = q.reject;
settings.promise = promise;
}
catch (e) {
//
settings.promise = {
resolve: noop,
reject: noop
};
}
//
if (!settings.crossDomain) {
settings.crossDomain = /^([\w-]+:)?\/\/([^\/]+)/.test(settings.url) && RegExp.$2 !== window.location.href;
}
var dataType = settings.dataType;
// jsonp
if (dataType === 'jsonp') {
//
var hasPlaceholder = /=\?/.test(settings.url);
if (!hasPlaceholder) {
var jsonpCallback = (settings.jsonp || 'callback') + '=?';
settings.url = appendQuery(settings.url, jsonpCallback)
}
return JSONP(settings);
}
// url
if (!settings.url) {
settings.url = window.location.toString();
}
//
serializeData(settings);
var mime = settings.accepts[dataType]; // mime
var baseHeader = {}; // header
var protocol = /^([\w-]+:)\/\//.test(settings.url) ? RegExp.$1 : window.location.protocol; // protocol
var xhr = _settings.xhr();
var abortTimeout;
// X-Requested-With header
// For cross-domain requests, seeing as conditions for a preflight are
// akin to a jigsaw puzzle, we simply never set it to be sure.
// (it can always be set on a per-request basis or even using ajaxSetup)
// For same-domain requests, won't change header if already provided.
if (!settings.crossDomain && !baseHeader['X-Requested-With']) {
baseHeader['X-Requested-With'] = 'XMLHttpRequest';
}
// mime
if (mime) {
//
baseHeader['Accept'] = mime;
if (mime.indexOf(',') > -1) {
mime = mime.split(',', 2)[0]
}
//
xhr.overrideMimeType && xhr.overrideMimeType(mime);
}
// contentType
if (settings.contentType || (settings.data && settings.type.toUpperCase() !== 'GET')) {
baseHeader['Content-Type'] = (settings.contentType || 'application/x-www-form-urlencoded; charset=UTF-8');
}
// headers
settings.headers = extend(baseHeader, settings.headers || {});
// on ready state change
xhr.onreadystatechange = function () {
// readystate
if (xhr.readyState === 4) {
clearTimeout(abortTimeout);
var result;
var error = false;
//
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
dataType = dataType || mimeToDataType(xhr.getResponseHeader('content-type'));
result = xhr.responseText;
try {
// xml
if (dataType === 'xml') {
result = xhr.responseXML;
}
// json
else if (dataType === 'json') {
result = blankRE.test(result) ? null : JSON.parse(result);
}
}
catch (e) {
error = e;
}
if (error) {
ajaxError(error, 'parseerror', xhr, settings);
}
else {
ajaxSuccess(result, xhr, settings);
}
}
else {
ajaxError(null, 'error', xhr, settings);
}
}
};
// async
var async = 'async' in settings ? settings.async : true;
// open
xhr.open(settings.type, settings.url, async, settings.username, settings.password);
// xhrFields
if (settings.xhrFields) {
for (var name in settings.xhrFields) {
xhr[name] = settings.xhrFields[name];
}
}
// Override mime type if needed
if (settings.mimeType && xhr.overrideMimeType) {
xhr.overrideMimeType(settings.mimeType);
}
// set request header
for (var name in settings.headers) {
// Support: IE<9
// IE's ActiveXObject throws a 'Type Mismatch' exception when setting
// request header to a null-value.
//
// To keep consistent with other XHR implementations, cast the value
// to string and ignore `undefined`.
if (settings.headers[name] !== undefined) {
xhr.setRequestHeader(name, settings.headers[name] + "");
}
}
// before send
if (ajaxBeforeSend(xhr, settings) === false) {
xhr.abort();
return false;
}
// timeout
if (settings.timeout > 0) {
abortTimeout = window.setTimeout(function () {
xhr.onreadystatechange = noop;
xhr.abort();
ajaxError(null, 'timeout', xhr, settings);
}, settings.timeout);
}
// send
xhr.send(settings.data ? settings.data : null);
return settings.promise;
};
/*
* method get
* */
ajax.get = function (url, data, success, dataType) {
if (isFunction(data)) {
dataType = dataType || success;
success = data;
data = undefined;
}
return ajax({
url: url,
data: data,
success: success,
dataType: dataType
});
};
/*
* method post
*
* dataType:
* */
ajax.post = function (url, data, success, dataType) {
if (isFunction(data)) {
dataType = dataType || success;
success = data;
data = undefined;
}
return ajax({
type: 'POST',
url: url,
data: data,
success: success,
dataType: dataType
})
};
/*
* method getJSON
* */
ajax.getJSON = function (url, data, success) {
if (isFunction(data)) {
success = data;
data = undefined;
}
return ajax({
url: url,
data: data,
success: success,
dataType: 'json'
})
};
/*
* method ajaxSetup
* */
ajax.ajaxSetup = function (target, settings) {
return settings ? extend(extend(target, _settings), settings) : extend(_settings, target);
};
/*
* utils
*
* */
// triggers and extra global event ajaxBeforeSend that's like ajaxSend but cancelable
function ajaxBeforeSend(xhr, settings) {
var context = settings.context;
//
if (settings.beforeSend.call(context, xhr, settings) === false) {
return false;
}
}
// ajax success
function ajaxSuccess(data, xhr, settings) {
var context = settings.context;
var status = 'success';
settings.success.call(context, data, status, xhr);
settings.promise.resolve(data, status, xhr);
ajaxComplete(status, xhr, settings);
}
// status: "success", "notmodified", "error", "timeout", "abort", "parsererror"
function ajaxComplete(status, xhr, settings) {
var context = settings.context;
settings.complete.call(context, xhr, status);
}
// type: "timeout", "error", "abort", "parsererror"
function ajaxError(error, type, xhr, settings) {
var context = settings.context;
settings.error.call(context, xhr, type, error);
settings.promise.reject(xhr, type, error);
ajaxComplete(type, xhr, settings);
}
// jsonp
/*
* tks: https://www.cnblogs.com/rubylouvre/archive/2011/02/13/1953087.html
* */
function JSONP(options) {
//
var callbackName = options.jsonpCallback || 'jsonp' + (new Date().getTime());
var script = window.document.createElement('script');
var abort = function () {
// 设置 window.xxx = noop
if (callbackName in window) {
window[callbackName] = noop;
}
};
var xhr = {abort: abort};
var abortTimeout;
var head = window.document.getElementsByTagName('head')[0] || window.document.documentElement;
// ie8+
script.onerror = function (error) {
_error(error);
};
function _error(error) {
window.clearTimeout(abortTimeout);
xhr.abort();
ajaxError(error.type, xhr, error.type, options);
_removeScript();
}
window[callbackName] = function (data) {
window.clearTimeout(abortTimeout);
ajaxSuccess(data, xhr, options);
_removeScript();
};
//
serializeData(options);
script.src = options.url.replace(/=\?/, '=' + callbackName);
//
script.src = appendQuery(script.src, '_=' + (new Date()).getTime());
//
script.async = true;
// script charset
if (options.scriptCharset) {
script.charset = options.scriptCharset;
}
//
head.insertBefore(script, head.firstChild);
//
if (options.timeout > 0) {
abortTimeout = window.setTimeout(function () {
xhr.abort();
ajaxError('timeout', xhr, 'timeout', options);
_removeScript();
}, options.timeout);
}
// remove script
function _removeScript() {
if (script.clearAttributes) {
script.clearAttributes();
} else {
script.onload = script.onreadystatechange = script.onerror = null;
}
if (script.parentNode) {
script.parentNode.removeChild(script);
}
//
script = null;
delete window[callbackName];
}
return options.promise;
}
// mime to data type
function mimeToDataType(mime) {
return mime && (mime === htmlType ? 'html' : mime === jsonType ? 'json' : xmlTypeRE.test(mime) && 'xml') || 'text'
}
// append query
function appendQuery(url, query) {
return (url + '&' + query).replace(/[&?]{1,2}/, '?');
}
// serialize data
function serializeData(options) {
// formData
if (isObject(options) && !isFormData(options.data) && options.processData) {
options.data = param(options.data);
}
if (options.data && (!options.type || options.type.toUpperCase() === 'GET')) {
options.url = appendQuery(options.url, options.data);
}
}
// serialize
function serialize(params, obj, traditional, scope) {
var _isArray = isArray(obj);
for (var key in obj) {
var value = obj[key];
if (scope) {
key = traditional ? scope : scope + '[' + (_isArray ? '' : key) + ']';
}
// handle data in serializeArray format
if (!scope && _isArray) {
params.add(value.name, value.value);
}
else if (traditional ? _isArray(value) : isObject(value)) {
serialize(params, value, traditional, key);
}
else {
params.add(key, value);
}
}
}
// param
function param(obj, traditional) {
var params = [];
//
params.add = function (k, v) {
this.push(encodeURIComponent(k) + '=' + encodeURIComponent(v));
};
serialize(params, obj, traditional);
return params.join('&').replace('%20', '+');
}
// extend
function extend(target) {
var slice = Array.prototype.slice;
var args = slice.call(arguments, 1);
//
for (var i = 0, length = args.length; i < length; i++) {
var source = args[i] || {};
for (var key in source) {
if (source.hasOwnProperty(key) && source[key] !== undefined) {
target[key] = source[key];
}
}
}
return target;
}
// is object
function isObject(obj) {
var type = typeof obj;
return type === 'function' || type === 'object' && !!obj;
}
// is formData
function isFormData(obj) {
return obj instanceof FormData;
}
// is array
function isArray(value) {
return Object.prototype.toString.call(value) === "[object Array]";
}
// is function
function isFunction(value) {
return typeof value === "function";
}
// browser
window.ajax = ajax;
})();

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>pm</title><link href=/css/app.74a1e2f4.css rel=preload as=style><link href=/css/chunk-vendors.22ebf426.css rel=preload as=style><link href=/js/app.4b08c1d1.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.74a1e2f4.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.4b08c1d1.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.13e2de5f.js rel=preload as=script><link href=/js/chunk-vendors.2e3b192a.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.2e3b192a.js></script><script src=/js/app.13e2de5f.js></script></body></html>

2
pm/dist/js/app.13e2de5f.js vendored Normal file

File diff suppressed because one or more lines are too long

1
pm/dist/js/app.13e2de5f.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" "@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": { "@intervolga/optimize-cssnano-plugin": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npm.taobao.org/@intervolga/optimize-cssnano-plugin/download/@intervolga/optimize-cssnano-plugin-1.0.6.tgz", "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" "lint": "vue-cli-service lint"
}, },
"dependencies": { "dependencies": {
"@iarna/toml": "^2.2.3",
"core-js": "^3.4.4", "core-js": "^3.4.4",
"view-design": "^4.0.0", "view-design": "^4.0.0",
"vue": "^2.6.10", "vue": "^2.6.10",
@@ -37,7 +38,7 @@
"plugin:vue/essential", "plugin:vue/essential",
"eslint:recommended" "eslint:recommended"
], ],
"rules": {}, "rules": {"no-console": "off"},
"parserOptions": { "parserOptions": {
"parser": "babel-eslint" "parser": "babel-eslint"
} }

535
pm/public/ajax.js Normal file
View File

@@ -0,0 +1,535 @@
// a simple ajax
!(function () {
var jsonType = 'application/json';
var htmlType = 'text/html';
var xmlTypeRE = /^(?:text|application)\/xml/i;
var blankRE = /^\s*$/; // \s
/*
* default setting
* */
var _settings = {
type: "GET",
beforeSend: noop,
success: noop,
error: noop,
complete: noop,
context: null,
xhr: function () {
return new window.XMLHttpRequest();
},
accepts: {
json: jsonType,
xml: 'application/xml, text/xml',
html: htmlType,
text: 'text/plain'
},
crossDomain: false,
timeout: 0,
username: null,
password: null,
processData: true,
promise: noop
};
function noop() {
}
var ajax = function (options) {
//
var settings = extend({}, options || {});
//
for (var key in _settings) {
if (settings[key] === undefined) {
settings[key] = _settings[key];
}
}
//
try {
var q = {};
var promise = new Promise(function (resolve, reject) {
q.resolve = resolve;
q.reject = reject;
});
promise.resolve = q.resolve;
promise.reject = q.reject;
settings.promise = promise;
}
catch (e) {
//
settings.promise = {
resolve: noop,
reject: noop
};
}
//
if (!settings.crossDomain) {
settings.crossDomain = /^([\w-]+:)?\/\/([^\/]+)/.test(settings.url) && RegExp.$2 !== window.location.href;
}
var dataType = settings.dataType;
// jsonp
if (dataType === 'jsonp') {
//
var hasPlaceholder = /=\?/.test(settings.url);
if (!hasPlaceholder) {
var jsonpCallback = (settings.jsonp || 'callback') + '=?';
settings.url = appendQuery(settings.url, jsonpCallback)
}
return JSONP(settings);
}
// url
if (!settings.url) {
settings.url = window.location.toString();
}
//
serializeData(settings);
var mime = settings.accepts[dataType]; // mime
var baseHeader = {}; // header
var protocol = /^([\w-]+:)\/\//.test(settings.url) ? RegExp.$1 : window.location.protocol; // protocol
var xhr = _settings.xhr();
var abortTimeout;
// X-Requested-With header
// For cross-domain requests, seeing as conditions for a preflight are
// akin to a jigsaw puzzle, we simply never set it to be sure.
// (it can always be set on a per-request basis or even using ajaxSetup)
// For same-domain requests, won't change header if already provided.
if (!settings.crossDomain && !baseHeader['X-Requested-With']) {
baseHeader['X-Requested-With'] = 'XMLHttpRequest';
}
// mime
if (mime) {
//
baseHeader['Accept'] = mime;
if (mime.indexOf(',') > -1) {
mime = mime.split(',', 2)[0]
}
//
xhr.overrideMimeType && xhr.overrideMimeType(mime);
}
// contentType
if (settings.contentType || (settings.data && settings.type.toUpperCase() !== 'GET')) {
baseHeader['Content-Type'] = (settings.contentType || 'application/x-www-form-urlencoded; charset=UTF-8');
}
// headers
settings.headers = extend(baseHeader, settings.headers || {});
// on ready state change
xhr.onreadystatechange = function () {
// readystate
if (xhr.readyState === 4) {
clearTimeout(abortTimeout);
var result;
var error = false;
//
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
dataType = dataType || mimeToDataType(xhr.getResponseHeader('content-type'));
result = xhr.responseText;
try {
// xml
if (dataType === 'xml') {
result = xhr.responseXML;
}
// json
else if (dataType === 'json') {
result = blankRE.test(result) ? null : JSON.parse(result);
}
}
catch (e) {
error = e;
}
if (error) {
ajaxError(error, 'parseerror', xhr, settings);
}
else {
ajaxSuccess(result, xhr, settings);
}
}
else {
ajaxError(null, 'error', xhr, settings);
}
}
};
// async
var async = 'async' in settings ? settings.async : true;
// open
xhr.open(settings.type, settings.url, async, settings.username, settings.password);
// xhrFields
if (settings.xhrFields) {
for (var name in settings.xhrFields) {
xhr[name] = settings.xhrFields[name];
}
}
// Override mime type if needed
if (settings.mimeType && xhr.overrideMimeType) {
xhr.overrideMimeType(settings.mimeType);
}
// set request header
for (var name in settings.headers) {
// Support: IE<9
// IE's ActiveXObject throws a 'Type Mismatch' exception when setting
// request header to a null-value.
//
// To keep consistent with other XHR implementations, cast the value
// to string and ignore `undefined`.
if (settings.headers[name] !== undefined) {
xhr.setRequestHeader(name, settings.headers[name] + "");
}
}
// before send
if (ajaxBeforeSend(xhr, settings) === false) {
xhr.abort();
return false;
}
// timeout
if (settings.timeout > 0) {
abortTimeout = window.setTimeout(function () {
xhr.onreadystatechange = noop;
xhr.abort();
ajaxError(null, 'timeout', xhr, settings);
}, settings.timeout);
}
// send
xhr.send(settings.data ? settings.data : null);
return settings.promise;
};
/*
* method get
* */
ajax.get = function (url, data, success, dataType) {
if (isFunction(data)) {
dataType = dataType || success;
success = data;
data = undefined;
}
return ajax({
url: url,
data: data,
success: success,
dataType: dataType
});
};
/*
* method post
*
* dataType:
* */
ajax.post = function (url, data, success, dataType) {
if (isFunction(data)) {
dataType = dataType || success;
success = data;
data = undefined;
}
return ajax({
type: 'POST',
url: url,
data: data,
success: success,
dataType: dataType
})
};
/*
* method getJSON
* */
ajax.getJSON = function (url, data, success) {
if (isFunction(data)) {
success = data;
data = undefined;
}
return ajax({
url: url,
data: data,
success: success,
dataType: 'json'
})
};
/*
* method ajaxSetup
* */
ajax.ajaxSetup = function (target, settings) {
return settings ? extend(extend(target, _settings), settings) : extend(_settings, target);
};
/*
* utils
*
* */
// triggers and extra global event ajaxBeforeSend that's like ajaxSend but cancelable
function ajaxBeforeSend(xhr, settings) {
var context = settings.context;
//
if (settings.beforeSend.call(context, xhr, settings) === false) {
return false;
}
}
// ajax success
function ajaxSuccess(data, xhr, settings) {
var context = settings.context;
var status = 'success';
settings.success.call(context, data, status, xhr);
settings.promise.resolve(data, status, xhr);
ajaxComplete(status, xhr, settings);
}
// status: "success", "notmodified", "error", "timeout", "abort", "parsererror"
function ajaxComplete(status, xhr, settings) {
var context = settings.context;
settings.complete.call(context, xhr, status);
}
// type: "timeout", "error", "abort", "parsererror"
function ajaxError(error, type, xhr, settings) {
var context = settings.context;
settings.error.call(context, xhr, type, error);
settings.promise.reject(xhr, type, error);
ajaxComplete(type, xhr, settings);
}
// jsonp
/*
* tks: https://www.cnblogs.com/rubylouvre/archive/2011/02/13/1953087.html
* */
function JSONP(options) {
//
var callbackName = options.jsonpCallback || 'jsonp' + (new Date().getTime());
var script = window.document.createElement('script');
var abort = function () {
// 设置 window.xxx = noop
if (callbackName in window) {
window[callbackName] = noop;
}
};
var xhr = {abort: abort};
var abortTimeout;
var head = window.document.getElementsByTagName('head')[0] || window.document.documentElement;
// ie8+
script.onerror = function (error) {
_error(error);
};
function _error(error) {
window.clearTimeout(abortTimeout);
xhr.abort();
ajaxError(error.type, xhr, error.type, options);
_removeScript();
}
window[callbackName] = function (data) {
window.clearTimeout(abortTimeout);
ajaxSuccess(data, xhr, options);
_removeScript();
};
//
serializeData(options);
script.src = options.url.replace(/=\?/, '=' + callbackName);
//
script.src = appendQuery(script.src, '_=' + (new Date()).getTime());
//
script.async = true;
// script charset
if (options.scriptCharset) {
script.charset = options.scriptCharset;
}
//
head.insertBefore(script, head.firstChild);
//
if (options.timeout > 0) {
abortTimeout = window.setTimeout(function () {
xhr.abort();
ajaxError('timeout', xhr, 'timeout', options);
_removeScript();
}, options.timeout);
}
// remove script
function _removeScript() {
if (script.clearAttributes) {
script.clearAttributes();
} else {
script.onload = script.onreadystatechange = script.onerror = null;
}
if (script.parentNode) {
script.parentNode.removeChild(script);
}
//
script = null;
delete window[callbackName];
}
return options.promise;
}
// mime to data type
function mimeToDataType(mime) {
return mime && (mime === htmlType ? 'html' : mime === jsonType ? 'json' : xmlTypeRE.test(mime) && 'xml') || 'text'
}
// append query
function appendQuery(url, query) {
return (url + '&' + query).replace(/[&?]{1,2}/, '?');
}
// serialize data
function serializeData(options) {
// formData
if (isObject(options) && !isFormData(options.data) && options.processData) {
options.data = param(options.data);
}
if (options.data && (!options.type || options.type.toUpperCase() === 'GET')) {
options.url = appendQuery(options.url, options.data);
}
}
// serialize
function serialize(params, obj, traditional, scope) {
var _isArray = isArray(obj);
for (var key in obj) {
var value = obj[key];
if (scope) {
key = traditional ? scope : scope + '[' + (_isArray ? '' : key) + ']';
}
// handle data in serializeArray format
if (!scope && _isArray) {
params.add(value.name, value.value);
}
else if (traditional ? _isArray(value) : isObject(value)) {
serialize(params, value, traditional, key);
}
else {
params.add(key, value);
}
}
}
// param
function param(obj, traditional) {
var params = [];
//
params.add = function (k, v) {
this.push(encodeURIComponent(k) + '=' + encodeURIComponent(v));
};
serialize(params, obj, traditional);
return params.join('&').replace('%20', '+');
}
// extend
function extend(target) {
var slice = Array.prototype.slice;
var args = slice.call(arguments, 1);
//
for (var i = 0, length = args.length; i < length; i++) {
var source = args[i] || {};
for (var key in source) {
if (source.hasOwnProperty(key) && source[key] !== undefined) {
target[key] = source[key];
}
}
}
return target;
}
// is object
function isObject(obj) {
var type = typeof obj;
return type === 'function' || type === 'object' && !!obj;
}
// is formData
function isFormData(obj) {
return obj instanceof FormData;
}
// is array
function isArray(value) {
return Object.prototype.toString.call(value) === "[object Array]";
}
// is function
function isFunction(value) {
return typeof value === "function";
}
// browser
window.ajax = ajax;
})();

View File

@@ -5,7 +5,8 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> <link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>pm</title> <title>Monibuca Instance Manager</title>
<script src="ajax.js"></script>
</head> </head>
<body> <body>
<noscript> <noscript>

View File

@@ -1,5 +1,5 @@
<template> <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"> <Steps :current="currentStep" size="small" :status="status">
<Step title="解析请求"></Step> <Step title="解析请求"></Step>
<Step title="创建目录"></Step> <Step title="创建目录"></Step>
@@ -12,46 +12,54 @@
<div> <div>
<pre>{{log}}</pre> <pre>{{log}}</pre>
</div> </div>
<div slot="footer">
<Checkbox v-model="clearDir">安装前清空目录</Checkbox>
<Button type="primary" @click="start" :loading="status=='process'">开始</Button>
</div>
</Modal> </Modal>
</template> </template>
<script> <script>
let eventSource = null let eventSource = null;
export default { export default {
name: "CreateInstance", name: "CreateInstance",
props: { props: {
info: Object, info: Object
}, },
watch: { methods: {
info(v) { start() {
if (v) { this.status = "process";
eventSource = new EventSource("/create?info="+JSON.stringify(v)) eventSource = new EventSource(
eventSource.onmessage = evt => { "/instance/create?info=" +
this.log += evt.data + "\n" JSON.stringify(this.info) +
if (evt.data == "success") { (this.clearDir ? "&clear=true" : "")
this.status = "finish" );
eventSource.close() eventSource.onopen = () => (this.log = "");
} eventSource.onmessage = evt => {
} this.log += evt.data + "\n";
eventSource.addEventListener("exception", evt => { if (evt.data == "success") {
this.log += evt.data + "\n" this.status = "finish";
this.status = "error" eventSource.close();
eventSource.close()
})
eventSource.addEventListener("step", evt => {
let [step,msg] = evt.data.split(":")
this.currentStep = step|0
this.log+=msg+"\n"
})
} }
} };
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 {currentStep: 0, log: "", status: "process"}
} data() {
return { clearDir: true, currentStep: 0, log: "", status: "wait" };
} }
};
</script> </script>
<style scoped> <style scoped>
</style> </style>

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,47 @@
<template>
<div>
<PathSelector v-model="instancePath" placeholder="输入实例所在的路径"></PathSelector>
<i-input style="width: 300px;margin:40px auto" v-model="instanceName" :placeholder="defaultInstanceName" search enter-button="Import" @on-search="doImport">
<span slot="prepend">实例名称</span>
</i-input>
</div>
</template>
<script>
import PathSelector from "./PathSelector"
export default {
name: "ImportInstance",
components:{
PathSelector
},
data(){
return {
instancePath:"",
instanceName:""
}
},
computed:{
defaultInstanceName(){
let path = this.instancePath.replace(/\\/g,"/")
let s = path.split("/")
if(path.endsWith("/")) s.pop()
return s.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,66 @@
<template>
<div>
<i-input ref="input" v-bind="$attrs" v-on="$listeners" clearable @on-change="onInput">
<Button slot="prepend" icon="md-arrow-round-up" @click="goUp"></Button>
</i-input>
<CellGroup @on-click="onSelectCand">
<Cell v-for="item in candidate" :key="item" :title="item" :name="item"></Cell>
</CellGroup>
</div>
</template>
<script>
export default {
name: "PathSelector",
data() {
return {
candidate: [],
lastInput: "",
searching: false,
}
},
methods: {
dir(){
let paths = this.$refs.input.value.split("/");
paths.pop();
return paths.join("/");
},
goUp() {
this.lastInput = this.$attrs.value = this.dir()
this.$refs.input.$emit('input', this.$attrs.value)
this.search(this.lastInput)
},
onSelectCand(name) {
this.lastInput = this.$attrs.value = this.dir()+"/"+name+"/"
this.$refs.input.$emit('input', this.$attrs.value)
this.search(this.lastInput)
},
onInput(evt) {
this.lastInput = evt.target.value
this.search(this.lastInput)
},
search(v) {
if(this.searching)return
window.ajax.getJSON("/instance/listDir?input=" + v).then(x => {
this.candidate = x
if (this.lastInput != v) {
this.search(this.lastInput)
}else{
this.searching = false
}
}).catch(e => {
this.$Message.error(e)
if (this.lastInput != v) {
this.search(this.lastInput)
}else{
this.searching = false
}
})
}
}
}
</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 Vue from 'vue'
import Vuex from 'vuex' import Vuex from 'vuex'
Vue.use(Vuex) Vue.use(Vuex)
export default new Vuex.Store({ export default new Vuex.Store({
state: { 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: { 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"> <Content class="content">
<Tabs value="name1"> <Tabs value="name1">
<TabPane label="实例" name="name1"> <TabPane label="实例" name="name1">
<List border> <InstanceList></InstanceList>
<ListItem v-for="item in instances" :key="item.Name">
<ListItemMeta :title="item.Name" :description="item.Path"></ListItemMeta>
</ListItem>
</List>
</TabPane> </TabPane>
<TabPane label="创建" name="name2"> <TabPane label="创建" name="name2">
<Steps :current="createStep"> <Steps :current="createStep">
@@ -17,24 +13,24 @@
<Step title="完成" content="完成实例创建"></Step> <Step title="完成" content="完成实例创建"></Step>
</Steps> </Steps>
<div style="margin:50px;width:auto"> <div style="margin:50px;width:auto">
<i-input v-model="createPath" v-if="createStep==0"> <PathSelector v-model="createPath" v-if="createStep==0"></PathSelector>
<Button slot="prepend" icon="md-arrow-round-up" @click="goUp"></Button> <div style="display: flex;flex-wrap: wrap" v-else-if="createStep==1">
</i-input> <Card v-for="(item,name) in plugins" :key="name" style="width:200px;margin:5px">
<List v-else-if="createStep==1" border> <Poptip :content="item.Description" slot="extra" width="200" word-wrap>
<ListItem v-for="(item,name) in plugins" :key="name"> <Icon size="18" type="ios-help-circle-outline" style="cursor:pointer"/>
<ListItemMeta :title="name" :description="item.Path"></ListItemMeta> </Poptip>
{{item.Config}} <Poptip :content="item.Path" trigger="hover" word-wrap slot="title">
<template slot="action"> <Checkbox v-model="item.enabled" style="color: #eb5e46">{{name}}</Checkbox>
<li @click="removePlugin(name)"> </Poptip>
<Icon type="ios-trash"/> <i-input type="textarea" v-model="item.Config" placeholder="请输入toml格式"></i-input>
移除 </Card>
</li> </div>
</template>
</ListItem>
</List>
<div v-else> <div v-else>
<h3>实例名称</h3> <h3>实例名称</h3>
<i-input v-model="instanceName" :placeholder="createPath.split('/').pop()"></i-input> <i-input
v-model="instanceName"
:placeholder="createPath.split('/').pop()"
></i-input>
<h4>安装路径</h4> <h4>安装路径</h4>
<div> <div>
<pre>{{createPath}}</pre> <pre>{{createPath}}</pre>
@@ -49,14 +45,31 @@
</div> </div>
</div> </div>
<ButtonGroup style="display:table;margin:50px auto;"> <ButtonGroup style="display:table;margin:50px auto;">
<Button size="large" type="primary" @click="createStep--" v-if="createStep!=0"> <Button
size="large"
type="primary"
@click="createStep--"
v-if="createStep!=0"
>
<Icon type="ios-arrow-back"></Icon> <Icon type="ios-arrow-back"></Icon>
上一步 上一步
</Button> </Button>
<Button size="large" type="success" @click="showAddPlugin=true" v-if="createStep==1">+ <Button
size="large"
type="success"
@click="showAddPlugin=true"
v-if="createStep==1"
>
+
添加插件 添加插件
</Button> </Button>
<Button size="large" type="primary" @click="createStep++" v-if="createStep!=2">下一步 <Button
size="large"
type="primary"
@click="createStep++"
v-if="createStep!=2"
>
下一步
<Icon type="ios-arrow-forward"></Icon> <Icon type="ios-arrow-forward"></Icon>
</Button> </Button>
<Button size="large" type="success" @click="createInstance" v-else>开始创建</Button> <Button size="large" type="success" @click="createInstance" v-else>开始创建</Button>
@@ -64,7 +77,7 @@
</div> </div>
</TabPane> </TabPane>
<TabPane label="导入" name="name3"> <TabPane label="导入" name="name3">
<ImportInstance></ImportInstance>
</TabPane> </TabPane>
</Tabs> </Tabs>
</Content> </Content>
@@ -74,11 +87,9 @@
<i-input v-model="formPlugin.Name" placeholder="插件名称必须和插件注册时的名称一致"></i-input> <i-input v-model="formPlugin.Name" placeholder="插件名称必须和插件注册时的名称一致"></i-input>
</FormItem> </FormItem>
<FormItem label="插件包地址"> <FormItem label="插件包地址">
<i-input v-model="formPlugin.Path"> <i-input v-model="formPlugin.Path"></i-input>
<Button slot="append" @click="showBuiltinPlugin=true">内置插件</Button>
</i-input>
</FormItem> </FormItem>
<Alert type="show-icon" v-if="!Object.values(builtinPlugins).includes(formPlugin.Path)"> <Alert show-icon type="warning">
如果该插件是私有仓库请到服务器上输入echo "machine {{privateHost}} login 用户名 password 密码" >> ~/.netrc 如果该插件是私有仓库请到服务器上输入echo "machine {{privateHost}} login 用户名 password 密码" >> ~/.netrc
并且添加环境变量GOPRIVATE={{privateHost}} 并且添加环境变量GOPRIVATE={{privateHost}}
</Alert> </Alert>
@@ -87,112 +98,90 @@
</FormItem> </FormItem>
</Form> </Form>
</Modal> </Modal>
<Modal v-model="showBuiltinPlugin">
<List>
<ListItem v-for="(item,name) in builtinPlugins" :key="name">
<ListItemMeta :title="name" :description="item"></ListItemMeta>
<template slot="action">
<li @click="addBuiltin(name,item)">
<Icon type="ios-add"/>
添加
</li>
</template>
</ListItem>
</List>
</Modal>
<CreateInstance v-model="showCreate" :info="createInfo"></CreateInstance> <CreateInstance v-model="showCreate" :info="createInfo"></CreateInstance>
</Layout> </Layout>
</template> </template>
<script> <script>
import CreateInstance from "../components/CreateInstance" import CreateInstance from "../components/CreateInstance";
import InstanceList from "../components/InstanceList";
import ImportInstance from "../components/ImportInstance";
import PathSelector from "../components/PathSelector"
export default { export default {
components: { components: {
CreateInstance CreateInstance, InstanceList, ImportInstance, PathSelector
}, },
data() { data() {
let plugins = {}
for (let name in this.$store.state.defaultPlugins) {
plugins[name] = {
Name: name,
enabled: ["GateWay", "LogRotate", "Jessica"].includes(name),
Path: "github.com/langhuihui/monibuca/plugins/" + this.$store.state.defaultPlugins[name][0],
Config: this.$store.state.defaultPlugins[name][1],
Description: this.$store.state.defaultPlugins[name][2],
}
}
return { return {
instanceName: "", instanceName: "",
createStep: 0, createStep: 0,
showCreate: false, showCreate: false,
createInfo: null, createInfo: null,
createPath: "/opt/monibuca", createPath: "/opt/monibuca",
instances: [], plugins,
plugins: {},
showAddPlugin: false, showAddPlugin: false,
formPlugin: {}, 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: { computed: {
pluginStr() { pluginStr() {
return Object.values(this.plugins).map(x => x.Path).join("\n") return Object.values(this.plugins).filter(x => x.enabled)
.map(x => x.Path)
.join("\n");
}, },
configStr() { configStr() {
return Object.values(this.plugins).map(x => `[Plugins.${x.Name}] return Object.values(this.plugins).filter(x => x.enabled)
${x.Config || ""}`).join("\n") .map(
x => `[Plugins.${x.Name}]
${x.Config || ""}`
)
.join("\n");
}, },
privateHost(){ privateHost() {
return (this.formPlugin.Path && this.formPlugin.Path.split("/")[0])||"仓库域名" return (
(this.formPlugin.Path && this.formPlugin.Path.split("/")[0]) ||
"仓库域名"
);
} }
}, },
methods: { methods: {
goUp() { goUp() {
let paths = this.createPath.split("/") let paths = this.createPath.split("/");
paths.pop() paths.pop();
this.createPath = paths.join("/") this.createPath = paths.join("/");
}, },
createInstance() { createInstance() {
this.showCreate = true this.showCreate = true;
this.createInfo = { this.createInfo = {
Name: this.instanceName || this.createPath.split('/').pop(), Name: this.instanceName || this.createPath.split("/").pop(),
Path: this.createPath, Path: this.createPath,
Plugins: Object.values(this.plugins).map(x => x.Path), Plugins: Object.values(this.plugins).filter(x => x.enabled).map(x => x.Path),
Config: this.configStr Config: this.configStr
} };
}, },
addPlugin() { addPlugin() {
this.plugins[this.formPlugin.Name] = this.formPlugin this.plugins[this.formPlugin.Name] = this.formPlugin;
this.formPlugin = {} this.formPlugin = {};
}, },
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
}
} }
} };
</script> </script>
<style> <style>
.content { .content {
background: white background: white;
} }
pre { pre {