#218 and WebSocket bug fix
This commit is contained in:
parent
b758ec854e
commit
a119370a23
9
main.go
9
main.go
|
@ -19,13 +19,11 @@ import (
|
||||||
"flag"
|
"flag"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"math/rand"
|
|
||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
_ "net/http/pprof"
|
_ "net/http/pprof"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -222,11 +220,6 @@ func indexHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
httpSession.Save(r, w)
|
httpSession.Save(r, w)
|
||||||
|
|
||||||
// create a Wide session
|
|
||||||
rand.Seed(time.Now().UnixNano())
|
|
||||||
sid := strconv.Itoa(rand.Int())
|
|
||||||
wideSession := session.WideSessions.New(httpSession, sid)
|
|
||||||
|
|
||||||
user := conf.GetUser(username)
|
user := conf.GetUser(username)
|
||||||
if nil == user {
|
if nil == user {
|
||||||
logger.Warnf("Not found user [%s]", username)
|
logger.Warnf("Not found user [%s]", username)
|
||||||
|
@ -241,7 +234,7 @@ func indexHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
wideSessions := session.WideSessions.GetByUsername(username)
|
wideSessions := session.WideSessions.GetByUsername(username)
|
||||||
|
|
||||||
model := map[string]interface{}{"conf": conf.Wide, "i18n": i18n.GetAll(locale), "locale": locale,
|
model := map[string]interface{}{"conf": conf.Wide, "i18n": i18n.GetAll(locale), "locale": locale,
|
||||||
"session": wideSession, "latestSessionContent": user.LatestSessionContent,
|
"username": username, "sid": session.WideSessions.GenId(), "latestSessionContent": user.LatestSessionContent,
|
||||||
"pathSeparator": conf.PathSeparator, "codeMirrorVer": conf.CodeMirrorVer,
|
"pathSeparator": conf.PathSeparator, "codeMirrorVer": conf.CodeMirrorVer,
|
||||||
"user": user, "editorThemes": conf.GetEditorThemes(), "crossPlatforms": util.Go.GetCrossPlatforms()}
|
"user": user, "editorThemes": conf.GetEditorThemes(), "crossPlatforms": util.Go.GetCrossPlatforms()}
|
||||||
|
|
||||||
|
|
|
@ -54,11 +54,6 @@ func IndexHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
username := httpSession.Values["username"].(string)
|
username := httpSession.Values["username"].(string)
|
||||||
|
|
||||||
// create a wide session
|
|
||||||
rand.Seed(time.Now().UnixNano())
|
|
||||||
sid := strconv.Itoa(rand.Int())
|
|
||||||
wideSession := session.WideSessions.New(httpSession, sid)
|
|
||||||
|
|
||||||
locale := conf.Wide.Locale
|
locale := conf.Wide.Locale
|
||||||
|
|
||||||
// try to load file
|
// try to load file
|
||||||
|
@ -92,8 +87,9 @@ func IndexHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
model := map[string]interface{}{"conf": conf.Wide, "i18n": i18n.GetAll(locale), "locale": locale,
|
model := map[string]interface{}{"conf": conf.Wide, "i18n": i18n.GetAll(locale), "locale": locale,
|
||||||
"session": wideSession, "pathSeparator": conf.PathSeparator, "codeMirrorVer": conf.CodeMirrorVer,
|
"sid": session.WideSessions.GenId(), "pathSeparator": conf.PathSeparator,
|
||||||
"code": template.HTML(code), "ver": conf.WideVersion, "year": time.Now().Year(),
|
"codeMirrorVer": conf.CodeMirrorVer,
|
||||||
|
"code": template.HTML(code), "ver": conf.WideVersion, "year": time.Now().Year(),
|
||||||
"embed": embed, "disqus": disqus, "fileName": fileName}
|
"embed": embed, "disqus": disqus, "fileName": fileName}
|
||||||
|
|
||||||
wideSessions := session.WideSessions.GetByUsername(username)
|
wideSessions := session.WideSessions.GetByUsername(username)
|
||||||
|
|
|
@ -25,6 +25,7 @@ package session
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -193,21 +194,6 @@ func (f userReports) Less(i, j int) bool { return f[i].processCnt > f[j].process
|
||||||
// When a channel closed, releases all resources associated with it.
|
// When a channel closed, releases all resources associated with it.
|
||||||
func WSHandler(w http.ResponseWriter, r *http.Request) {
|
func WSHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
sid := r.URL.Query()["sid"][0]
|
sid := r.URL.Query()["sid"][0]
|
||||||
wSession := WideSessions.Get(sid)
|
|
||||||
if nil == wSession {
|
|
||||||
httpSession, _ := HTTPSession.Get(r, "wide-session")
|
|
||||||
|
|
||||||
if httpSession.IsNew {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
httpSession.Options.MaxAge = conf.Wide.HTTPSessionMaxAge
|
|
||||||
httpSession.Save(r, w)
|
|
||||||
|
|
||||||
wSession = WideSessions.New(httpSession, sid)
|
|
||||||
|
|
||||||
logger.Tracef("Created a wide session [%s] for websocket reconnecting, user [%s]", sid, wSession.Username)
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, _ := websocket.Upgrade(w, r, nil, 1024, 1024)
|
conn, _ := websocket.Upgrade(w, r, nil, 1024, 1024)
|
||||||
wsChan := util.WSChannel{Sid: sid, Conn: conn, Request: r, Time: time.Now()}
|
wsChan := util.WSChannel{Sid: sid, Conn: conn, Request: r, Time: time.Now()}
|
||||||
|
@ -220,6 +206,22 @@ func WSHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
SessionWS[sid] = &wsChan
|
SessionWS[sid] = &wsChan
|
||||||
|
|
||||||
|
wSession := WideSessions.Get(sid)
|
||||||
|
if nil == wSession {
|
||||||
|
httpSession, _ := HTTPSession.Get(r, "wide-session")
|
||||||
|
|
||||||
|
if httpSession.IsNew {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
httpSession.Options.MaxAge = conf.Wide.HTTPSessionMaxAge
|
||||||
|
httpSession.Save(r, w)
|
||||||
|
|
||||||
|
wSession = WideSessions.new(httpSession, sid)
|
||||||
|
|
||||||
|
logger.Tracef("Created a wide session [%s] for websocket reconnecting, user [%s]", sid, wSession.Username)
|
||||||
|
}
|
||||||
|
|
||||||
logger.Tracef("Open a new [Session Channel] with session [%s], %d", sid, len(SessionWS))
|
logger.Tracef("Open a new [Session Channel] with session [%s], %d", sid, len(SessionWS))
|
||||||
|
|
||||||
input := map[string]interface{}{}
|
input := map[string]interface{}{}
|
||||||
|
@ -297,114 +299,11 @@ func (s *WideSession) Refresh() {
|
||||||
s.Updated = time.Now()
|
s.Updated = time.Now()
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a wide session.
|
// GenId generates a wide session id.
|
||||||
func (sessions *wSessions) New(httpSession *sessions.Session, sid string) *WideSession {
|
func (sessions *wSessions) GenId() string {
|
||||||
mutex.Lock()
|
rand.Seed(time.Now().UnixNano())
|
||||||
defer mutex.Unlock()
|
|
||||||
|
|
||||||
username := httpSession.Values["username"].(string)
|
return strconv.Itoa(rand.Int())
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
ret := &WideSession{
|
|
||||||
ID: sid,
|
|
||||||
Username: username,
|
|
||||||
HTTPSession: httpSession,
|
|
||||||
EventQueue: nil,
|
|
||||||
State: sessionStateActive,
|
|
||||||
Content: &conf.LatestSessionContent{},
|
|
||||||
Created: now,
|
|
||||||
Updated: now,
|
|
||||||
}
|
|
||||||
|
|
||||||
*sessions = append(*sessions, ret)
|
|
||||||
|
|
||||||
if "playground" == username {
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
// create user event queue
|
|
||||||
ret.EventQueue = event.UserEventQueues.New(sid)
|
|
||||||
|
|
||||||
// add a filesystem watcher to notify front-end after the files changed
|
|
||||||
watcher, err := fsnotify.NewWatcher()
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(err)
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer util.Recover()
|
|
||||||
|
|
||||||
for {
|
|
||||||
ch := SessionWS[sid]
|
|
||||||
if nil == ch {
|
|
||||||
return // release this gorutine
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case event := <-watcher.Events:
|
|
||||||
path := event.Name
|
|
||||||
dir := filepath.Dir(path)
|
|
||||||
|
|
||||||
ch = SessionWS[sid]
|
|
||||||
if nil == ch {
|
|
||||||
return // release this gorutine
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debug(event)
|
|
||||||
|
|
||||||
if event.Op&fsnotify.Create == fsnotify.Create {
|
|
||||||
if err = watcher.Add(path); nil != err {
|
|
||||||
logger.Warn(err, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Tracef("File watcher added a file [%s]", path)
|
|
||||||
|
|
||||||
cmd := map[string]interface{}{"path": path, "dir": dir, "cmd": "create-file"}
|
|
||||||
ch.WriteJSON(&cmd)
|
|
||||||
} else if event.Op&fsnotify.Remove == fsnotify.Remove {
|
|
||||||
cmd := map[string]interface{}{"path": path, "dir": dir, "cmd": "remove-file"}
|
|
||||||
ch.WriteJSON(&cmd)
|
|
||||||
|
|
||||||
} else if event.Op&fsnotify.Rename == fsnotify.Rename {
|
|
||||||
cmd := map[string]interface{}{"path": path, "dir": dir, "cmd": "rename-file"}
|
|
||||||
ch.WriteJSON(&cmd)
|
|
||||||
}
|
|
||||||
case err := <-watcher.Errors:
|
|
||||||
if nil != err {
|
|
||||||
logger.Error("File watcher ERROR: ", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer util.Recover()
|
|
||||||
|
|
||||||
workspaces := filepath.SplitList(conf.GetUserWorkspace(username))
|
|
||||||
for _, workspace := range workspaces {
|
|
||||||
filepath.Walk(filepath.Join(workspace, "src"), func(dirPath string, f os.FileInfo, err error) error {
|
|
||||||
if ".git" == f.Name() { // XXX: discard other unconcered dirs
|
|
||||||
return filepath.SkipDir
|
|
||||||
}
|
|
||||||
|
|
||||||
if f.IsDir() {
|
|
||||||
if err = watcher.Add(dirPath); nil != err {
|
|
||||||
logger.Error(err, dirPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Tracef("File watcher added a dir [%s]", dirPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
ret.FileWatcher = watcher
|
|
||||||
}()
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get gets a wide session with the specified session id.
|
// Get gets a wide session with the specified session id.
|
||||||
|
@ -505,3 +404,111 @@ func (sessions *wSessions) GetByUsername(username string) []*WideSession {
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// new creates a wide session.
|
||||||
|
func (sessions *wSessions) new(httpSession *sessions.Session, sid string) *WideSession {
|
||||||
|
mutex.Lock()
|
||||||
|
defer mutex.Unlock()
|
||||||
|
|
||||||
|
username := httpSession.Values["username"].(string)
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
ret := &WideSession{
|
||||||
|
ID: sid,
|
||||||
|
Username: username,
|
||||||
|
HTTPSession: httpSession,
|
||||||
|
EventQueue: nil,
|
||||||
|
State: sessionStateActive,
|
||||||
|
Content: &conf.LatestSessionContent{},
|
||||||
|
Created: now,
|
||||||
|
Updated: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
*sessions = append(*sessions, ret)
|
||||||
|
|
||||||
|
if "playground" == username {
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// create user event queue
|
||||||
|
ret.EventQueue = event.UserEventQueues.New(sid)
|
||||||
|
|
||||||
|
// add a filesystem watcher to notify front-end after the files changed
|
||||||
|
watcher, err := fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer util.Recover()
|
||||||
|
|
||||||
|
for {
|
||||||
|
ch := SessionWS[sid]
|
||||||
|
if nil == ch {
|
||||||
|
return // release this gorutine
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case event := <-watcher.Events:
|
||||||
|
path := event.Name
|
||||||
|
dir := filepath.Dir(path)
|
||||||
|
|
||||||
|
ch = SessionWS[sid]
|
||||||
|
if nil == ch {
|
||||||
|
return // release this gorutine
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Trace(event)
|
||||||
|
|
||||||
|
if event.Op&fsnotify.Create == fsnotify.Create {
|
||||||
|
if err = watcher.Add(path); nil != err {
|
||||||
|
logger.Warn(err, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := map[string]interface{}{"path": path, "dir": dir, "cmd": "create-file"}
|
||||||
|
ch.WriteJSON(&cmd)
|
||||||
|
} else if event.Op&fsnotify.Remove == fsnotify.Remove {
|
||||||
|
cmd := map[string]interface{}{"path": path, "dir": dir, "cmd": "remove-file"}
|
||||||
|
ch.WriteJSON(&cmd)
|
||||||
|
|
||||||
|
} else if event.Op&fsnotify.Rename == fsnotify.Rename {
|
||||||
|
cmd := map[string]interface{}{"path": path, "dir": dir, "cmd": "rename-file"}
|
||||||
|
ch.WriteJSON(&cmd)
|
||||||
|
}
|
||||||
|
case err := <-watcher.Errors:
|
||||||
|
if nil != err {
|
||||||
|
logger.Error("File watcher ERROR: ", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer util.Recover()
|
||||||
|
|
||||||
|
workspaces := filepath.SplitList(conf.GetUserWorkspace(username))
|
||||||
|
for _, workspace := range workspaces {
|
||||||
|
filepath.Walk(filepath.Join(workspace, "src"), func(dirPath string, f os.FileInfo, err error) error {
|
||||||
|
if ".git" == f.Name() { // XXX: discard other unconcered dirs
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.IsDir() {
|
||||||
|
if err = watcher.Add(dirPath); nil != err {
|
||||||
|
logger.Error(err, dirPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Tracef("File watcher added a dir [%s]", dirPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.FileWatcher = watcher
|
||||||
|
}()
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
|
@ -17,12 +17,10 @@ package shell
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"html/template"
|
"html/template"
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -57,16 +55,11 @@ func IndexHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
httpSession.Save(r, w)
|
httpSession.Save(r, w)
|
||||||
|
|
||||||
// create a wide session
|
|
||||||
rand.Seed(time.Now().UnixNano())
|
|
||||||
sid := strconv.Itoa(rand.Int())
|
|
||||||
wideSession := session.WideSessions.New(httpSession, sid)
|
|
||||||
|
|
||||||
username := httpSession.Values["username"].(string)
|
username := httpSession.Values["username"].(string)
|
||||||
locale := conf.GetUser(username).Locale
|
locale := conf.GetUser(username).Locale
|
||||||
|
|
||||||
model := map[string]interface{}{"conf": conf.Wide, "i18n": i18n.GetAll(locale), "locale": locale,
|
model := map[string]interface{}{"conf": conf.Wide, "i18n": i18n.GetAll(locale), "locale": locale,
|
||||||
"session": wideSession}
|
"sid": session.WideSessions.GenId()}
|
||||||
|
|
||||||
wideSessions := session.WideSessions.GetByUsername(username)
|
wideSessions := session.WideSessions.GetByUsername(username)
|
||||||
|
|
||||||
|
|
|
@ -55,7 +55,7 @@ var notification = {
|
||||||
};
|
};
|
||||||
|
|
||||||
notificationWS.onerror = function (e) {
|
notificationWS.onerror = function (e) {
|
||||||
console.log('[notification onerror] ' + JSON.parse(e));
|
console.log('[notification onerror]');
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
|
@ -186,7 +186,7 @@ var session = {
|
||||||
$(".notification-count").show();
|
$(".notification-count").show();
|
||||||
};
|
};
|
||||||
sessionWS.onerror = function (e) {
|
sessionWS.onerror = function (e) {
|
||||||
console.log('[session onerror] ' + JSON.parse(e));
|
console.log('[session onerror]');
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
|
@ -540,7 +540,7 @@ var wide = {
|
||||||
console.log('[output onclose] disconnected (' + e.code + ')');
|
console.log('[output onclose] disconnected (' + e.code + ')');
|
||||||
};
|
};
|
||||||
outputWS.onerror = function (e) {
|
outputWS.onerror = function (e) {
|
||||||
console.log('[output onerror] ' + e);
|
console.log('[output onerror]');
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
_initFooter: function () {
|
_initFooter: function () {
|
||||||
|
@ -812,8 +812,8 @@ $(document).ready(function () {
|
||||||
tree.init();
|
tree.init();
|
||||||
menu.init();
|
menu.init();
|
||||||
hotkeys.init();
|
hotkeys.init();
|
||||||
notification.init();
|
|
||||||
session.init();
|
session.init();
|
||||||
|
notification.init();
|
||||||
editors.init();
|
editors.init();
|
||||||
windows.init();
|
windows.init();
|
||||||
bottomGroup.init();
|
bottomGroup.init();
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
|
|
||||||
<link id="themesLink" rel="stylesheet" href="{{.conf.StaticServer}}/static/css/themes/{{.user.Theme}}.css?{{.conf.StaticResourceVersion}}">
|
<link id="themesLink" rel="stylesheet" href="{{.conf.StaticServer}}/static/css/themes/{{.user.Theme}}.css?{{.conf.StaticResourceVersion}}">
|
||||||
|
|
||||||
<link rel="stylesheet" href="{{.conf.StaticServer}}/static/user/{{.session.Username}}/style.css?{{.conf.StaticResourceVersion}}">
|
<link rel="stylesheet" href="{{.conf.StaticServer}}/static/user/{{.username}}/style.css?{{.conf.StaticResourceVersion}}">
|
||||||
|
|
||||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
</head>
|
</head>
|
||||||
|
@ -613,7 +613,7 @@
|
||||||
"pathSeparator": '{{.pathSeparator}}',
|
"pathSeparator": '{{.pathSeparator}}',
|
||||||
"label": {{.i18n}},
|
"label": {{.i18n}},
|
||||||
"channel": {{.conf.Channel}},
|
"channel": {{.conf.Channel}},
|
||||||
"wideSessionId": '{{.session.ID}}',
|
"wideSessionId": '{{.sid}}',
|
||||||
"editorTheme": '{{.user.Editor.Theme}}',
|
"editorTheme": '{{.user.Editor.Theme}}',
|
||||||
"latestSessionContent": {{.latestSessionContent}},
|
"latestSessionContent": {{.latestSessionContent}},
|
||||||
"editorTabSize": '{{.user.Editor.TabSize}}',
|
"editorTabSize": '{{.user.Editor.TabSize}}',
|
||||||
|
|
|
@ -100,7 +100,7 @@
|
||||||
"server": "{{.conf.Server}}",
|
"server": "{{.conf.Server}}",
|
||||||
"staticServer": "{{.conf.StaticServer}}",
|
"staticServer": "{{.conf.StaticServer}}",
|
||||||
"channel": "{{.conf.Channel}}",
|
"channel": "{{.conf.Channel}}",
|
||||||
"wideSessionId": "{{.session.ID}}",
|
"wideSessionId": "{{.sid}}",
|
||||||
"label": {{.i18n}},
|
"label": {{.i18n}},
|
||||||
"autocomplete": {{.conf.Autocomplete}},
|
"autocomplete": {{.conf.Autocomplete}},
|
||||||
"embed": {{.embed}},
|
"embed": {{.embed}},
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
channel: {
|
channel: {
|
||||||
shell: '{{.conf.ShellChannel}}'
|
shell: '{{.conf.ShellChannel}}'
|
||||||
},
|
},
|
||||||
wideSessionId: {{.session.ID}}
|
wideSessionId: {{.sid}}
|
||||||
};</script>
|
};</script>
|
||||||
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/jquery-2.1.1.min.js"></script>
|
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/jquery-2.1.1.min.js"></script>
|
||||||
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/reconnecting-websocket.js"></script>
|
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/reconnecting-websocket.js"></script>
|
||||||
|
|
Loading…
Reference in New Issue