diff --git a/docs/changes.md b/docs/changes.md index 145aad77b0..252d3e5eb6 100644 --- a/docs/changes.md +++ b/docs/changes.md @@ -2,6 +2,12 @@ Release date: `2018-??-??` +# 3.0.6 +Release date: `2018-03-02` + +### Core +- [NXDRIVE-1138](https://jira.nuxeo.com/browse/NXDRIVE-1138): Fix Direct Edit auto-lock on Windows 10 + # 3.0.5 Release date: `2018-02-23` diff --git a/nuxeo-drive-client/nxdrive/__init__.py b/nuxeo-drive-client/nxdrive/__init__.py index bad69e2cb9..7b53163b01 100644 --- a/nuxeo-drive-client/nxdrive/__init__.py +++ b/nuxeo-drive-client/nxdrive/__init__.py @@ -26,4 +26,4 @@ """ __author__ = 'Nuxeo' -__version__ = '3.0.5' +__version__ = '3.0.6' diff --git a/nuxeo-drive-client/nxdrive/autolocker.py b/nuxeo-drive-client/nxdrive/autolocker.py index d5617878ae..caecb7842d 100644 --- a/nuxeo-drive-client/nxdrive/autolocker.py +++ b/nuxeo-drive-client/nxdrive/autolocker.py @@ -1,26 +1,26 @@ # coding: utf-8 +import psutil from copy import deepcopy from logging import getLogger -import psutil -from PyQt4 import QtCore +from PyQt4.QtCore import QTimer, pyqtSignal -from engine.workers import PollWorker -from nxdrive.engine.workers import ThreadInterrupt +from nxdrive.engine.workers import PollWorker, ThreadInterrupt +from nxdrive.utils import force_decode log = getLogger(__name__) class ProcessAutoLockerWorker(PollWorker): - orphanLocks = QtCore.pyqtSignal(object) - documentLocked = QtCore.pyqtSignal(str) - documentUnlocked = QtCore.pyqtSignal(str) + orphanLocks = pyqtSignal(object) + documentLocked = pyqtSignal(str) + documentUnlocked = pyqtSignal(str) def __init__(self, check_interval, dao, folder): super(ProcessAutoLockerWorker, self).__init__(check_interval) self._dao = dao - self._folder = folder + self._folder = force_decode(folder) self._autolocked = {} self._lockers = {} @@ -34,7 +34,7 @@ def set_autolock(self, filepath, locker): self._autolocked[filepath] = 0 self._lockers[filepath] = locker - QtCore.QTimer.singleShot(2000, self.force_poll) + QTimer.singleShot(2000, self.force_poll) def _poll(self): try: @@ -47,7 +47,7 @@ def _poll(self): except ThreadInterrupt: raise except: - log.trace('Unhandled error', exc_info=True) + log.exception('Unhandled error') def orphan_unlocked(self, path): self._dao.unlock_path(path) @@ -56,9 +56,9 @@ def _process(self): to_unlock = deepcopy(self._autolocked) for pid, path in get_open_files(): if path.startswith(self._folder): - log.trace('Found in watched folder: %r (PID=%r)', path, pid) + log.debug('Found in watched folder: %r (PID=%r)', path, pid) elif path in self._autolocked: - log.trace('Found in auto-locked: %r (PID=%r)', path, pid) + log.debug('Found in auto-locked: %r (PID=%r)', path, pid) else: continue @@ -82,7 +82,7 @@ def _unlock_files(self, files): def _lock_file(self, item): pid, path = item - log.trace('Locking file %r (PID=%r)', path, pid) + log.debug('Locking file %r (PID=%r)', path, pid) if path in self._lockers: locker = self._lockers[path] locker.autolock_lock(path) @@ -90,7 +90,7 @@ def _lock_file(self, item): self._to_lock.remove(item) def _unlock_file(self, path): - log.trace('Unlocking file %r', path) + log.debug('Unlocking file %r', path) if path in self._lockers: locker = self._lockers[path] locker.autolock_unlock(path) @@ -110,6 +110,6 @@ def get_open_files(): for proc in psutil.process_iter(): try: for handler in proc.open_files(): - yield proc.pid, handler.path + yield proc.pid, force_decode(handler.path) except psutil.Error: pass diff --git a/nuxeo-drive-client/nxdrive/direct_edit.py b/nuxeo-drive-client/nxdrive/direct_edit.py index 27ae99d763..5ce9961e37 100644 --- a/nuxeo-drive-client/nxdrive/direct_edit.py +++ b/nuxeo-drive-client/nxdrive/direct_edit.py @@ -45,7 +45,8 @@ def __init__(self, manager, folder, url): self._folder = folder self.url = url - self._thread.started.connect(self.run) + self.autolock = self._manager.autolock_service + self.use_autolock = self._manager.get_direct_edit_auto_lock() self._event_handler = None self._metrics = {'edit_files': 0} self._observer = None @@ -54,10 +55,12 @@ def __init__(self, manager, folder, url): self._lock_queue = Queue() self._error_queue = BlacklistQueue() self._stop = False - self._manager.autolock_service.orphanLocks.connect(self._autolock_orphans) self._last_action_timing = -1 self.watchdog_queue = Queue() + self._thread.started.connect(self.run) + self.autolock.orphanLocks.connect(self._autolock_orphans) + @pyqtSlot(object) def _autolock_orphans(self, locks): log.trace('Orphans lock: %r', locks) @@ -65,7 +68,7 @@ def _autolock_orphans(self, locks): if lock.path.startswith(self._folder): log.debug('Should unlock %r', lock.path) if not os.path.exists(lock.path): - self._manager.autolock_service.orphan_unlocked(lock.path) + self.autolock.orphan_unlocked(lock.path) continue ref = self._local_client.get_path(lock.path) @@ -385,7 +388,7 @@ def _handle_lock_queue(self): remote.lock(uid) local.set_remote_id(dir_path, '1', name='nxdirecteditlock') # Emit the lock signal only when the lock is really set - self._manager.autolock_service.documentLocked.emit(os.path.basename(ref)) + self.autolock.documentLocked.emit(os.path.basename(ref)) continue try: @@ -398,13 +401,13 @@ def _handle_lock_queue(self): if purge or action.startswith('unlock'): path = local.abspath(ref) log.trace('Remove orphan: %r', path) - self._manager.autolock_service.orphan_unlocked(path) + self.autolock.orphan_unlocked(path) shutil.rmtree(path, ignore_errors=True) continue local.remove_remote_id(dir_path, name='nxdirecteditlock') # Emit the signal only when the unlock is done - self._manager.autolock_service.documentUnlocked.emit(os.path.basename(ref)) + self.autolock.documentUnlocked.emit(os.path.basename(ref)) except ThreadInterrupt: raise except: @@ -563,7 +566,7 @@ def handle_watchdog_event(self, evt): return local = self._local_client - file_name = os.path.basename(src_path) + file_name = force_decode(os.path.basename(src_path)) if local.is_temp_file(file_name): return @@ -571,23 +574,41 @@ def handle_watchdog_event(self, evt): evt.event_type, evt.src_path) if evt.event_type == 'moved': - src_path = evt.dest_path - file_name = os.path.basename(src_path) + src_path = normalize_event_filename(evt.dest_path) + file_name = force_decode(os.path.basename(src_path)) ref = local.get_path(src_path) dir_path = local.get_path(os.path.dirname(src_path)) name = local.get_remote_id(dir_path, name='nxdirecteditname') - if not name or force_decode(name) != force_decode(file_name): - if (evt.event_type == 'deleted' - and self._is_lock_file(file_name)): - # Free the xattr to let _cleanup() does its work - local.remove_remote_id(dir_path, name='nxdirecteditlock') + if not name: + return + + editing = local.get_remote_id(dir_path, name='nxdirecteditlock') + + if force_decode(name) != file_name: + if self._is_lock_file(file_name): + if (evt.event_type == 'created' + and self.use_autolock and editing != '1'): + """ + [Windows 10] The original file is not modified until + we specifically click on the save button. Instead, it + applies changes to the temporary file. + So the auto-lock does not happen because there is no + 'modified' event on the original file. + Here we try to address that by checking the lock state + and use the lock if not already done. + """ + # Recompute the path from 'dir/temp_file' -> 'dir/file' + path = os.path.join(os.path.dirname(src_path), name) + self.autolock.set_autolock(path, self) + elif evt.event_type == 'deleted': + # Free the xattr to let _cleanup() does its work + local.remove_remote_id(dir_path, name='nxdirecteditlock') return - if (local.get_remote_id(dir_path, name='nxdirecteditlock') != '1' - and self._manager.get_direct_edit_auto_lock()): - self._manager.autolock_service.set_autolock(src_path, self) + if self.use_autolock and editing != '1': + self.autolock.set_autolock(src_path, self) if evt.event_type != 'deleted': self._upload_queue.put(ref) diff --git a/nuxeo-drive-client/nxdrive/notification.py b/nuxeo-drive-client/nxdrive/notification.py index 53f39c1bb5..5b1c323915 100644 --- a/nuxeo-drive-client/nxdrive/notification.py +++ b/nuxeo-drive-client/nxdrive/notification.py @@ -180,7 +180,7 @@ def get_notifications(self, engine=None, include_generic=True): return result def send_notification(self, notification): - log.trace('Sending %r', notification) + log.debug('Sending %r', notification) notification._time = int(time.time()) with self._lock: if notification.is_persistent():