This commit is contained in:
Liang Ding 2014-09-23 21:03:44 +08:00
parent 18c2cc2ad9
commit bbe17c431c
7 changed files with 152 additions and 94 deletions

View File

@ -15,16 +15,9 @@
"Password": "admin", "Password": "admin",
"Workspace": "{pwd}/data/user_workspaces/admin", "Workspace": "{pwd}/data/user_workspaces/admin",
"LatestSessionContent": { "LatestSessionContent": {
"FileTree": [ "FileTree": [],
"1/", "Files": [],
"2/" "CurrentFile": ""
],
"Files": [
"1.go",
"2.go",
"3.go"
],
"CurrentFile": "current file"
} }
} }
] ]

View File

@ -40,6 +40,9 @@ func init() {
// 定时保存配置 // 定时保存配置
conf.FixedTimeSave() conf.FixedTimeSave()
// 定时检查无效会话
session.FixedTimeRelease()
} }
// 登录. // 登录.

View File

@ -63,6 +63,27 @@ var WideSessions Sessions
// 排它锁,防止并发修改. // 排它锁,防止并发修改.
var mutex sync.Mutex var mutex sync.Mutex
// 在一些特殊情况(例如浏览器不间断刷新时会话通道建立并发)下 Wide 会话集内会出现无效会话该函数定时1 小时)检查并移除这些无效会话.
// 无效会话:在检查时间内 30 分钟都没有使用过的会话WideSession.Updated 字段.
func FixedTimeRelease() {
go func() {
for {
hour, _ := time.ParseDuration("-30m")
threshold := time.Now().Add(hour)
for _, s := range WideSessions {
if s.Updated.Before(threshold) {
glog.V(3).Infof("Removes a invalid session [%s]", s.Id)
WideSessions.Remove(s.Id)
}
}
time.Sleep(time.Hour)
}
}()
}
// 建立会话通道. // 建立会话通道.
// 通道断开时销毁会话状态,回收相关资源. // 通道断开时销毁会话状态,回收相关资源.
func WSHandler(w http.ResponseWriter, r *http.Request) { func WSHandler(w http.ResponseWriter, r *http.Request) {
@ -90,40 +111,9 @@ func WSHandler(w http.ResponseWriter, r *http.Request) {
if err := wsChan.Conn.ReadJSON(&input); err != nil { if err := wsChan.Conn.ReadJSON(&input); err != nil {
glog.V(3).Infof("[Session Channel] of session [%s] disconnected, releases all resources with it", sid) glog.V(3).Infof("[Session Channel] of session [%s] disconnected, releases all resources with it", sid)
for i, s := range WideSessions { WideSessions.Remove(sid)
if s.Id == sid {
mutex.Lock()
// 从会话集中移除 return
WideSessions = append(WideSessions[:i], WideSessions[i+1:]...)
// 关闭用户事件队列
event.UserEventQueues.Close(sid)
// 杀进程
for _, p := range s.Processes {
if err := p.Kill(); nil != err {
glog.Errorf("Can't kill process [%d] of session [%s]", p.Pid, sid)
} else {
glog.V(3).Infof("Killed a process [%d] of session [%s]", p.Pid, sid)
}
}
// 回收所有通道
OutputWS[sid].Close()
delete(OutputWS, sid)
NotificationWS[sid].Close()
delete(NotificationWS, sid)
sessionWS[sid].Close()
delete(sessionWS, sid)
mutex.Unlock()
return
}
}
} }
ret = map[string]interface{}{"output": "", "cmd": "session-output"} ret = map[string]interface{}{"output": "", "cmd": "session-output"}
@ -143,7 +133,7 @@ func SaveContent(w http.ResponseWriter, r *http.Request) {
defer util.RetJSON(w, r, data) defer util.RetJSON(w, r, data)
args := struct { args := struct {
sid string Sid string
*conf.LatestSessionContent *conf.LatestSessionContent
}{} }{}
@ -154,7 +144,7 @@ func SaveContent(w http.ResponseWriter, r *http.Request) {
return return
} }
wSession := WideSessions.Get(args.sid) wSession := WideSessions.Get(args.Sid)
if nil == wSession { if nil == wSession {
data["succ"] = false data["succ"] = false
@ -165,9 +155,10 @@ func SaveContent(w http.ResponseWriter, r *http.Request) {
for _, user := range conf.Wide.Users { for _, user := range conf.Wide.Users {
if user.Name == wSession.Username { if user.Name == wSession.Username {
// 更新配置内存变量conf.FixedTimeSave() 会负责定时持久化
user.LatestSessionContent = wSession.Content user.LatestSessionContent = wSession.Content
// 定时任务会负责持久化 wSession.Refresh()
return return
} }
@ -236,9 +227,46 @@ func (sessions *Sessions) Remove(sid string) {
for i, s := range *sessions { for i, s := range *sessions {
if s.Id == sid { if s.Id == sid {
// 从会话集中移除
*sessions = append((*sessions)[:i], (*sessions)[i+1:]...) *sessions = append((*sessions)[:i], (*sessions)[i+1:]...)
glog.V(3).Infof("Removed a session [%s], has [%d] wide sessions currently", sid, len(*sessions)) // 关闭用户事件队列
event.UserEventQueues.Close(sid)
// 杀进程
for _, p := range s.Processes {
if err := p.Kill(); nil != err {
glog.Errorf("Can't kill process [%d] of session [%s]", p.Pid, sid)
} else {
glog.V(3).Infof("Killed a process [%d] of session [%s]", p.Pid, sid)
}
}
// 回收所有通道
if ws, ok := OutputWS[sid]; ok {
ws.Close()
delete(OutputWS, sid)
}
if ws, ok := NotificationWS[sid]; ok {
ws.Close()
delete(NotificationWS, sid)
}
if ws, ok := sessionWS[sid]; ok {
ws.Close()
delete(sessionWS, sid)
}
glog.V(3).Infof("Removed a session [%s]", s.Id)
cnt := 0 // 统计当前 HTTP 会话关联的 Wide 会话数量
for _, s := range *sessions {
if s.HTTPSession.ID == s.HTTPSession.ID {
cnt++
}
}
glog.V(3).Infof("User [%s] has [%d] sessions", s.Username, cnt)
return return
} }

View File

@ -1,5 +1,6 @@
var editors = { var editors = {
data: [], data: [],
tabs: {},
init: function () { init: function () {
editors._initAutocomplete(); editors._initAutocomplete();
editors.tabs = new Tabs({ editors.tabs = new Tabs({

View File

@ -1,37 +1,58 @@
// 用于保持会话,如果该通道断开,则服务器端会销毁会话状态,回收相关资源. var session = {
var sessionWS = new WebSocket(config.channel.session + '/session/ws?sid=' + config.wideSessionId); init: function () {
sessionWS.onopen = function () { this._initWS();
console.log('[session onopen] connected');
};
sessionWS.onmessage = function (e) { // 定时30 秒)保存会话内容.
console.log('[session onmessage]' + e.data); setInterval(function () {
var data = JSON.parse(e.data); var request = newWideRequest(),
filse = [],
fileTree = [],
currentFile = "";
}; editors.tabs.obj._$tabs.find("div").each(function () {
sessionWS.onclose = function (e) { var $it = $(this);
console.log('[session onclose] disconnected (' + e.code + ')'); if ($it.hasClass("current")) {
delete sessionWS; currentFile = $it.find("span:eq(0)").attr("title");
}; }
sessionWS.onerror = function (e) {
console.log('[session onerror] ' + JSON.parse(e));
};
// 定时30 秒)保存会话内容. filse.push($it.find("span:eq(0)").attr("title"));
setTimeout(function () { });
var request = newWideRequest();
fileTree = tree.getOpenPaths();
// TODO: 会话状态保存
request.currentFile = "current file"; // 当前编辑器
request.fileTree = ["1/", "2/"]; // 文件树展开状态
request.files = ["1.go", "2.go", "3.go"]; // 编辑器打开状态
$.ajax({ request.currentFile = currentFile; // 当前编辑器
type: 'POST', request.fileTree = fileTree; // 文件树展开状态
url: '/session/save', request.files = filse; // 编辑器打开状态
data: JSON.stringify(request),
dataType: "json", $.ajax({
success: function (data) { type: 'POST',
} url: '/session/save',
}); data: JSON.stringify(request),
}, 30000); dataType: "json",
success: function (data) {
}
});
}, 5000);
},
_initWS: function () {
// 用于保持会话,如果该通道断开,则服务器端会销毁会话状态,回收相关资源.
var sessionWS = new WebSocket(config.channel.session + '/session/ws?sid=' + config.wideSessionId);
sessionWS.onopen = function () {
console.log('[session onopen] connected');
};
sessionWS.onmessage = function (e) {
console.log('[session onmessage]' + e.data);
var data = JSON.parse(e.data);
};
sessionWS.onclose = function (e) {
console.log('[session onclose] disconnected (' + e.code + ')');
delete sessionWS;
};
sessionWS.onerror = function (e) {
console.log('[session onerror] ' + JSON.parse(e));
};
}
};

View File

@ -1,6 +1,6 @@
var tree = { var tree = {
// 递归获取当前节点展开中的最后一个节点 // 递归获取当前节点展开中的最后一个节点
getCurrentNodeLastNode: function(node) { getCurrentNodeLastNode: function (node) {
var returnNode = node.children[node.children.length - 1]; var returnNode = node.children[node.children.length - 1];
if (returnNode.open) { if (returnNode.open) {
return tree.getCurrentNodeLastNode(returnNode); return tree.getCurrentNodeLastNode(returnNode);
@ -9,7 +9,7 @@ var tree = {
} }
}, },
// 按照树展现获取下一个节点 // 按照树展现获取下一个节点
getNextShowNode: function(node) { getNextShowNode: function (node) {
if (node.level !== 0) { if (node.level !== 0) {
if (node.getParentNode().getNextNode()) { if (node.getParentNode().getNextNode()) {
return node.getParentNode().getNextNode(); return node.getParentNode().getNextNode();
@ -20,7 +20,7 @@ var tree = {
return node.getNextNode(); return node.getNextNode();
} }
}, },
isBottomNode: function(node) { isBottomNode: function (node) {
if (node.open) { if (node.open) {
return false; return false;
} }
@ -39,7 +39,7 @@ var tree = {
} }
} }
}, },
getTIdByPath: function(path) { getTIdByPath: function (path) {
var nodes = tree.fileTree.transformToArray(tree.fileTree.getNodes()); var nodes = tree.fileTree.transformToArray(tree.fileTree.getNodes());
for (var i = 0, ii = nodes.length; i < ii; i++) { for (var i = 0, ii = nodes.length; i < ii; i++) {
if (nodes[i].path === path) { if (nodes[i].path === path) {
@ -49,8 +49,19 @@ var tree = {
return undefined; return undefined;
}, },
getOpenPaths: function () {
var nodes = tree.fileTree.transformToArray(tree.fileTree.getNodes()),
paths = [];
for (var i = 0, ii = nodes.length; i < ii; i++) {
if (nodes[i].open) {
paths.push(nodes[i].path);
}
}
return paths;
},
fileTree: undefined, fileTree: undefined,
_isParents: function(tId, parentTId) { _isParents: function (tId, parentTId) {
var node = tree.fileTree.getNodeByTId(tId); var node = tree.fileTree.getNodeByTId(tId);
if (!node || !node.parentTId) { if (!node || !node.parentTId) {
return false; return false;
@ -62,20 +73,20 @@ var tree = {
} }
} }
}, },
newFile: function() { newFile: function () {
$("#dirRMenu").hide(); $("#dirRMenu").hide();
$("#dialogNewFilePrompt").dialog("open"); $("#dialogNewFilePrompt").dialog("open");
}, },
newDir: function() { newDir: function () {
$("#dirRMenu").hide(); $("#dirRMenu").hide();
$("#dialogNewDirPrompt").dialog("open"); $("#dialogNewDirPrompt").dialog("open");
}, },
removeIt: function() { removeIt: function () {
$("#dirRMenu").hide(); $("#dirRMenu").hide();
$("#fileRMenu").hide(); $("#fileRMenu").hide();
$("#dialogRemoveConfirm").dialog("open"); $("#dialogRemoveConfirm").dialog("open");
}, },
init: function() { init: function () {
var request = newWideRequest(); var request = newWideRequest();
$.ajax({ $.ajax({
@ -83,7 +94,7 @@ var tree = {
url: '/files', url: '/files',
data: JSON.stringify(request), data: JSON.stringify(request),
dataType: "json", dataType: "json",
success: function(data) { success: function (data) {
if (data.succ) { if (data.succ) {
var dirRMenu = $("#dirRMenu"); var dirRMenu = $("#dirRMenu");
var fileRMenu = $("#fileRMenu"); var fileRMenu = $("#fileRMenu");
@ -92,7 +103,7 @@ var tree = {
selectedMulti: false selectedMulti: false
}, },
callback: { callback: {
onRightClick: function(event, treeId, treeNode) { onRightClick: function (event, treeId, treeNode) {
if (treeNode) { if (treeNode) {
wide.curNode = treeNode; wide.curNode = treeNode;
if ("ico-ztree-dir " !== treeNode.iconSkin) { // 如果右击了文件 if ("ico-ztree-dir " !== treeNode.iconSkin) { // 如果右击了文件
@ -112,7 +123,7 @@ var tree = {
} }
} }
}, },
onClick: function(event, treeId, treeNode, clickFlag) { onClick: function (event, treeId, treeNode, clickFlag) {
tree._onClick(treeNode); tree._onClick(treeNode);
} }
} }
@ -122,7 +133,7 @@ var tree = {
} }
}); });
}, },
_onClick: function(treeNode) { _onClick: function (treeNode) {
if (wide.curNode) { if (wide.curNode) {
for (var i = 0, ii = editors.data.length; i < ii; i++) { for (var i = 0, ii = editors.data.length; i < ii; i++) {
// 该节点文件已经打开 // 该节点文件已经打开
@ -147,7 +158,7 @@ var tree = {
url: '/file', url: '/file',
data: JSON.stringify(request), data: JSON.stringify(request),
dataType: "json", dataType: "json",
success: function(data) { success: function (data) {
if (!data.succ) { if (!data.succ) {
alert(data.msg); alert(data.msg);

View File

@ -452,4 +452,5 @@ $(document).ready(function() {
menu.init(); menu.init();
hotkeys.init(); hotkeys.init();
notification.init(); notification.init();
session.init();
}); });