Commit b9a3e57a authored by Benjamin Fischer's avatar Benjamin Fischer
Browse files

Some rework related to the file change watch feature:

 Implement inline watch for get_file/save_file.
 Replace the codeeditors implementation for file change watching with the global one.
 TODO: renameing a file does not trigger the desired events (needs fix in fsmonitor)
parent 107c4d07
......@@ -107,7 +107,6 @@ class FSAjaxController(AbstractController):
fs = self.get('fs')
self.release_database()
filefilter = filefilter or []
deep = self.convert(deep, bool)
reverse = self.convert(reverse, bool)
......@@ -247,6 +246,28 @@ class FSAjaxController(AbstractController):
except Exception, e:
return self.fail(msg=str(e), encode_json=True)
@cherrypy.expose
@cherrypy.tools.ajax(encoded=True)
def save_file(self, path, content, watch_id=None, utf8=False):
self.release_session()
fs = self.get('fs')
self.release_database()
return fs.save_file(path, content, utf8=utf8,
window_id=self.get('window_id'),
view_id=self.get('view_id'),
watch_id=watch_id)
@cherrypy.expose
@cherrypy.tools.ajax(encoded=True)
def get_file(self, path, watch_id=None, utf8=False):
self.release_session()
fs = self.get('fs')
self.release_database()
return fs.get_file(path, utf8=utf8,
window_id=self.get('window_id'),
view_id=self.get('view_id'),
watch_id=watch_id)
@cherrypy.expose
@cherrypy.tools.ajax()
def watch(self, path, watch_id):
......
......@@ -14,22 +14,6 @@ class EditorController(AbstractController):
return self.get("proxy", "CodeEditorRpc", self.get("combined_id"),
window_id=windowId, view_id=viewId)
@cherrypy.expose
@cherrypy.tools.ajax(encoded=True)
def getcontent(self, path):
self.release_session()
rpc = self.getrpc()
self.release_database()
return rpc.getcontent(path)
@cherrypy.expose
@cherrypy.tools.ajax(encoded=True)
def savecontent(self, path, content):
self.release_session()
rpc = self.getrpc()
self.release_database()
return rpc.savecontent(path, content)
@cherrypy.expose
@cherrypy.tools.ajax()
def execute(self, cmd, base):
......@@ -56,16 +40,6 @@ class EditorController(AbstractController):
self.release_database()
return {"success": rpc.abort()}
@cherrypy.expose
@cherrypy.tools.ajax(encoded=True)
def getpreviews(self, path, filter):
self.release_session()
rfs = self.get("fs")
rpc = self.getrpc()
self.release_database()
rpc.watch_dir(path)
return rfs.get_file_list(path, reverse=True, filter=[filter])
@cherrypy.expose
@cherrypy.tools.ajax()
def close(self):
......
......@@ -38,12 +38,16 @@ var CodeEditor = Emitter.extend({
this.ace = ace.edit($(self.node).get(0));
this.ace.focus();
this.view.onSocket('modified', function(data) {
if (self.modified_processing)
this.view.onSocket('watch', function(data) {
if (data.watch_id != 'code') {
return;
}
if (data.action_name == 'attrib') {
return;
if (data.exists && data.renamed)
}
if (self.modified_processing)
return;
else if (!data.exists) {
if (data.mtime == -1) {
self.modified_processing = true;
self.view.confirm(
"The file " + self.path + " has been deleted or renamed." +
......@@ -102,8 +106,10 @@ var CodeEditor = Emitter.extend({
this.view.setLoading(true);
this.view.POST("getcontent", {
path: path
this.view.POST(vispa.url.dynamic("/ajax/fs/get_file"), {
path: path,
utf8: true,
watch_id: "code"
}).done(function(res) {
if (!res.success) {
self.view.alert("<html>An internal server error has occured. <br /><br /> The editor has been closed. </html>")
......@@ -267,20 +273,16 @@ var CodeEditor = Emitter.extend({
this.view.setLoading(true);
var req = {
this.view.POST(vispa.url.dynamic("/ajax/fs/save_file"), {
path: this.path,
content: this.getContent()
};
this.view.POST("savecontent", req).done(function(res) {
content: this.getContent(),
utf8: true,
watch_id: "code"
}).done(function(res) {
self.mtime = res.mtime;
self.lastContent = self.getContent();
self.checkModifications();
self.setMode();
if (!self.view.preview.pathSelected) {
var parts = req.path.split("/");
parts.pop();
self.view.preview.path = parts.join("/");
}
if ($.isFunction(callback))
callback();
}).always(function() {
......@@ -434,4 +436,5 @@ var CodeEditor = Emitter.extend({
}
return mode;
}
});
\ No newline at end of file
......@@ -173,6 +173,7 @@ var CodeEditorView = vispa.ExtensionView.Center.extend({
// cleanup
var cleanUp = function() {
self.GET(vispa.url.dynamic("/ajax/fs/unwatch"), {});
self.POST("close");
self.output.running = false;
self.preview.clearInterval();
......
......@@ -44,7 +44,13 @@ var CodeEditorPreview = Emitter.extend({
self.inputFocused = false;
});
this.view.onSocket('dir', function(){
this.view.onSocket('watch', function(data){
if (data.watch_id != 'preview') {
return;
}
if (data.action_name == 'attrib') {
return;
}
self.previewChanged = true;
})
......@@ -96,9 +102,11 @@ var CodeEditorPreview = Emitter.extend({
if (path.substr(-1) == "/")
path = path.substr(0, path.length - 1);
self.view.GET("getpreviews", {
self.view.GET(vispa.url.dynamic("/ajax/fs/filelist"), {
path: path,
filter: "\\.(" + this.allowedExtensions.join("|") + ")$",
filefilter: "\\.(" + this.allowedExtensions.join("|") + ")$",
reverse: true,
watch_id: "preview"
}).done(function(res) {
var files = $.grep(res.filelist, function(file) {
return file.type == "f" && ~self.allowedExtensions.indexOf(file.name.split(".").pop().toLowerCase());
......@@ -231,7 +239,12 @@ var CodeEditorPreview = Emitter.extend({
path = parts.join("/");
}
var oldpath = this.pathInput.val();
this.pathInput.val(path);
if (path != oldpath) {
this.previewChanged = true;
this.refresh();
}
return this;
}
});
\ No newline at end of file
......@@ -10,7 +10,6 @@ import threading
import shlex
import select
import pty
import fsmonitor
from subprocess import Popen, PIPE
logger = logging.getLogger(__name__)
......@@ -36,96 +35,16 @@ class CodeEditorRpc:
self._pty_fno = self._pty_fd.fileno()
self._popen = None
self._monitor_thread = fsmonitor.FSMonitorThread(self._watch_callback)
self._watch_dir = None
self._watch_file = None
self._watch_file_path = None
logger.debug("CodeEditorRpc created")
def close(self):
if self._popen and self._popen.poll() is None:
self._popen.kill()
self._abort = True
if self._monitor_thread:
self._monitor_thread.remove_all_watches()
self._monitor_thread._running = False
def _send(self, topic, data=None):
vispa.remote.send_topic(self._topic+"."+topic, window_id=self._window_id, data=data)
def watch_file(self, path=None):
if self._watch_file:
if self._watch_file.path == path:
return
self._monitor_thread.remove_watch(self._watch_file)
self._watch_file = None
if path:
self._watch_file = self._monitor_thread.add_file_watch(path)
self._watch_file_path = expand(path)
def watch_dir(self, path):
path = expand(path)
if self._watch_dir:
if self._watch_dir.path == path:
return
self._monitor_thread.remove_watch(self._watch_dir)
self._watch_dir = self._monitor_thread.add_dir_watch(path)
def _watch_callback(self, event):
if event.action_name == 'access':
return
if event.watch == self._watch_file or event.action_name == 'move from': #covers modifications in the code and delete/rename of file
exists = os.path.exists(self._watch_file_path)
renamed = False
if event.action_name == 'move from':
renamed = True
if exists:
mtime = os.path.getmtime(event.path)
else:
mtime = None
self._send('modified', {
"exists": exists,
"mtime" : mtime,
"renamed" : renamed
})
if event.watch == self._watch_dir:
self._send('dir')
def getcontent(self, path):
path = expand(path)
try:
with open(path, "r") as f:
content = f.read().decode('utf8')
mtime = os.path.getmtime(path)
self.watch_file(path)
response = {
"content": content,
"mtime": mtime,
"success": True
}
return json.dumps(response)
except Exception as e:
response = {
"content": "",
"mtime": 0,
"success": False
}
return json.dumps(response)
def savecontent(self, path, content):
path = expand(path)
self.watch_file()
with open(path, "w") as f:
f.write(content.encode('utf8'))
self.watch_file(path)
response = {
"mtime": os.path.getmtime(path)
}
return json.dumps(response)
def runningjob(self):
return bool(self._thread and self._thread.is_alive())
......
# Copyright (c) 2010, 2012 Luke McCarthy <luke@iogopro.co.uk>
#
# This is free software released under the MIT license.
# See COPYING file for details, or visit:
# http://www.opensource.org/licenses/mit-license.php
#
# The file is part of FSMonitor, a file-system monitoring library.
# https://github.com/shaurz/fsmonitor
import sys
import threading
import traceback
from .common import FSEvent, FSMonitorError, FSMonitorOSError
# set to None when unloaded
module_loaded = True
if sys.platform == "linux2":
from .linux import FSMonitor
elif sys.platform == "win32":
from .win32 import FSMonitor
else:
from .polling import FSMonitor
class FSMonitorThread(threading.Thread):
def __init__(self, callback=None):
threading.Thread.__init__(self)
self.monitor = FSMonitor()
self.callback = callback
self._running = True
self._events = []
self._events_lock = threading.Lock()
self.daemon = True
self.start()
def add_dir_watch(self, path, flags=FSEvent.All, user=None):
return self.monitor.add_dir_watch(path, flags=flags, user=user)
def add_file_watch(self, path, flags=FSEvent.All, user=None):
return self.monitor.add_file_watch(path, flags=flags, user=user)
def remove_watch(self, watch):
self.monitor.remove_watch(watch)
def remove_all_watches(self):
self.monitor.remove_all_watches()
with self._events_lock:
self._events = []
def run(self):
while module_loaded and self._running:
try:
events = self.monitor.read_events()
if self.callback:
for event in events:
self.callback(event)
else:
with self._events_lock:
self._events.extend(events)
except Exception:
print "Exception in FSMonitorThread:\n" + traceback.format_exc()
def stop(self):
if self.monitor.watches:
self.remove_all_watches()
self._running = False
def read_events(self):
with self._events_lock:
events = self._events
self._events = []
return events
__all__ = (
"FSMonitor",
"FSMonitorThread",
"FSMonitorError",
"FSMonitorOSError",
"FSEvent",
)
# Copyright (c) 2010 Luke McCarthy <luke@iogopro.co.uk>
#
# This is free software released under the MIT license.
# See COPYING file for details, or visit:
# http://www.opensource.org/licenses/mit-license.php
#
# The file is part of FSMonitor, a file-system monitoring library.
# https://github.com/shaurz/fsmonitor
class FSMonitorError(Exception):
pass
class FSMonitorOSError(OSError, FSMonitorError):
pass
class FSEvent(object):
def __init__(self, watch, action, name=""):
self.watch = watch
self.name = name
self.action = action
@property
def action_name(self):
return self.action_names[self.action]
@property
def path(self):
return self.watch.path
@property
def user(self):
return self.watch.user
Access = 0x01
Modify = 0x02
Attrib = 0x04
Create = 0x08
Delete = 0x10
DeleteSelf = 0x20
MoveFrom = 0x40
MoveTo = 0x80
All = 0xFF
action_names = {
Access : "access",
Modify : "modify",
Attrib : "attrib",
Create : "create",
Delete : "delete",
DeleteSelf : "delete self",
MoveFrom : "move from",
MoveTo : "move to",
}
# Copyright (c) 2010 Luke McCarthy <luke@iogopro.co.uk>
#
# This is free software released under the MIT license.
# See COPYING file for details, or visit:
# http://www.opensource.org/licenses/mit-license.php
#
# The file is part of FSMonitor, a file-system monitoring library.
# https://github.com/shaurz/fsmonitor
import os, struct, threading, errno, select
from ctypes import CDLL, CFUNCTYPE, POINTER, c_int, c_char_p, c_uint32, get_errno
from .common import FSEvent, FSMonitorOSError
# set to None when unloaded
module_loaded = True
libc = CDLL("libc.so.6")
strerror = CFUNCTYPE(c_char_p, c_int)(
("strerror", libc))
inotify_init = CFUNCTYPE(c_int, use_errno=True)(
("inotify_init", libc))
inotify_add_watch = CFUNCTYPE(c_int, c_int, c_char_p, c_uint32, use_errno=True)(
("inotify_add_watch", libc))
inotify_rm_watch = CFUNCTYPE(c_int, c_int, c_int, use_errno=True)(
("inotify_rm_watch", libc))
# Supported events suitable for MASK parameter of INOTIFY_ADD_WATCH.
IN_ACCESS = 0x00000001 # File was accessed.
IN_MODIFY = 0x00000002 # File was modified.
IN_ATTRIB = 0x00000004 # Metadata changed.
IN_CLOSE_WRITE = 0x00000008 # Writtable file was closed.
IN_CLOSE_NOWRITE = 0x00000010 # Unwrittable file closed.
IN_CLOSE = IN_CLOSE_WRITE | IN_CLOSE_NOWRITE # Close.
IN_OPEN = 0x00000020 # File was opened.
IN_MOVED_FROM = 0x00000040 # File was moved from X.
IN_MOVED_TO = 0x00000080 # File was moved to Y.
IN_MOVE = IN_MOVED_FROM | IN_MOVED_TO # Moves.
IN_CREATE = 0x00000100 # Subfile was created.
IN_DELETE = 0x00000200 # Subfile was deleted.
IN_DELETE_SELF = 0x00000400 # Self was deleted.
IN_MOVE_SELF = 0x00000800 # Self was moved.
# Events sent by the kernel.
IN_UNMOUNT = 0x00002000 # Backing fs was unmounted.
IN_Q_OVERFLOW = 0x00004000 # Event queued overflowed.
IN_IGNORED = 0x00008000 # File was ignored.
# Special flags.
IN_ONLYDIR = 0x01000000 # Only watch the path if it is a directory.
IN_DONT_FOLLOW = 0x02000000 # Do not follow a sym link.
IN_MASK_ADD = 0x20000000 # Add to the mask of an already existing watch.
IN_ISDIR = 0x40000000 # Event occurred against dir.
IN_ONESHOT = 0x80000000 # Only send event once.
action_map = {
IN_ACCESS : FSEvent.Access,
IN_MODIFY : FSEvent.Modify,
IN_ATTRIB : FSEvent.Attrib,
IN_MOVED_FROM : FSEvent.MoveFrom,
IN_MOVED_TO : FSEvent.MoveTo,
IN_CREATE : FSEvent.Create,
IN_DELETE : FSEvent.Delete,
IN_DELETE_SELF : FSEvent.DeleteSelf,
}
flags_map = {
FSEvent.Access : IN_ACCESS,
FSEvent.Modify : IN_MODIFY,
FSEvent.Attrib : IN_ATTRIB,
FSEvent.Create : IN_CREATE,
FSEvent.Delete : IN_DELETE,
FSEvent.DeleteSelf : IN_DELETE_SELF,
FSEvent.MoveFrom : IN_MOVED_FROM,
FSEvent.MoveTo : IN_MOVED_TO,
}
def convert_flags(flags):
os_flags = 0
flag = 1
while flag < FSEvent.All + 1:
if flags & flag:
os_flags |= flags_map[flag]
flag <<= 1
return os_flags
def parse_events(s):
i = 0
while i + 16 <= len(s):
wd, mask, cookie, length = struct.unpack_from("iIII", s, i)
name = s[i+16:i+16+length].rstrip("\0")
i += 16 + length
yield wd, mask, cookie, name
class FSMonitorWatch(object):
def __init__(self, wd, path, flags, user):
self._wd = wd
self.path = path
self.flags = flags
self.user = user
self.enabled = True
def __repr__(self):
return "<FSMonitorWatch %r>" % self.path
class FSMonitor(object):
def __init__(self):
fd = inotify_init()
if fd == -1:
errno = get_errno()
raise FSMonitorOSError(errno, strerror(errno))
self.__fd = fd
self.__lock = threading.Lock()
self.__wd_to_watch = {}
def __del__(self):
if module_loaded:
self.close()
def close(self):
if self.__fd is not None:
os.close(self.__fd)
self.__fd = None
def _add_watch(self, path, flags, user, inotify_flags=0):
inotify_flags |= convert_flags(flags) | IN_DELETE_SELF
wd = inotify_add_watch(self.__fd, path, inotify_flags)
if wd == -1:
errno = get_errno()
raise FSMonitorOSError(errno, strerror(errno))
watch = FSMonitorWatch(wd, path, flags, user)
with self.__lock:
self.__wd_to_watch[wd] = watch
return watch
def add_dir_watch(self, path, flags=FSEvent.All, user=None):
return self._add_watch(path, flags, user, IN_ONLYDIR)
def add_file_watch(self, path, flags=FSEvent.All, user=None):
return self._add_watch(path, flags, user)
def remove_watch(self, watch):
return inotify_rm_watch(self.__fd, watch._wd) != -1
def remove_all_watches(self):
</