From 1fdbdf78310af4ee8426bdf72ef6198233fe45a7 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Thu, 7 May 2020 10:57:37 +0200 Subject: [PATCH 001/101] minor improvements to path handling --- maestral/main.py | 4 ++-- maestral/sync.py | 15 ++++----------- maestral/utils/path.py | 2 +- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/maestral/main.py b/maestral/main.py index 0df0a0068..475ed927f 100644 --- a/maestral/main.py +++ b/maestral/main.py @@ -1113,7 +1113,7 @@ def move_dropbox_directory(self, new_path): """ old_path = self.sync.dropbox_path - new_path = os.path.expanduser(new_path) + new_path = osp.abspath(osp.expanduser(new_path)) try: if osp.samefile(old_path, new_path): @@ -1145,7 +1145,7 @@ def create_dropbox_directory(self, path): :raises: :class:`errors.NotLinkedError` if no Dropbox account is linked. """ - path = os.path.expanduser(path) + path = osp.abspath(osp.expanduser(path)) self.monitor.reset_sync_state() diff --git a/maestral/sync.py b/maestral/sync.py index 0c800325f..7e199955a 100644 --- a/maestral/sync.py +++ b/maestral/sync.py @@ -890,21 +890,14 @@ def to_dbx_path(self, local_path): :raises: :class:`ValueError` the path lies outside of the local Dropbox folder. """ - dbx_root_list = osp.normpath(self.dropbox_path).split(osp.sep) - path_list = osp.normpath(local_path).split(osp.sep) - - # Work out how much of the file path is shared by dropbox_path and path - # noinspection PyTypeChecker - i = len(osp.commonprefix([dbx_root_list, path_list])) - - if i == len(path_list): # path corresponds to dropbox_path + if local_path == self.dropbox_path: # path corresponds to dropbox_path return '/' - elif i != len(dbx_root_list): # path is outside of dropbox_path + elif is_child(local_path, self.dropbox_path): + return local_path.replace(self.dropbox_path, '', 1) + else: raise ValueError(f'Specified path "{local_path}" is outside of Dropbox ' f'directory "{self.dropbox_path}"') - return '/{}'.format('/'.join(path_list[i:])) - def to_local_path(self, dbx_path): """ Converts a Dropbox path to the corresponding local path. diff --git a/maestral/utils/path.py b/maestral/utils/path.py index ebe2d25ee..973675eb1 100644 --- a/maestral/utils/path.py +++ b/maestral/utils/path.py @@ -26,7 +26,7 @@ def is_child(path, parent): :rtype: bool """ - parent = parent.rstrip(osp.sep) + os.sep + parent = parent.rstrip(osp.sep) + osp.sep path = path.rstrip(osp.sep) return path.startswith(parent) From dc8d5b6bb5c099819ddf5b8d7b937c9fcc47833b Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Thu, 7 May 2020 12:18:09 +0200 Subject: [PATCH 002/101] [sync] simplify code to ignore local events --- maestral/sync.py | 65 +++++++++++++++++++++++++----------------------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/maestral/sync.py b/maestral/sync.py index 7e199955a..7cc29ddbf 100644 --- a/maestral/sync.py +++ b/maestral/sync.py @@ -130,8 +130,8 @@ def __init__(self, syncing, startup, sync): self.sync = sync self.sync.fs_events = self - self._ignored_paths = list() - self._mutex = Lock() + self._ignored_paths = [] + self._ignored_paths_mutex = RLock() self.local_file_event_queue = Queue() @@ -153,9 +153,9 @@ def ignore(self, *events, recursive=True): ignored as well. """ - with self._mutex: + with self._ignored_paths_mutex: now = time.time() - new_ignores = list() + new_ignores = [] for e in events: new_ignores.append( dict( @@ -170,54 +170,57 @@ def ignore(self, *events, recursive=True): try: yield finally: - with self._mutex: + with self._ignored_paths_mutex: for ignore in new_ignores: ignore['ttl'] = time.time() + self.ignore_timeout def _expire_ignored_paths(self): """Removes all expired ignore entries.""" - with self._mutex: + with self._ignored_paths_mutex: + now = time.time() for ignore in self._ignored_paths.copy(): ttl = ignore['ttl'] if ttl and ttl < now: self._ignored_paths.remove(ignore) - def _discrad_ignored(self, event): + def _is_ignored(self, event): """ - Checks if a file system event should been explicitly ignored because it was likely - triggered by Maestral. Split moved events if necessary and returns the event to - keep (if any) + Checks if a file system event should been explicitly ignored because it was + triggered by Maestral itself. :param FileSystemEvent event: Local file system event. - :returns: Event to keep or ``None``. - :rtype: :class:`watchdog.FileSystemEvent` + :returns: ``True`` if the event should be ignored, ``False`` otherwise. + :rtype: bool """ - self._expire_ignored_paths() + with self._ignored_paths_mutex: - for ignore in self._ignored_paths: - ignore_event = ignore['event'] - recursive = ignore['recursive'] + self._expire_ignored_paths() - if event == ignore_event: + for ignore in self._ignored_paths: + ignore_event = ignore['event'] + recursive = ignore['recursive'] - if not recursive: - self._ignored_paths.remove(ignore) + if event == ignore_event: - return None + if not recursive: + self._ignored_paths.remove(ignore) - elif recursive: + return True - type_match = event.event_type == ignore_event.event_type - src_match = is_equal_or_child(event.src_path, ignore_event.src_path) - dest_match = is_equal_or_child(get_dest_path(event), get_dest_path(ignore_event)) + elif recursive: - if type_match and src_match and dest_match: - return None + type_match = event.event_type == ignore_event.event_type + src_match = is_equal_or_child(event.src_path, ignore_event.src_path) + dest_match = is_equal_or_child(get_dest_path(event), + get_dest_path(ignore_event)) - return event + if type_match and src_match and dest_match: + return True + + return False def on_any_event(self, event): """ @@ -236,11 +239,11 @@ def on_any_event(self, event): if isinstance(event, DirModifiedEvent): return - # check for ignored paths, split moved events if necessary - event = self._discrad_ignored(event) + # check if event should be ignored + if self._is_ignored(event): + return - if event: - self.local_file_event_queue.put(event) + self.local_file_event_queue.put(event) class MaestralStateWrapper(abc.MutableSet): From fd60f08e2e0fc283a5d4470f3c165383897f57c4 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Thu, 7 May 2020 12:21:08 +0200 Subject: [PATCH 003/101] [sync] remove unnecessary path conversion in `_filter_excluded_changes_local` --- maestral/sync.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/maestral/sync.py b/maestral/sync.py index 7cc29ddbf..047c5732e 100644 --- a/maestral/sync.py +++ b/maestral/sync.py @@ -964,16 +964,16 @@ def clear_all_sync_errors(self): @staticmethod def is_excluded(path): """ - Checks if file is excluded from sync. Certain file names are always excluded from - syncing, following the Dropbox support article: + Checks if a file is excluded from sync. Certain file names are always excluded + from syncing, following the Dropbox support article: https://help.dropbox.com/installs-integrations/sync-uploads/files-not-syncing - The include file system files such as 'desktop.ini' and '.DS_Store' and some - temporary files. This is determined by the basename alone and `is_excluded` - therefore accepts both relative and absolute paths. + This includes file system files such as 'desktop.ini' and '.DS_Store' and some + temporary files as well as caches used by Dropbox or Maestral. `is_excluded` + accepts both local and Dropbox paths. - :param str path: Path of item. Can be absolute or relative. + :param str path: Path of item. Can be both a local or Dropbox paths. :returns: ``True`` if excluded, ``False`` otherwise. :rtype: bool """ @@ -1191,9 +1191,8 @@ def _filter_excluded_changes_local(self, events): for event in events: local_path = get_dest_path(event) - dbx_path = self.to_dbx_path(local_path) - if self.is_excluded(dbx_path): + if self.is_excluded(local_path): events_excluded.append(event) elif self.is_mignore(event): # moved events with an ignored path are From 509a2a3ba76c26a519bcf3faf275309c6d2c67cb Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Thu, 7 May 2020 12:54:26 +0200 Subject: [PATCH 004/101] improved locking --- maestral/main.py | 6 +- maestral/sync.py | 392 +++++++++++++++++++++++++-------------------- tests/test_main.py | 2 +- tests/test_sync.py | 2 +- 4 files changed, 219 insertions(+), 183 deletions(-) diff --git a/maestral/main.py b/maestral/main.py index 475ed927f..767735d6f 100644 --- a/maestral/main.py +++ b/maestral/main.py @@ -604,7 +604,7 @@ def syncing(self): else: return (self.monitor.syncing.is_set() or self.monitor.startup.is_set() - or self.sync.lock.locked()) + or self.sync.busy()) @property def paused(self): @@ -614,7 +614,7 @@ def paused(self): if self.pending_link: return False else: - return self.monitor.paused_by_user.is_set() and not self.sync.lock.locked() + return self.monitor.paused_by_user.is_set() and not self.sync.busy() @property def running(self): @@ -626,7 +626,7 @@ def running(self): if self.pending_link: return False else: - return self.monitor.running.is_set() or self.sync.lock.locked() + return self.monitor.running.is_set() or self.sync.busy() @property def connected(self): diff --git a/maestral/sync.py b/maestral/sync.py index 047c5732e..3d8864059 100644 --- a/maestral/sync.py +++ b/maestral/sync.py @@ -19,7 +19,7 @@ import tempfile import random import json -from threading import Thread, Event, Lock, RLock, current_thread +from threading import Thread, Event, RLock, current_thread from concurrent.futures import ThreadPoolExecutor, as_completed from queue import Queue, Empty from collections import abc @@ -426,10 +426,9 @@ class UpDownSync: :param MaestralApiClient client: Dropbox API client instance. """ - lock = Lock() + sync_lock = RLock() _rev_lock = RLock() - _last_sync_lock = RLock() _max_history = 30 _num_threads = min(32, os.cpu_count() * 3) @@ -494,10 +493,12 @@ def dropbox_path(self): @dropbox_path.setter def dropbox_path(self, path): """Setter: dropbox_path""" - self._dropbox_path = path - self._mignore_path = osp.join(self._dropbox_path, MIGNORE_FILE) - self._file_cache_path = osp.join(self._dropbox_path, FILE_CACHE) - self._conf.set('main', 'path', path) + + with self.sync_lock: + self._dropbox_path = path + self._mignore_path = osp.join(self._dropbox_path, MIGNORE_FILE) + self._file_cache_path = osp.join(self._dropbox_path, FILE_CACHE) + self._conf.set('main', 'path', path) @property def rev_file_path(self): @@ -521,9 +522,11 @@ def excluded_items(self): @excluded_items.setter def excluded_items(self, folder_list): """Setter: excluded_items""" - clean_list = self.clean_excluded_items_list(folder_list) - self._excluded_items = clean_list - self._conf.set('main', 'excluded_items', clean_list) + + with self.sync_lock: + clean_list = self.clean_excluded_items_list(folder_list) + self._excluded_items = clean_list + self._conf.set('main', 'excluded_items', clean_list) @staticmethod def clean_excluded_items_list(folder_list): @@ -572,8 +575,9 @@ def last_cursor(self): @last_cursor.setter def last_cursor(self, cursor): """Setter: last_cursor""" - self._state.set('sync', 'cursor', cursor) - logger.debug('Remote cursor saved: %s', cursor) + with self.sync_lock: + self._state.set('sync', 'cursor', cursor) + logger.debug('Remote cursor saved: %s', cursor) @property def last_sync(self): @@ -584,8 +588,9 @@ def last_sync(self): @last_sync.setter def last_sync(self, last_sync): """Setter: last_sync""" - logger.debug('Local cursor saved: %s', last_sync) - self._state.set('sync', 'lastsync', last_sync) + with self.sync_lock: + logger.debug('Local cursor saved: %s', last_sync) + self._state.set('sync', 'lastsync', last_sync) @property def last_reindex(self): @@ -606,9 +611,8 @@ def get_last_sync_for_path(self, dbx_path): :returns: Time of last sync. :rtype: float """ - with self._last_sync_lock: - dbx_path = dbx_path.lower() - return max(self._last_sync_for_path.get(dbx_path, 0.0), self.last_sync) + dbx_path = dbx_path.lower() + return max(self._last_sync_for_path.get(dbx_path, 0.0), self.last_sync) def set_last_sync_for_path(self, dbx_path, last_sync): """ @@ -617,15 +621,14 @@ def set_last_sync_for_path(self, dbx_path, last_sync): :param str dbx_path: Path relative to Dropbox folder. :param float last_sync: Time of last sync. """ - with self._last_sync_lock: - dbx_path = dbx_path.lower() - if last_sync == 0.0: - try: - del self._last_sync_for_path[dbx_path] - except KeyError: - pass - else: - self._last_sync_for_path[dbx_path] = last_sync + dbx_path = dbx_path.lower() + if last_sync == 0.0: + try: + del self._last_sync_for_path[dbx_path] + except KeyError: + pass + else: + self._last_sync_for_path[dbx_path] = last_sync # ==== rev file management =========================================================== @@ -690,7 +693,8 @@ def set_local_rev(self, dbx_path, rev): def _clean_and_save_rev_file(self): """Cleans the revision index from duplicate entries and keeps only the last entry for any individual path. Then saves the index to the drive.""" - self._save_rev_dict_to_file() + with self._rev_lock: + self._save_rev_dict_to_file() def clear_rev_index(self): """Clears the revision index.""" @@ -1052,6 +1056,21 @@ def _slow_down(self): while cpu_usage > self._max_cpu_percent: cpu_usage = cpu_usage_percent(0.5 + 2 * random.random()) + def busy(self): + """ + Checks if we are currently syncing. + + :returns: ``True`` if :attr:`sync.sync_lock` cannot be aquired, ``False`` + otherwise. + :rtype: bool + """ + + idle = self.sync_lock.acquire(blocking=False) + if idle: + self.sync_lock.release() + + return not idle + # ==== Upload sync =================================================================== def upload_local_changes_while_inactive(self): @@ -1060,22 +1079,24 @@ def upload_local_changes_while_inactive(self): Call this method when resuming sync. """ - logger.info('Indexing local changes...') + with self.sync_lock: - try: - events, local_cursor = self._get_local_changes_while_inactive() - logger.debug('Retrieved local changes:\n%s', pprint.pformat(events)) - events = self._clean_local_events(events) - except FileNotFoundError: - self.ensure_dropbox_folder_present() - return + logger.info('Indexing local changes...') - if len(events) > 0: - self.apply_local_changes(events, local_cursor) - logger.debug('Uploaded local changes while inactive') - else: - self.last_sync = local_cursor - logger.debug('No local changes while inactive') + try: + events, local_cursor = self._get_local_changes_while_inactive() + logger.debug('Retrieved local changes:\n%s', pprint.pformat(events)) + events = self._clean_local_events(events) + except FileNotFoundError: + self.ensure_dropbox_folder_present() + return + + if len(events) > 0: + self.apply_local_changes(events, local_cursor) + logger.debug('Uploaded local changes while inactive') + else: + self.last_sync = local_cursor + logger.debug('No local changes while inactive') def _get_local_changes_while_inactive(self): @@ -1475,74 +1496,76 @@ def apply_local_changes(self, events, local_cursor): :param float local_cursor: Time stamp of last event in ``events``. """ - events, _ = self._filter_excluded_changes_local(events) + with self.sync_lock: - sorted_events = dict(deleted=[], dir_created=[], dir_moved=[], file=[]) - for e in events: - if e.event_type == EVENT_TYPE_DELETED: - sorted_events['deleted'].append(e) - elif e.is_directory and e.event_type == EVENT_TYPE_CREATED: - sorted_events['dir_created'].append(e) - elif e.is_directory and e.event_type == EVENT_TYPE_MOVED: - sorted_events['dir_moved'].append(e) - elif not e.is_directory and e.event_type != EVENT_TYPE_DELETED: - sorted_events['file'].append(e) - - # update queues - for e in itertools.chain(*sorted_events.values()): - self.queued_for_upload.put(get_dest_path(e)) - - success = [] - - # apply deleted events first, folder moved events second - # neither event type requires an actual upload - if sorted_events['deleted']: - logger.info('Uploading deletions...') + events, _ = self._filter_excluded_changes_local(events) - last_emit = time.time() - with ThreadPoolExecutor(max_workers=self._num_threads, - thread_name_prefix='maestral-upload-pool') as executor: - fs = (executor.submit(self.create_remote_entry, e) - for e in sorted_events['deleted']) - n_files = len(sorted_events['deleted']) - for f, n in zip(as_completed(fs), range(1, n_files + 1)): - if time.time() - last_emit > 1 or n in (1, n_files): - # emit message at maximum every second - logger.info(f'Deleting {n}/{n_files}...') - last_emit = time.time() - success.append(f.result()) - - if sorted_events['dir_moved']: - logger.info('Moving folders...') - - for event in sorted_events['dir_moved']: - logger.info(f'Moving {event.src_path}...') - res = self.create_remote_entry(event) - success.append(res) - - # apply file created events in parallel since order does not matter - last_emit = time.time() - with ThreadPoolExecutor(max_workers=self._num_threads, - thread_name_prefix='maestral-upload-pool') as executor: - fs = (executor.submit(self.create_remote_entry, e) for e in - itertools.chain(sorted_events['dir_created'], sorted_events['file'])) - n_files = len(sorted_events['dir_created']) + len(sorted_events['file']) - for f, n in zip(as_completed(fs), range(1, n_files + 1)): - if time.time() - last_emit > 1 or n in (1, n_files): - # emit message at maximum every second - logger.info(f'Uploading {n}/{n_files}...') - last_emit = time.time() - success.append(f.result()) - - # bookkeeping - - if all(success): - self.last_sync = local_cursor - - self._clean_and_save_rev_file() + sorted_events = dict(deleted=[], dir_created=[], dir_moved=[], file=[]) + for e in events: + if e.event_type == EVENT_TYPE_DELETED: + sorted_events['deleted'].append(e) + elif e.is_directory and e.event_type == EVENT_TYPE_CREATED: + sorted_events['dir_created'].append(e) + elif e.is_directory and e.event_type == EVENT_TYPE_MOVED: + sorted_events['dir_moved'].append(e) + elif not e.is_directory and e.event_type != EVENT_TYPE_DELETED: + sorted_events['file'].append(e) + + # update queues + for e in itertools.chain(*sorted_events.values()): + self.queued_for_upload.put(get_dest_path(e)) + + success = [] + + # apply deleted events first, folder moved events second + # neither event type requires an actual upload + if sorted_events['deleted']: + logger.info('Uploading deletions...') + + last_emit = time.time() + with ThreadPoolExecutor(max_workers=self._num_threads, + thread_name_prefix='maestral-upload-pool') as executor: + fs = (executor.submit(self._create_remote_entry, e) + for e in sorted_events['deleted']) + n_files = len(sorted_events['deleted']) + for f, n in zip(as_completed(fs), range(1, n_files + 1)): + if time.time() - last_emit > 1 or n in (1, n_files): + # emit message at maximum every second + logger.info(f'Deleting {n}/{n_files}...') + last_emit = time.time() + success.append(f.result()) + + if sorted_events['dir_moved']: + logger.info('Moving folders...') + + for event in sorted_events['dir_moved']: + logger.info(f'Moving {event.src_path}...') + res = self._create_remote_entry(event) + success.append(res) + + # apply file created events in parallel since order does not matter + last_emit = time.time() + with ThreadPoolExecutor(max_workers=self._num_threads, + thread_name_prefix='maestral-upload-pool') as executor: + fs = (executor.submit(self._create_remote_entry, e) for e in + itertools.chain(sorted_events['dir_created'], sorted_events['file'])) + n_files = len(sorted_events['dir_created']) + len(sorted_events['file']) + for f, n in zip(as_completed(fs), range(1, n_files + 1)): + if time.time() - last_emit > 1 or n in (1, n_files): + # emit message at maximum every second + logger.info(f'Uploading {n}/{n_files}...') + last_emit = time.time() + success.append(f.result()) + + # bookkeeping + + if all(success): + self.last_sync = local_cursor + + self._clean_and_save_rev_file() @catch_sync_issues(download=False) - def create_remote_entry(self, event): + def _create_remote_entry(self, event): """ Applies a local file system event to the remote Dropbox and clears any existing sync errors belonging to that path. Any :class:`errors.MaestralApiError` will be @@ -1930,42 +1953,45 @@ def get_remote_folder(self, dbx_path='/', ignore_excluded=True): :rtype: bool """ - dbx_path = dbx_path or '/' - is_dbx_root = dbx_path == '/' - success = [] + with self.sync_lock: - if is_dbx_root: - logger.info('Downloading your Dropbox') - else: - logger.info('Downloading %s', dbx_path) + dbx_path = dbx_path or '/' + is_dbx_root = dbx_path == '/' + success = [] - if not any(is_child(folder, dbx_path) for folder in self.excluded_items): - # if there are no excluded subfolders, index and download all at once - ignore_excluded = False + if is_dbx_root: + logger.info('Downloading your Dropbox') + else: + logger.info('Downloading %s', dbx_path) + + if not any(is_child(folder, dbx_path) for folder in self.excluded_items): + # if there are no excluded subfolders, index and download all at once + ignore_excluded = False - # get a cursor for the folder - cursor = self.client.get_latest_cursor(dbx_path) + # get a cursor for the folder + cursor = self.client.get_latest_cursor(dbx_path) - root_result = self.client.list_folder(dbx_path, recursive=(not ignore_excluded), - include_deleted=False) + root_result = self.client.list_folder(dbx_path, + recursive=(not ignore_excluded), + include_deleted=False) - # download top-level folders / files first - logger.info(SYNCING) - _, s = self.apply_remote_changes(root_result, save_cursor=False) - success.append(s) + # download top-level folders / files first + logger.info(SYNCING) + _, s = self.apply_remote_changes(root_result, save_cursor=False) + success.append(s) - if ignore_excluded: - # download sub-folders if not excluded - for entry in root_result.entries: - if isinstance(entry, FolderMetadata) and not self.is_excluded_by_user( - entry.path_display): - success.append(self.get_remote_folder(entry.path_display)) + if ignore_excluded: + # download sub-folders if not excluded + for entry in root_result.entries: + if isinstance(entry, FolderMetadata) and not self.is_excluded_by_user( + entry.path_display): + success.append(self.get_remote_folder(entry.path_display)) - if is_dbx_root: - self.last_cursor = cursor - self.last_reindex = time.time() + if is_dbx_root: + self.last_cursor = cursor + self.last_reindex = time.time() - return all(success) + return all(success) def get_remote_item(self, dbx_path): """ @@ -1984,19 +2010,22 @@ def get_remote_item(self, dbx_path): :returns: ``True`` on success, ``False`` otherwise. :rtype: bool """ - self.pending_downloads.add(dbx_path) - md = self.client.get_metadata(dbx_path, include_deleted=True) - if isinstance(md, FolderMetadata): - res = self.get_remote_folder(dbx_path) - else: # FileMetadata or DeletedMetadata - with InQueue(self.queue_downloading, md.path_display): - res = self.create_local_entry(md) + with self.sync_lock: + + self.pending_downloads.add(dbx_path) + md = self.client.get_metadata(dbx_path, include_deleted=True) - if res is True: - self.pending_downloads.discard(dbx_path) + if isinstance(md, FolderMetadata): + res = self.get_remote_folder(dbx_path) + else: # FileMetadata or DeletedMetadata + with InQueue(self.queue_downloading, md.path_display): + res = self._create_local_entry(md) - return res + if res is True: + self.pending_downloads.discard(dbx_path) + + return res def wait_for_remote_changes(self, last_cursor, timeout=40, delay=2): """ @@ -2100,14 +2129,14 @@ def apply_remote_changes(self, changes, save_cursor=True): if deleted: logger.info('Applying deletions...') for item in deleted: - res = self.create_local_entry(item) + res = self._create_local_entry(item) downloaded.append(res) # create local folders, start with top-level and work your way down if folders: logger.info('Creating folders...') for folder in folders: - res = self.create_local_entry(folder) + res = self._create_local_entry(folder) downloaded.append(res) # apply created files @@ -2115,7 +2144,7 @@ def apply_remote_changes(self, changes, save_cursor=True): last_emit = time.time() with ThreadPoolExecutor(max_workers=self._num_threads, thread_name_prefix='maestral-download-pool') as executor: - fs = (executor.submit(self.create_local_entry, file) for file in files) + fs = (executor.submit(self._create_local_entry, file) for file in files) for f, n in zip(as_completed(fs), range(1, n_files + 1)): if time.time() - last_emit > 1 or n in (1, n_files): # emit messages at maximum every second @@ -2132,7 +2161,7 @@ def apply_remote_changes(self, changes, save_cursor=True): return [entry for entry in downloaded if isinstance(entry, Metadata)], success - def check_download_conflict(self, md): + def _check_download_conflict(self, md): """ Check if a local item is conflicting with remote change. The equivalent check when uploading and a change will be carried out by Dropbox itself. @@ -2380,7 +2409,7 @@ def _clean_remote_changes(self, changes): return changes @catch_sync_issues(download=True) - def create_local_entry(self, entry): + def _create_local_entry(self, entry): """ Applies a file change from Dropbox servers to the local Dropbox folder. Any :class:`errors.MaestralApiError` will be caught and logged as appropriate. @@ -2407,7 +2436,7 @@ def create_local_entry(self, entry): with InQueue(self.queue_downloading, entry.path_display): - conflict_check = self.check_download_conflict(entry) + conflict_check = self._check_download_conflict(entry) applied = None @@ -2426,7 +2455,7 @@ def create_local_entry(self, entry): # re-check for conflict and move the conflict # out of the way if anything has changed - if self.check_download_conflict(entry) == Conflict.Conflict: + if self._check_download_conflict(entry) == Conflict.Conflict: new_local_path = generate_cc_name(local_path) event_cls = DirMovedEvent if osp.isdir(local_path) else FileMovedEvent with self.fs_events.ignore(event_cls(local_path, new_local_path)): @@ -2593,11 +2622,12 @@ def download_worker(sync, syncing, running, connected): try: has_changes = sync.wait_for_remote_changes(sync.last_cursor) - if not (running.is_set() and syncing.is_set()): - continue + with sync.sync_lock: - if has_changes: - with sync.lock: + if not (running.is_set() and syncing.is_set()): + continue + + if has_changes: logger.info(SYNCING) changes = sync.list_remote_changes(sync.last_cursor) @@ -2606,9 +2636,9 @@ def download_worker(sync, syncing, running, connected): logger.info(IDLE) - sync.client.get_space_usage() + sync.client.get_space_usage() - gc.collect() + gc.collect() except ConnectionError: syncing.clear() @@ -2635,17 +2665,19 @@ def download_worker_added_item(sync, syncing, running, connected): syncing.wait() - dbx_path = sync.queued_newly_included_downloads.get() + try: + dbx_path = sync.queued_newly_included_downloads.get() - if not (running.is_set() and syncing.is_set()): - sync.pending_downloads.add(dbx_path) - continue + with sync.sync_lock: + if not (running.is_set() and syncing.is_set()): + sync.pending_downloads.add(dbx_path) + continue - try: - with sync.lock: sync.get_remote_item(dbx_path) + logger.info(IDLE) + gc.collect() - logger.info(IDLE) + except ConnectionError: syncing.clear() connected.clear() @@ -2674,18 +2706,18 @@ def upload_worker(sync, syncing, running, connected): try: events, local_cursor = sync.wait_for_local_changes() - if not (running.is_set() and syncing.is_set()): - continue + with sync.sync_lock: + if not (running.is_set() and syncing.is_set()): + continue - if len(events) > 0: - with sync.lock: + if len(events) > 0: logger.info(SYNCING) sync.apply_local_changes(events, local_cursor) logger.info(IDLE) - gc.collect() - else: - sync.last_sync = local_cursor + gc.collect() + else: + sync.last_sync = local_cursor except ConnectionError: syncing.clear() @@ -2715,7 +2747,7 @@ def startup_worker(sync, syncing, running, connected, startup, paused_by_user): startup.wait() try: - with sync.lock: + with sync.sync_lock: # run / resume initial download # local changes during this download will be registered # by the local FileSystemObserver but only uploaded after @@ -2756,12 +2788,12 @@ def startup_worker(sync, syncing, running, connected, startup, paused_by_user): gc.collect() - if not paused_by_user.is_set(): - syncing.set() + if not paused_by_user.is_set(): + syncing.set() - startup.clear() + startup.clear() - logger.info(IDLE) + logger.info(IDLE) except ConnectionError: syncing.clear() @@ -2979,7 +3011,7 @@ def idle_time(self): def reset_sync_state(self): - if self.syncing.is_set() or self.startup.is_set() or self.sync.lock.locked(): + if self.syncing.is_set() or self.startup.is_set() or self.sync.busy(): raise RuntimeError('Cannot reset sync state while syncing.') self.sync.last_cursor = '' @@ -3011,8 +3043,12 @@ def rebuild_index(self): self.resume() def _wait_for_idle(self): - self.sync.lock.acquire() - self.sync.lock.release() + + with self.sync.sync_lock: + pass + + self.sync.sync_lock.acquire() + self.sync.sync_lock.release() def _threads_alive(self): """Returns ``True`` if all threads are alive, ``False`` otherwise.""" diff --git a/tests/test_main.py b/tests/test_main.py index 5c114bd6c..5e677cb60 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -91,7 +91,7 @@ def wait_for_idle(self, minimum=4): t0 = time.time() while time.time() - t0 < minimum: - if self.m.sync.lock.locked(): + if self.m.sync.busy(): self.m.monitor._wait_for_idle() t0 = time.time() else: diff --git a/tests/test_sync.py b/tests/test_sync.py index 39cfe49a5..97f4cc2e5 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -451,7 +451,7 @@ def wait_for_idle(self, minimum=4): t0 = time.time() while time.time() - t0 < minimum: - if self.m.sync.lock.locked(): + if self.m.sync.busy(): self.m.monitor._wait_for_idle() t0 = time.time() else: From b09259b36a8722066836f5cd2a87c848029aacd6 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Thu, 7 May 2020 13:27:09 +0200 Subject: [PATCH 005/101] [sync] reordered methods and updated doc strings --- maestral/sync.py | 378 +++++++++++++++++++++++------------------------ 1 file changed, 188 insertions(+), 190 deletions(-) diff --git a/maestral/sync.py b/maestral/sync.py index 3d8864059..607936b16 100644 --- a/maestral/sync.py +++ b/maestral/sync.py @@ -356,12 +356,12 @@ class UpDownSync: Notes on event processing: - Remote events come in three types, DeletedMetadata, FolderMetadata and FileMetadata. + Remote events come in three types: DeletedMetadata, FolderMetadata and FileMetadata. The Dropbox API does not differentiate between created, moved or modified events. - Maestral processes the events as follows: + Maestral processes remote events as follows: 1) ``_clean_remote_changes``: Combine multiple events per file path into one. This - is rarely necessary, Dropbox typically already provides a only single event per + is rarely necessary, Dropbox typically already provides only a single event per path but this is not guaranteed and may change. One exception is sharing a folder: This is done by removing the folder from Dropbox and re-mounting it as a shared folder and produces at least one DeletedMetadata and one FolderMetadata @@ -370,50 +370,48 @@ class UpDownSync: replacing a folder with a file, we explicitly generate the necessary DeletedMetadata here to simplify conflict resolution. 2) ``_filter_excluded_changes_remote``: Filters out events that occurred for files - or folders excluded by selective sync as well as hard-coded file names which are - always excluded (e.g., `.DS_Store`). + or folders excluded by selective sync, hard-coded file names which are always + excluded (e.g., '.DS_Store') and items in our cache path. 3) ``apply_remote_changes``: Sorts all events hierarchically, with top-level events coming first. Deleted and folder events are processed in order, file events in parallel with up to 6 worker threads. - 4) ``create_local_entry``: Checks for sync conflicts by comparing the file version, - as determined from its rev number, with our locally saved rev. We assign folders + 4) ``_create_local_entry``: Checks for sync conflicts by comparing the file version, + as determined from its remote rev, with our locally saved rev. We assign folders a rev of 'folder' and deleted / non-existent items a rev of None. If revs are equal, the local item is the same or newer as an Dropbox, no download / deletion occurs. If revs are different, we compare content hashes. Folders are assigned a hash of 'folder'. If hashes are equal, no download occurs. Finally we check if the local item has been modified since the last download sync. In case of a - folder, we take newest change of any of its children. If the local item has not - been modified since the last sync, it will be overridden. Otherwise, we create a - conflicting copy. + folder, we take the newest change of any of its children. If the local item has + not been modified since the last sync, it will be replaced. Otherwise, we + create a conflicting copy. Local file events come in eight types: For both files and folders we collect created, moved, modified and deleted events. They are processed as follows: 1) ``FSEventHandler``: Our file system event handler tries to discard any events - that originate from Maestral itself, e.g., from downloads. In case of a moved - event, if only one of the two paths should be ignored at this stage, the event - will be split into a deleted event (old path) and a created event (new path) and - one of the two will be ignored. + that originate from Maestral itself, e.g., from downloads or from renaming + conflicts. 2) We wait until no new changes happen for at least 1.0 sec. 3) ``_filter_excluded_changes_local``: Filters out events ignored by a `.mignore` - pattern as well as hard-coded file names which are always excluded. + pattern as well as hard-coded file names and changes in our cache path. 4) ``_clean_local_events``: Cleans up local events in two stages. First, multiple - events per path are combined into a single event to reproduce the file changes. - The only exceptions is when the item type changes from file to folder or vice - versa: in this case, both deleted and created events are kept. Second, when a - whole folder is moved or deleted, we discard the moved and deleted events of its - children. - 4) ``apply_local_changes``: Sort local changes hierarchically and apply events in - the order of deleted, folders and files. File uploads will be carrier out in - parallel with up to 6 threads. Conflict resolution and upload / move / deletion - will be handled by ``create_remote_entry`` as follows: + events per path are combined into a single event which reproduces the file + changes. The only exceptions is when the item type changes from file to folder or + vice versa: in this case, both deleted and created events are kept. Second, when + a whole folder is moved or deleted, we discard the moved and deleted events of + its children. + 4) ``apply_local_changes``: Sorts local changes hierarchically and applies events in + the order of deleted, folders and files. Deletions and file uploads will be + carried out in parallel with up to 6 threads. Conflict resolution and upload + / move / deletion will be handled by ``create_remote_entry`` as follows: 5) Conflict resolution: For created and moved events, we check if the new path has been excluded by the user with selective sync but still exists on Dropbox. If yes, it will be renamed by appending "(selective sync conflict)". On case- sensitive file systems, we check if the new path differs only in casing from an existing path. If yes, it will be renamed by appending "(case conflict)". If a file has been replaced with a folder or vice versa, check if any un-synced - changes will be lost replacing the remote item. Create a conflicting copy if + changes will be lost by replacing the remote item. Create a conflicting copy if necessary. Dropbox does not handle conflict resolution for us in this case. 7) For created or modified files, check if the local content hash equals the remote content hash. If yes, we don't upload but update our rev number. @@ -824,12 +822,12 @@ def mignore_path(self): @property def mignore_rules(self): """List of mignore rules following git wildmatch syntax.""" - if self.get_ctime(self.mignore_path) != self._mignore_ctime_loaded: + if self._get_ctime(self.mignore_path) != self._mignore_ctime_loaded: self._mignore_rules = self._load_mignore_rules_form_file() return self._mignore_rules def _load_mignore_rules_form_file(self): - self._mignore_ctime_loaded = self.get_ctime(self.mignore_path) + self._mignore_ctime_loaded = self._get_ctime(self.mignore_path) try: with open(self.mignore_path) as f: spec = f.read() @@ -1197,6 +1195,82 @@ def wait_for_local_changes(self, timeout=5, delay=1): return self._clean_local_events(events), local_cursor + def apply_local_changes(self, events, local_cursor): + """ + Applies locally detected changes to the remote Dropbox. + + :param iterable events: List of local file system events. + :param float local_cursor: Time stamp of last event in ``events``. + """ + + with self.sync_lock: + + events, _ = self._filter_excluded_changes_local(events) + + sorted_events = dict(deleted=[], dir_created=[], dir_moved=[], file=[]) + for e in events: + if e.event_type == EVENT_TYPE_DELETED: + sorted_events['deleted'].append(e) + elif e.is_directory and e.event_type == EVENT_TYPE_CREATED: + sorted_events['dir_created'].append(e) + elif e.is_directory and e.event_type == EVENT_TYPE_MOVED: + sorted_events['dir_moved'].append(e) + elif not e.is_directory and e.event_type != EVENT_TYPE_DELETED: + sorted_events['file'].append(e) + + # update queues + for e in itertools.chain(*sorted_events.values()): + self.queued_for_upload.put(get_dest_path(e)) + + success = [] + + # apply deleted events first, folder moved events second + # neither event type requires an actual upload + if sorted_events['deleted']: + logger.info('Uploading deletions...') + + last_emit = time.time() + with ThreadPoolExecutor(max_workers=self._num_threads, + thread_name_prefix='maestral-upload-pool') as executor: + fs = (executor.submit(self._create_remote_entry, e) + for e in sorted_events['deleted']) + n_files = len(sorted_events['deleted']) + for f, n in zip(as_completed(fs), range(1, n_files + 1)): + if time.time() - last_emit > 1 or n in (1, n_files): + # emit message at maximum every second + logger.info(f'Deleting {n}/{n_files}...') + last_emit = time.time() + success.append(f.result()) + + if sorted_events['dir_moved']: + logger.info('Moving folders...') + + for event in sorted_events['dir_moved']: + logger.info(f'Moving {event.src_path}...') + res = self._create_remote_entry(event) + success.append(res) + + # apply file created events in parallel since order does not matter + last_emit = time.time() + with ThreadPoolExecutor(max_workers=self._num_threads, + thread_name_prefix='maestral-upload-pool') as executor: + fs = (executor.submit(self._create_remote_entry, e) for e in + itertools.chain(sorted_events['dir_created'], sorted_events['file'])) + n_files = len(sorted_events['dir_created']) + len(sorted_events['file']) + for f, n in zip(as_completed(fs), range(1, n_files + 1)): + if time.time() - last_emit > 1 or n in (1, n_files): + # emit message at maximum every second + logger.info(f'Uploading {n}/{n_files}...') + last_emit = time.time() + success.append(f.result()) + + # bookkeeping + + if all(success): + self.last_sync = local_cursor + + self._clean_and_save_rev_file() + def _filter_excluded_changes_local(self, events): """ Checks for and removes file events referring to items which are excluded from @@ -1488,82 +1562,6 @@ def _handle_selective_sync_conflict(self, event): else: return False - def apply_local_changes(self, events, local_cursor): - """ - Applies locally detected changes to the remote Dropbox. - - :param iterable events: List of local file system events. - :param float local_cursor: Time stamp of last event in ``events``. - """ - - with self.sync_lock: - - events, _ = self._filter_excluded_changes_local(events) - - sorted_events = dict(deleted=[], dir_created=[], dir_moved=[], file=[]) - for e in events: - if e.event_type == EVENT_TYPE_DELETED: - sorted_events['deleted'].append(e) - elif e.is_directory and e.event_type == EVENT_TYPE_CREATED: - sorted_events['dir_created'].append(e) - elif e.is_directory and e.event_type == EVENT_TYPE_MOVED: - sorted_events['dir_moved'].append(e) - elif not e.is_directory and e.event_type != EVENT_TYPE_DELETED: - sorted_events['file'].append(e) - - # update queues - for e in itertools.chain(*sorted_events.values()): - self.queued_for_upload.put(get_dest_path(e)) - - success = [] - - # apply deleted events first, folder moved events second - # neither event type requires an actual upload - if sorted_events['deleted']: - logger.info('Uploading deletions...') - - last_emit = time.time() - with ThreadPoolExecutor(max_workers=self._num_threads, - thread_name_prefix='maestral-upload-pool') as executor: - fs = (executor.submit(self._create_remote_entry, e) - for e in sorted_events['deleted']) - n_files = len(sorted_events['deleted']) - for f, n in zip(as_completed(fs), range(1, n_files + 1)): - if time.time() - last_emit > 1 or n in (1, n_files): - # emit message at maximum every second - logger.info(f'Deleting {n}/{n_files}...') - last_emit = time.time() - success.append(f.result()) - - if sorted_events['dir_moved']: - logger.info('Moving folders...') - - for event in sorted_events['dir_moved']: - logger.info(f'Moving {event.src_path}...') - res = self._create_remote_entry(event) - success.append(res) - - # apply file created events in parallel since order does not matter - last_emit = time.time() - with ThreadPoolExecutor(max_workers=self._num_threads, - thread_name_prefix='maestral-upload-pool') as executor: - fs = (executor.submit(self._create_remote_entry, e) for e in - itertools.chain(sorted_events['dir_created'], sorted_events['file'])) - n_files = len(sorted_events['dir_created']) + len(sorted_events['file']) - for f, n in zip(as_completed(fs), range(1, n_files + 1)): - if time.time() - last_emit > 1 or n in (1, n_files): - # emit message at maximum every second - logger.info(f'Uploading {n}/{n_files}...') - last_emit = time.time() - success.append(f.result()) - - # bookkeeping - - if all(success): - self.last_sync = local_cursor - - self._clean_and_save_rev_file() - @catch_sync_issues(download=False) def _create_remote_entry(self, event): """ @@ -2058,29 +2056,6 @@ def list_remote_changes(self, last_cursor): logger.debug('Cleaned remote changes:\n%s', entries_to_str(clean_changes.entries)) return clean_changes - def _filter_excluded_changes_remote(self, changes): - """Removes all excluded items from the given list of changes. - - :param changes: :class:`dropbox.files.ListFolderResult` instance. - :returns: (``changes_filtered``, ``changes_discarded``) - :rtype: tuple[:class:`dropbox.files.ListFolderResult`] - """ - entries_filtered = [] - entries_discarded = [] - - for e in changes.entries: - if self.is_excluded_by_user(e.path_lower) or self.is_excluded(e.path_lower): - entries_discarded.append(e) - else: - entries_filtered.append(e) - - changes_filtered = dropbox.files.ListFolderResult( - entries=entries_filtered, cursor=changes.cursor, has_more=False) - changes_discarded = dropbox.files.ListFolderResult( - entries=entries_discarded, cursor=changes.cursor, has_more=False) - - return changes_filtered, changes_discarded - def apply_remote_changes(self, changes, save_cursor=True): """ Applies remote changes to local folder. Call this on the result of @@ -2161,6 +2136,89 @@ def apply_remote_changes(self, changes, save_cursor=True): return [entry for entry in downloaded if isinstance(entry, Metadata)], success + def notify_user(self, changes): + """ + Sends system notification for file changes. + + :param list changes: List of Dropbox metadata which has been applied locally. + """ + + # get number of remote changes + n_changed = len(changes) + + if n_changed == 0: + return + + user_name = None + change_type = 'changed' + + # find out who changed the item(s), get the user name if its only a single user + dbid_list = set(self._get_modified_by_dbid(md) for md in changes) + if len(dbid_list) == 1: + # all files have been modified by the same user + dbid = dbid_list.pop() + if dbid == self._conf.get('account', 'account_id'): + user_name = 'You' + else: + account_info = self.client.get_account_info(dbid) + user_name = account_info.name.display_name + + if n_changed == 1: + # display user name, file name, and type of change + md = changes[0] + file_name = os.path.basename(md.path_display) + + if isinstance(md, DeletedMetadata): + change_type = 'removed' + elif isinstance(md, FileMetadata): + revs = self.client.list_revisions(md.path_lower, limit=2) + is_new_file = len(revs.entries) == 1 + change_type = 'added' if is_new_file else 'changed' + elif isinstance(md, FolderMetadata): + change_type = 'added' + + else: + # display user name if unique, number of files, and type of change + file_name = f'{n_changed} items' + + if all(isinstance(x, DeletedMetadata) for x in changes): + change_type = 'removed' + elif all(isinstance(x, FolderMetadata) for x in changes): + change_type = 'added' + file_name = f'{n_changed} folders' + elif all(isinstance(x, FileMetadata) for x in changes): + file_name = f'{n_changed} files' + + if user_name: + msg = f'{user_name} {change_type} {file_name}' + else: + msg = f'{file_name} {change_type}' + + self._notifier.notify(msg, level=FILECHANGE) + + def _filter_excluded_changes_remote(self, changes): + """Removes all excluded items from the given list of changes. + + :param changes: :class:`dropbox.files.ListFolderResult` instance. + :returns: (``changes_filtered``, ``changes_discarded``) + :rtype: tuple[:class:`dropbox.files.ListFolderResult`] + """ + entries_filtered = [] + entries_discarded = [] + + for e in changes.entries: + if self.is_excluded_by_user(e.path_lower) or self.is_excluded(e.path_lower): + entries_discarded.append(e) + else: + entries_filtered.append(e) + + changes_filtered = dropbox.files.ListFolderResult( + entries=entries_filtered, cursor=changes.cursor, has_more=False) + changes_discarded = dropbox.files.ListFolderResult( + entries=entries_discarded, cursor=changes.cursor, has_more=False) + + return changes_filtered, changes_discarded + def _check_download_conflict(self, md): """ Check if a local item is conflicting with remote change. The equivalent check when @@ -2214,7 +2272,7 @@ def _check_download_conflict(self, md): logger.debug('Equal content hashes for "%s": no conflict', dbx_path) self.set_local_rev(dbx_path, remote_rev) return Conflict.Identical - elif self.get_ctime(local_path) <= self.get_last_sync_for_path(dbx_path): + elif self._get_ctime(local_path) <= self.get_last_sync_for_path(dbx_path): logger.debug('Ctime is older than last sync for "%s": remote item ' 'is newer', dbx_path) return Conflict.RemoteNewer @@ -2226,7 +2284,7 @@ def _check_download_conflict(self, md): logger.debug('Ctime is newer than last sync for "%s": conflict', dbx_path) return Conflict.Conflict - def get_ctime(self, local_path, ignore_excluded=True): + def _get_ctime(self, local_path, ignore_excluded=True): """ Returns the ctime of a local item or -1.0 if there is nothing at the path. If the item is a directory, return the largest ctime of itself and its children. @@ -2254,66 +2312,6 @@ def get_ctime(self, local_path, ignore_excluded=True): except FileNotFoundError: return -1.0 - def notify_user(self, changes): - """ - Sends system notification for file changes. - - :param list changes: List of Dropbox metadata which has been applied locally. - """ - - # get number of remote changes - n_changed = len(changes) - - if n_changed == 0: - return - - user_name = None - change_type = 'changed' - - # find out who changed the item(s), get the user name if its only a single user - dbid_list = set(self._get_modified_by_dbid(md) for md in changes) - if len(dbid_list) == 1: - # all files have been modified by the same user - dbid = dbid_list.pop() - if dbid == self._conf.get('account', 'account_id'): - user_name = 'You' - else: - account_info = self.client.get_account_info(dbid) - user_name = account_info.name.display_name - - if n_changed == 1: - # display user name, file name, and type of change - md = changes[0] - file_name = os.path.basename(md.path_display) - - if isinstance(md, DeletedMetadata): - change_type = 'removed' - elif isinstance(md, FileMetadata): - revs = self.client.list_revisions(md.path_lower, limit=2) - is_new_file = len(revs.entries) == 1 - change_type = 'added' if is_new_file else 'changed' - elif isinstance(md, FolderMetadata): - change_type = 'added' - - else: - # display user name if unique, number of files, and type of change - file_name = f'{n_changed} items' - - if all(isinstance(x, DeletedMetadata) for x in changes): - change_type = 'removed' - elif all(isinstance(x, FolderMetadata) for x in changes): - change_type = 'added' - file_name = f'{n_changed} folders' - elif all(isinstance(x, FileMetadata) for x in changes): - file_name = f'{n_changed} files' - - if user_name: - msg = f'{user_name} {change_type} {file_name}' - else: - msg = f'{file_name} {change_type}' - - self._notifier.notify(msg, level=FILECHANGE) - def _get_modified_by_dbid(self, md): """ Returns the Dropbox ID of the user who modified a shared item or our own ID if the @@ -2480,7 +2478,7 @@ def _create_local_entry(self, entry): raise os_to_maestral_error(exc, dbx_path=entry.path_display, local_path=local_path) - self.set_last_sync_for_path(entry.path_lower, self.get_ctime(local_path)) + self.set_last_sync_for_path(entry.path_lower, self._get_ctime(local_path)) self.set_local_rev(entry.path_lower, md.rev) logger.debug('Created local file "%s"', entry.path_display) @@ -2518,7 +2516,7 @@ def _create_local_entry(self, entry): raise os_to_maestral_error(exc, dbx_path=entry.path_display, local_path=local_path) - self.set_last_sync_for_path(entry.path_lower, self.get_ctime(local_path)) + self.set_last_sync_for_path(entry.path_lower, self._get_ctime(local_path)) self.set_local_rev(entry.path_lower, 'folder') logger.debug('Created local folder "%s"', entry.path_display) From 30e8a931a278d94ac55744185ac92cf5cc91a419 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Thu, 7 May 2020 19:21:19 +0200 Subject: [PATCH 006/101] [daemon] add timeout to acquiring lock This works around a possible race condition where the lock file has already been created but no PID has been written yet. In this case, `is_pidfile_stale` would return True. --- maestral/daemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maestral/daemon.py b/maestral/daemon.py index 36ab0398b..b477a67b5 100644 --- a/maestral/daemon.py +++ b/maestral/daemon.py @@ -212,7 +212,7 @@ def start_maestral_daemon(config_name='maestral', log_to_stdout=False): # acquire PID lock file try: - lockfile.acquire() + lockfile.acquire(timeout=0.5) except AlreadyLocked: if is_pidfile_stale(lockfile): lockfile.break_lock() From fec90aad2e52980a10f1116e4e61b1d8c871edd3 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Thu, 7 May 2020 19:34:02 +0200 Subject: [PATCH 007/101] [cli] fixes an issue when calling `maestral ls` with an empty folder --- maestral/cli.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/maestral/cli.py b/maestral/cli.py index 54a48c4e5..f1dd8f33c 100755 --- a/maestral/cli.py +++ b/maestral/cli.py @@ -219,9 +219,13 @@ def format_table(rows=None, columns=None, headers=None, padding_right=2): for i, col in enumerate(columns): col.insert(0, headers[i]) - # transpose columns to get rows and vice versa + # transpose rows to get columns columns = list(columns) if columns else list(map(list, zip(*rows))) + # return early if all columns are empty (including headers) + if all(len(col) == 0 for col in columns): + return '' + terminal_width, terminal_height = click.get_terminal_size() available_width = terminal_width - padding_right * len(columns) From 2d71c2f860ede6332b3aa8083fe54ee371b5b264 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Thu, 7 May 2020 19:34:12 +0200 Subject: [PATCH 008/101] bumped to v1.0.1.dev1 --- maestral/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maestral/__init__.py b/maestral/__init__.py index 4449502a6..fcc074f5f 100644 --- a/maestral/__init__.py +++ b/maestral/__init__.py @@ -16,6 +16,6 @@ """ -__version__ = '1.0.0' +__version__ = '1.0.1.dev' __author__ = 'Sam Schott' __url__ = 'https://github.com/SamSchott/maestral' From 53269cc471b8a7b064670b8f2eb2c32de61aee2f Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Thu, 7 May 2020 19:34:19 +0200 Subject: [PATCH 009/101] updated changelog --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37fefa69f..6e011b2f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## v1.0.1.dev1 + +#### Fixed: + +- Fixes an error when listing the contents of an empty directory with `maestral ls`. +- Fixes a possible race condition during daemon startup. +- Fixes an issue which could lead to the local Dropbox folder being moved before syncing + has been paused. + ## v1.0.0 This is the first stable release of Maestral. There have been numerous bug fixes to error From b30142c82127e6d70dcee06334f10bce8892137b Mon Sep 17 00:00:00 2001 From: SamSchott Date: Thu, 7 May 2020 23:48:38 +0200 Subject: [PATCH 010/101] [cli] fix crash when showing update message See #136 --- maestral/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/maestral/cli.py b/maestral/cli.py index f1dd8f33c..f8b039764 100755 --- a/maestral/cli.py +++ b/maestral/cli.py @@ -144,9 +144,9 @@ def check_for_updates(): has_update = Version(__version__) < Version(latest_release) if has_update: - click.secho( + click.echo( f'Maestral v{latest_release} has been released, you have v{__version__}. ' - f'Please use your package manager to update.', fg='orange' + f'Please use your package manager to update.' ) From 4c59e1fb8a59afe7598b8751d40032b425817c47 Mon Sep 17 00:00:00 2001 From: SamSchott Date: Fri, 8 May 2020 00:02:13 +0200 Subject: [PATCH 011/101] Updated changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e011b2f0..eade135a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,9 @@ -## v1.0.1.dev1 +## v1.0.1 #### Fixed: - Fixes an error when listing the contents of an empty directory with `maestral ls`. -- Fixes a possible race condition during daemon startup. +- Fixes a crash of the CLI when trying to display an update message. - Fixes an issue which could lead to the local Dropbox folder being moved before syncing has been paused. From 91fbb42959300a62ed27a79ba033719be55b19ef Mon Sep 17 00:00:00 2001 From: SamSchott Date: Fri, 8 May 2020 00:04:04 +0200 Subject: [PATCH 012/101] Bumped to v1.0.1 --- maestral/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maestral/__init__.py b/maestral/__init__.py index fcc074f5f..0a3c26c4f 100644 --- a/maestral/__init__.py +++ b/maestral/__init__.py @@ -16,6 +16,6 @@ """ -__version__ = '1.0.1.dev' +__version__ = '1.0.1' __author__ = 'Sam Schott' __url__ = 'https://github.com/SamSchott/maestral' From a3007df1ed0abf1b36f8d511d8c9570e206500cb Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Fri, 8 May 2020 11:34:13 +0200 Subject: [PATCH 013/101] only run CI on pull request --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5df1fc937..d52cb7c1d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,6 @@ name: CI -on: [push, pull_request] +on: [pull_request] jobs: flake: From 222388a79c46cfd7c0177d0b77d29c8c29ddfd58 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Fri, 8 May 2020 11:47:19 +0200 Subject: [PATCH 014/101] bumped to v1.0.3.dev1 --- CHANGELOG.md | 7 +++++++ maestral/__init__.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bf311b8f..88d0a075a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## v1.0.3.dev + +#### Fixed: + +- Fixes an issue which could lead to the local Dropbox folder being moved before syncing + has been paused. + ## v1.0.2 This release fixes bugs in the command line interface. diff --git a/maestral/__init__.py b/maestral/__init__.py index 69acef49a..e11848404 100644 --- a/maestral/__init__.py +++ b/maestral/__init__.py @@ -16,6 +16,6 @@ """ -__version__ = '1.0.2' +__version__ = '1.0.3.dev1' __author__ = 'Sam Schott' __url__ = 'https://github.com/SamSchott/maestral' From aa6729de77e2244b5450305da7b6195c68051f56 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Fri, 8 May 2020 12:11:23 +0200 Subject: [PATCH 015/101] [sync] updated doc strings --- maestral/sync.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/maestral/sync.py b/maestral/sync.py index 607936b16..204486cf2 100644 --- a/maestral/sync.py +++ b/maestral/sync.py @@ -1058,8 +1058,7 @@ def busy(self): """ Checks if we are currently syncing. - :returns: ``True`` if :attr:`sync.sync_lock` cannot be aquired, ``False`` - otherwise. + :returns: ``True`` if :attr:`sync_lock` cannot be aquired, ``False`` otherwise. :rtype: bool """ @@ -1999,7 +1998,7 @@ def get_remote_item(self, dbx_path): so that they will be resumed in case Maestral is terminated during the download. If ``dbx_path`` refers to a folder, the download will be handled by :meth:`get_remote_folder`. If it refers to a single file, the download will be - performed by :meth:`create_local_entry`. + performed by :meth:`_create_local_entry`. This method can be used to fetch individual items outside of the regular sync cycle, for instance when including a new file or folder. From 434cff78c46f3d05152229795cd50e57af85d246 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Fri, 8 May 2020 12:52:15 +0200 Subject: [PATCH 016/101] [docs] document private members of sync --- docs/api/sync.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/api/sync.rst b/docs/api/sync.rst index add7bac6a..a1cba43b7 100644 --- a/docs/api/sync.rst +++ b/docs/api/sync.rst @@ -4,4 +4,5 @@ Sync module .. automodule:: sync :members: + :private-members: :show-inheritance: From e6277704e22e2dc911578408931e7c90c99f34e5 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Fri, 8 May 2020 14:29:24 +0200 Subject: [PATCH 017/101] [sync] refactor --- maestral/sync.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/maestral/sync.py b/maestral/sync.py index 204486cf2..f8bbf3068 100644 --- a/maestral/sync.py +++ b/maestral/sync.py @@ -130,8 +130,8 @@ def __init__(self, syncing, startup, sync): self.sync = sync self.sync.fs_events = self - self._ignored_paths = [] - self._ignored_paths_mutex = RLock() + self._ignored_events = [] + self._ignored_events_mutex = RLock() self.local_file_event_queue = Queue() @@ -149,11 +149,11 @@ def ignore(self, *events, recursive=True): itself, for instance during a download or when moving a conflict. :param iterable events: Local events to ignore. - :param bool recursive: If ``True``, all child events of a dirctory event will be + :param bool recursive: If ``True``, all child events of a directory event will be ignored as well. """ - with self._ignored_paths_mutex: + with self._ignored_events_mutex: now = time.time() new_ignores = [] for e in events: @@ -165,25 +165,25 @@ def ignore(self, *events, recursive=True): recursive=recursive and e.is_directory, ) ) - self._ignored_paths.extend(new_ignores) + self._ignored_events.extend(new_ignores) try: yield finally: - with self._ignored_paths_mutex: + with self._ignored_events_mutex: for ignore in new_ignores: ignore['ttl'] = time.time() + self.ignore_timeout - def _expire_ignored_paths(self): + def _expire_ignored_events(self): """Removes all expired ignore entries.""" - with self._ignored_paths_mutex: + with self._ignored_events_mutex: now = time.time() - for ignore in self._ignored_paths.copy(): + for ignore in self._ignored_events.copy(): ttl = ignore['ttl'] if ttl and ttl < now: - self._ignored_paths.remove(ignore) + self._ignored_events.remove(ignore) def _is_ignored(self, event): """ @@ -195,18 +195,18 @@ def _is_ignored(self, event): :rtype: bool """ - with self._ignored_paths_mutex: + with self._ignored_events_mutex: - self._expire_ignored_paths() + self._expire_ignored_events() - for ignore in self._ignored_paths: + for ignore in self._ignored_events: ignore_event = ignore['event'] recursive = ignore['recursive'] if event == ignore_event: if not recursive: - self._ignored_paths.remove(ignore) + self._ignored_events.remove(ignore) return True From 925efb394a15801bff6695ef0f9db430ee54f864 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Fri, 8 May 2020 15:06:47 +0200 Subject: [PATCH 018/101] [sync] further updates to doc strings --- maestral/sync.py | 122 +++++++++++++++++++++++++---------------------- 1 file changed, 64 insertions(+), 58 deletions(-) diff --git a/maestral/sync.py b/maestral/sync.py index f8bbf3068..388a6c2af 100644 --- a/maestral/sync.py +++ b/maestral/sync.py @@ -248,12 +248,13 @@ def on_any_event(self, event): class MaestralStateWrapper(abc.MutableSet): """ - A wrapper for a list in the saved state that implements a MutableSet interface. All - given paths are stored in lower-case, reflecting Dropbox's insensitive file system. + A wrapper for a list of strings in the saved state that implements a MutableSet + interface. All strings are stored as lower-case, reflecting Dropbox's case-insensitive + file system. :param str config_name: Name of config. - :param str section: Section name in state. - :param str option: Option name in state. + :param str section: Section name in state file. + :param str option: Option name in state file. """ _lock = RLock() @@ -306,7 +307,10 @@ def __repr__(self): def catch_sync_issues(download=False): """ Returns a decorator that catches all SyncErrors and logs them. - Should only be used for methods of UpDownSync. + Should only be used to decorate methods of :class:`UpDownSync`. + + :param bool download: If ``True``, sync issues will be added to the `download_errors` + list to retry later. """ def decorator(func): @@ -360,68 +364,70 @@ class UpDownSync: The Dropbox API does not differentiate between created, moved or modified events. Maestral processes remote events as follows: - 1) ``_clean_remote_changes``: Combine multiple events per file path into one. This - is rarely necessary, Dropbox typically already provides only a single event per - path but this is not guaranteed and may change. One exception is sharing a + 1) :meth:`wait_for_remote_changes` blocks until remote changes are available. + 2) :meth:`get_remote_changes` lists all remote changes since the last sync. + 3) :meth:`_clean_remote_changes`: Combines multiple events per file path into one. + This is rarely necessary, Dropbox typically already provides only a single event + per path but this is not guaranteed and may change. One exception is sharing a folder: This is done by removing the folder from Dropbox and re-mounting it as a shared folder and produces at least one DeletedMetadata and one FolderMetadata event. If querying for changes *during* this process, multiple DeletedMetadata - events may be returned. If a File / Folder event implies a type changes, e.g., + events may be returned. If a file / folder event implies a type changes, e.g., replacing a folder with a file, we explicitly generate the necessary DeletedMetadata here to simplify conflict resolution. - 2) ``_filter_excluded_changes_remote``: Filters out events that occurred for files - or folders excluded by selective sync, hard-coded file names which are always - excluded (e.g., '.DS_Store') and items in our cache path. - 3) ``apply_remote_changes``: Sorts all events hierarchically, with top-level events - coming first. Deleted and folder events are processed in order, file events in - parallel with up to 6 worker threads. - 4) ``_create_local_entry``: Checks for sync conflicts by comparing the file version, - as determined from its remote rev, with our locally saved rev. We assign folders - a rev of 'folder' and deleted / non-existent items a rev of None. If revs are - equal, the local item is the same or newer as an Dropbox, no download / deletion - occurs. If revs are different, we compare content hashes. Folders are assigned a - hash of 'folder'. If hashes are equal, no download occurs. Finally we check if - the local item has been modified since the last download sync. In case of a - folder, we take the newest change of any of its children. If the local item has - not been modified since the last sync, it will be replaced. Otherwise, we - create a conflicting copy. + 2) :meth:`_filter_excluded_changes_remote`: Filters out events that occurred for + entries that are excluded by selective sync and hard-coded file names which are + always excluded (e.g., '.DS_Store'). + 3) :meth:`apply_remote_changes`: Sorts all events hierarchically, with top-level + events coming first. Deleted and folder events are processed in order, file + events in parallel with up to 6 worker threads. The actual download is carried + out by :meth:`_create_local_entry`. + 4) :meth:`_create_local_entry`: Checks for sync conflicts by comparing the file + "rev" with our locally saved rev. We assign folders a rev of ``'folder'`` and + deleted / non-existent items a rev of ``None``. If revs are equal, the local item + is the same or newer as an Dropbox, no download / deletion occurs. If revs are + different, we compare content hashes. Folders are assigned a hash of 'folder'. If + hashes are equal, no download occurs. If they are different, we check if the + local item has been modified since the last download sync. In case of a folder, + we take the newest change of any of its children. If the local entry has not been + modified since the last sync, it will be replaced. Otherwise, we create a + conflicting copy. + 5) :meth:`notify_user`: Shows a desktop notification for the remote changes. Local file events come in eight types: For both files and folders we collect created, moved, modified and deleted events. They are processed as follows: - 1) ``FSEventHandler``: Our file system event handler tries to discard any events - that originate from Maestral itself, e.g., from downloads or from renaming - conflicts. - 2) We wait until no new changes happen for at least 1.0 sec. - 3) ``_filter_excluded_changes_local``: Filters out events ignored by a `.mignore` - pattern as well as hard-coded file names and changes in our cache path. - 4) ``_clean_local_events``: Cleans up local events in two stages. First, multiple - events per path are combined into a single event which reproduces the file - changes. The only exceptions is when the item type changes from file to folder or - vice versa: in this case, both deleted and created events are kept. Second, when - a whole folder is moved or deleted, we discard the moved and deleted events of - its children. - 4) ``apply_local_changes``: Sorts local changes hierarchically and applies events in - the order of deleted, folders and files. Deletions and file uploads will be - carried out in parallel with up to 6 threads. Conflict resolution and upload - / move / deletion will be handled by ``create_remote_entry`` as follows: - 5) Conflict resolution: For created and moved events, we check if the new path has - been excluded by the user with selective sync but still exists on Dropbox. If - yes, it will be renamed by appending "(selective sync conflict)". On case- - sensitive file systems, we check if the new path differs only in casing from an - existing path. If yes, it will be renamed by appending "(case conflict)". If a - file has been replaced with a folder or vice versa, check if any un-synced - changes will be lost by replacing the remote item. Create a conflicting copy if - necessary. Dropbox does not handle conflict resolution for us in this case. - 7) For created or modified files, check if the local content hash equals the remote - content hash. If yes, we don't upload but update our rev number. - 8) Upload the changes, specify the rev which we want to replace / delete. If the - remote item is newer (different rev), Dropbox will handle conflict resolution. - 9) Confirm the successful upload and check if Dropbox has renamed the item to a - conflicting copy. In the latter case, apply those changes locally. - 10) Update local revs with the new revs assigned by Dropbox. + 1) :meth:`wait_for_local_changes`: Blocks until local changes were registered by + :class:`FSEventHandler` available and returns those changes. + 2) :meth:`_filter_excluded_changes_local`: Filters out events ignored by a + "mignore" pattern as well as hard-coded file names and changes in our cache path. + 3) :meth:`_clean_local_events`: Cleans up local events in two stages. First, + multiple events per path are combined into a single event which reproduces the + file changes. The only exceptions is when the entry type changes from file to + folder or vice versa: in this case, both deleted and created events are kept. + Second, when a whole folder is moved or deleted, we discard the moved and deleted + events of its children. + 4) :meth:`apply_local_changes`: Sorts local changes hierarchically and applies + events in the order of deleted, folders and files. Deletions and file uploads + will be carried out in parallel with up to 6 threads. Conflict resolution and + the actual upload will be handled by :meth:`_create_remote_entry` as follows: + 5) :meth:`_create_remote_entry`: For created and moved events, we check if the new + path has been excluded by the user with selective sync but still exists on + Dropbox. If yes, it will be renamed by appending "(selective sync conflict)". On + case-sensitive file systems, we check if the new path differs only in casing from + an existing path. If yes, it will be renamed by appending "(case conflict)". If a + file has been replaced with a folder or vice versa, we check if any un-synced + changes will be lost by replacing the remote item and create a conflicting copy + if necessary. Dropbox does not handle conflict resolution for us in this case. + For created or modified files, check if the local content hash equals the remote + content hash. If yes, we don't upload but update our rev number. If no, we upload + the changes and specify the rev which we want to replace or delete. If the remote + item is newer (different rev), Dropbox will handle conflict resolution for us. We + finally confirm the successful upload and check if Dropbox has renamed the item + to a conflicting copy. In the latter case, we apply those changes locally. :param MaestralApiClient client: Dropbox API client instance. + """ sync_lock = RLock() @@ -662,7 +668,7 @@ def set_local_rev(self, dbx_path, rev): entry for the file is removed. :param str dbx_path: Path relative to Dropbox folder. - :param str, None rev: Revision number as string or ``None``. + :param str rev: Revision number as string or ``None``. """ with self._rev_lock: dbx_path = dbx_path.lower() @@ -803,7 +809,7 @@ def _append_rev_to_file(self, path, rev, raise_exception=False): overwritten with newer ones and all entries with rev == None will be discarded. :param str path: Path for rev. - :param str, None rev: Dropbox rev number. + :param str rev: Dropbox rev or ``None``. :raises: RevFileError if ``raise_exception`` is ``True``. """ From 161561d94801fc794ac5bc55255443d3c3ba1171 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Fri, 8 May 2020 15:17:57 +0200 Subject: [PATCH 019/101] [sync] fixed typo --- maestral/sync.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/maestral/sync.py b/maestral/sync.py index 388a6c2af..5f6b77c13 100644 --- a/maestral/sync.py +++ b/maestral/sync.py @@ -385,7 +385,7 @@ class UpDownSync: 4) :meth:`_create_local_entry`: Checks for sync conflicts by comparing the file "rev" with our locally saved rev. We assign folders a rev of ``'folder'`` and deleted / non-existent items a rev of ``None``. If revs are equal, the local item - is the same or newer as an Dropbox, no download / deletion occurs. If revs are + is the same or newer as on Dropbox and no download / deletion occurs. If revs are different, we compare content hashes. Folders are assigned a hash of 'folder'. If hashes are equal, no download occurs. If they are different, we check if the local item has been modified since the last download sync. In case of a folder, @@ -398,17 +398,17 @@ class UpDownSync: moved, modified and deleted events. They are processed as follows: 1) :meth:`wait_for_local_changes`: Blocks until local changes were registered by - :class:`FSEventHandler` available and returns those changes. + :class:`FSEventHandler` and returns those changes. 2) :meth:`_filter_excluded_changes_local`: Filters out events ignored by a "mignore" pattern as well as hard-coded file names and changes in our cache path. 3) :meth:`_clean_local_events`: Cleans up local events in two stages. First, multiple events per path are combined into a single event which reproduces the - file changes. The only exceptions is when the entry type changes from file to + file changes. The only exception is when the entry type changes from file to folder or vice versa: in this case, both deleted and created events are kept. Second, when a whole folder is moved or deleted, we discard the moved and deleted events of its children. 4) :meth:`apply_local_changes`: Sorts local changes hierarchically and applies - events in the order of deleted, folders and files. Deletions and file uploads + events in the order of deleted, folders and files. Deletions and creations will be carried out in parallel with up to 6 threads. Conflict resolution and the actual upload will be handled by :meth:`_create_remote_entry` as follows: 5) :meth:`_create_remote_entry`: For created and moved events, we check if the new From 9e960a81307b2c44265de8a21fe37c92ff4558ac Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Fri, 8 May 2020 15:25:55 +0200 Subject: [PATCH 020/101] [sync] further docstring updates --- maestral/sync.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/maestral/sync.py b/maestral/sync.py index 5f6b77c13..ab0ad0a8e 100644 --- a/maestral/sync.py +++ b/maestral/sync.py @@ -2818,11 +2818,11 @@ def startup_worker(sync, syncing, running, connected, startup, paused_by_user): class MaestralMonitor: """ - Class to sync changes between Dropbox and a local folder. It creates four - threads: `observer` to catch local file events, `upload_thread` to upload - caught changes to Dropbox, `download_thread` to query for and download - remote changes, and `connection_thread` which periodically checks the - connection to Dropbox servers. + Class to sync changes between Dropbox and a local folder. It creates five threads: + `observer` to retrieve local file system events, `startup_thread` to carry out any + startup jobs such as initial syncs, `upload_thread` to upload local changes to + Dropbox, `download_thread` to query for and download remote changes, and + `connection_thread` which periodically checks the connection to Dropbox servers. :param MaestralApiClient client: The Dropbox API client, a wrapper around the Dropbox Python SDK. @@ -3013,6 +3013,7 @@ def idle_time(self): return 0.0 def reset_sync_state(self): + """Resets all saved sync state.""" if self.syncing.is_set() or self.startup.is_set() or self.sync.busy(): raise RuntimeError('Cannot reset sync state while syncing.') From d335f31297279e42c796da9c3aa3b96770a9e874 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Fri, 8 May 2020 15:29:15 +0200 Subject: [PATCH 021/101] [sync] move sync_lock and _rev_lock to instance variables --- maestral/sync.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/maestral/sync.py b/maestral/sync.py index ab0ad0a8e..bc82be306 100644 --- a/maestral/sync.py +++ b/maestral/sync.py @@ -430,10 +430,6 @@ class UpDownSync: """ - sync_lock = RLock() - - _rev_lock = RLock() - _max_history = 30 _num_threads = min(32, os.cpu_count() * 3) @@ -444,6 +440,9 @@ def __init__(self, client): self.cancel_pending = Event() self.fs_events = None + self.sync_lock = RLock() + self._rev_lock = RLock() + self._conf = MaestralConfig(self.config_name) self._state = MaestralState(self.config_name) self._notifier = MaestralDesktopNotifier.for_config(self.config_name) @@ -2822,7 +2821,7 @@ class MaestralMonitor: `observer` to retrieve local file system events, `startup_thread` to carry out any startup jobs such as initial syncs, `upload_thread` to upload local changes to Dropbox, `download_thread` to query for and download remote changes, and - `connection_thread` which periodically checks the connection to Dropbox servers. + `helper_thread` which periodically checks the connection to Dropbox servers. :param MaestralApiClient client: The Dropbox API client, a wrapper around the Dropbox Python SDK. From 0872e77a8cd5870155f3e19554d74fd60b67a982 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Fri, 8 May 2020 23:43:22 +0200 Subject: [PATCH 022/101] [utils.path] updated doc string --- maestral/utils/path.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/maestral/utils/path.py b/maestral/utils/path.py index 973675eb1..2f7019429 100644 --- a/maestral/utils/path.py +++ b/maestral/utils/path.py @@ -39,8 +39,8 @@ def is_equal_or_child(path, parent): :param str path: Item path. :param str parent: Parent path. - :returns: ``True`` if ``path`` semantically lies inside ``parent``, - ``False`` otherwise (including ``path == parent``). + :returns: ``True`` if ``path`` semantically lies inside ``parent`` or + ``path == parent``. :rtype: bool """ From ffa213b69329c8b0aa70eca608242d11f4ab9e6e Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Fri, 8 May 2020 23:44:11 +0200 Subject: [PATCH 023/101] ensure that dropbox folder always has absolute path --- maestral/main.py | 6 +++--- maestral/sync.py | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/maestral/main.py b/maestral/main.py index 767735d6f..7aea43084 100644 --- a/maestral/main.py +++ b/maestral/main.py @@ -705,7 +705,7 @@ def get_file_status(self, local_path): if not self.syncing: return FileStatus.Unwatched.value - local_path = osp.abspath(local_path) + local_path = osp.realpath(local_path) try: dbx_path = self.sync.to_dbx_path(local_path) @@ -1113,7 +1113,7 @@ def move_dropbox_directory(self, new_path): """ old_path = self.sync.dropbox_path - new_path = osp.abspath(osp.expanduser(new_path)) + new_path = osp.realpath(osp.expanduser(new_path)) try: if osp.samefile(old_path, new_path): @@ -1145,7 +1145,7 @@ def create_dropbox_directory(self, path): :raises: :class:`errors.NotLinkedError` if no Dropbox account is linked. """ - path = osp.abspath(osp.expanduser(path)) + path = osp.realpath(osp.expanduser(path)) self.monitor.reset_sync_state() diff --git a/maestral/sync.py b/maestral/sync.py index bc82be306..c3e88fdcf 100644 --- a/maestral/sync.py +++ b/maestral/sync.py @@ -466,7 +466,7 @@ def __init__(self, client): self.queue_downloading = Queue() # load cached properties - self._dropbox_path = self._conf.get('main', 'path') + self._dropbox_path = osp.realpath(self._conf.get('main', 'path')) self._mignore_path = osp.join(self._dropbox_path, MIGNORE_FILE) self._file_cache_path = osp.join(self._dropbox_path, FILE_CACHE) self._rev_file_path = get_data_path('maestral', f'{self.config_name}.index') @@ -497,6 +497,8 @@ def dropbox_path(self): def dropbox_path(self, path): """Setter: dropbox_path""" + path = osp.realpath(path) + with self.sync_lock: self._dropbox_path = path self._mignore_path = osp.join(self._dropbox_path, MIGNORE_FILE) @@ -900,10 +902,8 @@ def to_dbx_path(self, local_path): :raises: :class:`ValueError` the path lies outside of the local Dropbox folder. """ - if local_path == self.dropbox_path: # path corresponds to dropbox_path - return '/' - elif is_child(local_path, self.dropbox_path): - return local_path.replace(self.dropbox_path, '', 1) + if is_equal_or_child(local_path, self.dropbox_path): + return '/' + local_path.replace(self.dropbox_path, '', 1).lstrip('/') else: raise ValueError(f'Specified path "{local_path}" is outside of Dropbox ' f'directory "{self.dropbox_path}"') From 7f7ba2ec2d0cdacb62758dfea17ef8094cfa1b3f Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Sat, 9 May 2020 19:21:21 +0200 Subject: [PATCH 024/101] use os.path.sep for local paths and '/' for dbx paths --- maestral/main.py | 6 +++--- maestral/sync.py | 15 ++++++++------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/maestral/main.py b/maestral/main.py index 7aea43084..cec34acb9 100644 --- a/maestral/main.py +++ b/maestral/main.py @@ -942,7 +942,7 @@ def exclude_item(self, dbx_path): raise NotFoundError('Cannot exlcude item', f'"{dbx_path}" does not exist on Dropbox') - dbx_path = dbx_path.lower().rstrip(osp.sep) + dbx_path = dbx_path.lower().rstrip('/') # add the path to excluded list if self.sync.is_excluded_by_user(dbx_path): @@ -1004,7 +1004,7 @@ def include_item(self, dbx_path): raise NotFoundError('Cannot include item', f'"{dbx_path}" does not exist on Dropbox') - dbx_path = dbx_path.lower().rstrip(osp.sep) + dbx_path = dbx_path.lower().rstrip('/') old_excluded_items = self.sync.excluded_items @@ -1086,7 +1086,7 @@ def excluded_status(self, dbx_path): :raises: :class:`errors.NotLinkedError` if no Dropbox account is linked. """ - dbx_path = dbx_path.lower().rstrip(osp.sep) + dbx_path = dbx_path.lower().rstrip('/') excluded_items = self._conf.get('main', 'excluded_items') diff --git a/maestral/sync.py b/maestral/sync.py index c3e88fdcf..e721fe0d2 100644 --- a/maestral/sync.py +++ b/maestral/sync.py @@ -279,7 +279,7 @@ def __len__(self): return len(self._state.get(self.section, self.option)) def discard(self, dbx_path): - dbx_path = dbx_path.lower().rstrip(osp.sep) + dbx_path = dbx_path.lower().rstrip('/') with self._lock: state_list = self._state.get(self.section, self.option) state_list = set(state_list) @@ -287,7 +287,7 @@ def discard(self, dbx_path): self._state.set(self.section, self.option, list(state_list)) def add(self, dbx_path): - dbx_path = dbx_path.lower().rstrip(osp.sep) + dbx_path = dbx_path.lower().rstrip('/') with self._lock: state_list = self._state.get(self.section, self.option) state_list = set(state_list) @@ -545,7 +545,7 @@ def clean_excluded_items_list(folder_list): """ # remove duplicate entries by creating set, strip trailing '/' - folder_list = set(f.lower().rstrip(osp.sep) for f in folder_list) + folder_list = set(f.lower().rstrip('/') for f in folder_list) # remove all children of excluded folders clean_list = list(folder_list) @@ -903,7 +903,8 @@ def to_dbx_path(self, local_path): """ if is_equal_or_child(local_path, self.dropbox_path): - return '/' + local_path.replace(self.dropbox_path, '', 1).lstrip('/') + dbx_path = osp.sep + local_path.replace(self.dropbox_path, '', 1).lstrip(osp.sep) + return dbx_path.replace(osp.sep, '/') else: raise ValueError(f'Specified path "{local_path}" is outside of Dropbox ' f'directory "{self.dropbox_path}"') @@ -931,7 +932,7 @@ def to_local_path(self, dbx_path): local_parent = to_cased_path(dbx_path_parent, root=self.dropbox_path) if local_parent == '': - return osp.join(self.dropbox_path, dbx_path.lstrip(osp.sep)) + return osp.join(self.dropbox_path, dbx_path.lstrip('/')) else: return osp.join(local_parent, dbx_path_basename) @@ -984,7 +985,7 @@ def is_excluded(path): :returns: ``True`` if excluded, ``False`` otherwise. :rtype: bool """ - path = path.lower() + path = path.lower().replace(osp.sep, '/') # is root folder? if path in ('/', ''): @@ -2170,7 +2171,7 @@ def notify_user(self, changes): if n_changed == 1: # display user name, file name, and type of change md = changes[0] - file_name = os.path.basename(md.path_display) + file_name = osp.basename(md.path_display) if isinstance(md, DeletedMetadata): change_type = 'removed' From 54cb81779d489ca1b0dc4e0e1648ea8cd0cedfe2 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Sat, 9 May 2020 23:48:52 +0200 Subject: [PATCH 025/101] [tests] added mignore test --- tests/test_main.py | 18 ++++++++++------- tests/test_sync.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index 5e677cb60..5ca84ec1e 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -60,6 +60,11 @@ def tearDownClass(cls): except NotFoundError: pass + try: + cls.m.client.remove('/.mignore') + except NotFoundError: + pass + # release test lock try: @@ -104,6 +109,11 @@ def clean_remote(self): except NotFoundError: pass + try: + self.m.client.remove('/.mignore') + except NotFoundError: + pass + self.m.client.make_dir(self.test_folder_dbx) # test functions @@ -172,13 +182,7 @@ def test_upload_sync_issues(self): self.assertFalse(any(e['local_path'] == test_path_local for e in self.m.sync_errors)) def test_download_sync_issues(self): - # TODO: find a file with reproducible download error - pass - - def test_mignore(self): - # TODO: - # 1) test that new files are excluded - # 2) test that tracked files are unaffected + # TODO: find a file with a reproducible download error pass diff --git a/tests/test_sync.py b/tests/test_sync.py index 97f4cc2e5..0c406de3b 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -420,6 +420,11 @@ def tearDownClass(cls): except NotFoundError: pass + try: + cls.m.client.remove('/.mignore') + except NotFoundError: + pass + # release test lock try: @@ -464,6 +469,11 @@ def clean_remote(self): except NotFoundError: pass + try: + self.m.client.remove('/.mignore') + except NotFoundError: + pass + self.m.client.make_dir(self.test_folder_dbx) def assert_synced(self, local_folder, remote_folder): @@ -984,6 +994,44 @@ def test_case_conflict(self): self.assert_synced(self.test_folder_local, self.test_folder_dbx) + def test_mignore(self): + + # 1) test that tracked items are unaffected + + os.mkdir(self.test_folder_local + '/bar') + self.wait_for_idle() + + with open(self.m.sync.mignore_path, 'w') as f: + f.write('foo/\n') # ignore folder "foo" + f.write('bar\n') # ignore file or folder "bar" + f.write('build\n') # ignore file or folder "build" + + self.wait_for_idle() + + self.assert_synced(self.test_folder_local, self.test_folder_dbx) + self.assert_exists(self.test_folder_dbx, 'bar') + + # 2) test that new items are excluded + + os.mkdir(self.test_folder_local + '/foo') + self.wait_for_idle() + + self.assertIsNone(self.m.client.get_metadata(self.test_folder_dbx + '/foo')) + + # 3) test that renaming an item excludes it + + move(self.test_folder_local + '/bar', self.test_folder_local + '/build') + self.wait_for_idle() + + self.assertIsNone(self.m.client.get_metadata(self.test_folder_dbx + '/build')) + + # 4) test that renaming an item includes it + + move(self.test_folder_local + '/build', self.test_folder_local + '/folder') + self.wait_for_idle() + + self.assert_exists(self.test_folder_dbx, 'folder') + if __name__ == '__main__': unittest.main() From 3a3575c792456b8c4dae9c0f9aea3927651bdef0 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Sat, 9 May 2020 23:49:50 +0200 Subject: [PATCH 026/101] [tests] fixed spelling --- tests/test_main.py | 6 +++--- tests/test_sync.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index 5ca84ec1e..912ca9148 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -36,7 +36,7 @@ def setUpClass(cls): cls.test_folder_dbx = cls.TEST_FOLDER_PATH cls.test_folder_local = cls.m.dropbox_path + cls.TEST_FOLDER_PATH - # aquire test lock + # acquire test lock while True: try: cls.m.client.make_dir(cls.TEST_LOCK_PATH) @@ -119,7 +119,7 @@ def clean_remote(self): # test functions def test_selective_sync(self): - """Test `Maetsral.exclude_item` and Maetsral.include_item`.""" + """Test `Maestral.exclude_item` and Maestral.include_item`.""" test_path_local = self.test_folder_local + '/selective_sync_test_folder' test_path_dbx = self.test_folder_dbx + '/selective_sync_test_folder' @@ -158,7 +158,7 @@ def test_selective_sync(self): self.assertNotIn(test_path_dbx, self.m.excluded_items, 'deleted item is still in "excluded_items" list') - # test exluding a non-existant folder + # test excluding a non-existent folder with self.assertRaises(NotFoundError): self.m.exclude_item(test_path_dbx) diff --git a/tests/test_sync.py b/tests/test_sync.py index 0c406de3b..b250ca85b 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -30,7 +30,7 @@ from unittest import TestCase -# run tests in decleration order and not alphabetically +# run tests in declaration order and not alphabetically unittest.TestLoader.sortTestMethodsUsing = None @@ -307,7 +307,7 @@ def setUp(self): self.observer.schedule(self.fs_event_handler, str(self.dummy_dir), recursive=True) self.observer.start() - def test_recieveing_events(self): + def test_receiving_events(self): new_dir = self.dummy_dir / 'parent' new_dir.mkdir() @@ -396,7 +396,7 @@ def setUpClass(cls): cls.test_folder_dbx = cls.TEST_FOLDER_PATH cls.test_folder_local = cls.m.dropbox_path + cls.TEST_FOLDER_PATH - # aquire test lock + # acquire test lock while True: try: cls.m.client.make_dir(cls.TEST_LOCK_PATH) @@ -514,7 +514,7 @@ def _count_conflicts(entries, name): candidates = list(e for e in entries if e['name'].startswith(basename)) ccs = list(e for e in candidates if '(1)' in e['name'] # created by Dropbox for add conflict - or 'conflicted copy' in e['name'] # created by Dropbox for update confict + or 'conflicted copy' in e['name'] # created by Dropbox for update conflict or 'conflicting copy' in e['name']) # created by us return len(ccs) From eb895551be4bad62769a14e0983e6ccc72f7b5be Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Mon, 11 May 2020 11:03:48 +0200 Subject: [PATCH 027/101] remove `get_old_runtime_path` --- setup.py | 5 ++--- tests/utils/test_appdirs.py | 8 ++------ 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/setup.py b/setup.py index 1fcb20ced..0e3e78f27 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ # local imports (must not depend on 3rd party packages) from maestral import __version__, __author__, __url__ -from maestral.utils.appdirs import get_runtime_path, get_old_runtime_path +from maestral.utils.appdirs import get_runtime_path from maestral.config.base import list_configs @@ -17,8 +17,7 @@ for config in list_configs(): pid_file = get_runtime_path('maestral', config + '.pid') - old_pid_file = get_old_runtime_path('maestral', config + '.pid') - if osp.exists(pid_file) or osp.exists(old_pid_file): + if osp.exists(pid_file): running_daemons.append(config) if running_daemons: diff --git a/tests/utils/test_appdirs.py b/tests/utils/test_appdirs.py index 406cc5ef5..3bc267609 100644 --- a/tests/utils/test_appdirs.py +++ b/tests/utils/test_appdirs.py @@ -7,10 +7,9 @@ """ import os import platform -import tempfile from maestral.utils.appdirs import ( - get_home_dir, get_runtime_path, get_old_runtime_path, get_conf_path, - get_log_path, get_cache_path, get_data_path, get_autostart_path, + get_home_dir, get_runtime_path, get_conf_path, get_log_path, get_cache_path, + get_data_path, get_autostart_path, ) @@ -21,7 +20,6 @@ def test_macos_dirs(): assert get_cache_path(create=False) == get_conf_path(create=False) assert get_data_path(create=False) == get_conf_path(create=False) assert get_runtime_path(create=False) == get_conf_path(create=False) - assert get_old_runtime_path(create=False) == tempfile.gettempdir() assert get_log_path(create=False) == get_home_dir() + '/Library/Logs' assert get_autostart_path(create=False) == get_home_dir() + '/Library/LaunchAgents' @@ -38,7 +36,6 @@ def test_linux_dirs(): assert get_cache_path(create=False) == '/xdg_cache_home' assert get_data_path(create=False) == '/xdg_data_dir' assert get_runtime_path(create=False) == '/xdg_runtime_dir' - assert get_old_runtime_path(create=False) == '/xdg_runtime_dir' assert get_log_path(create=False) == '/xdg_cache_home' assert get_autostart_path(create=False) == '/xdg_config_home/autostart' @@ -51,6 +48,5 @@ def test_linux_dirs(): assert get_cache_path(create=False) == get_home_dir() + '/.cache' assert get_data_path(create=False) == get_home_dir() + '/.local/share' assert get_runtime_path(create=False) == get_home_dir() + '/.cache' - assert get_old_runtime_path(create=False) == get_home_dir() + '/.cache' assert get_log_path(create=False) == get_home_dir() + '/.cache' assert get_autostart_path(create=False) == get_home_dir() + '/.config/autostart' From c889ff9af068680bb47e1a6eb975a7d1789b7bac Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Mon, 11 May 2020 12:51:24 +0200 Subject: [PATCH 028/101] [docs] removed utils.appdirs and utils.autostart from external API list --- docs/index.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 3a277ab73..e958cbb97 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,11 +10,9 @@ refer to the `wiki `_. The following APIs should remain stable for frontends such as the GUI or CLI: * maestral.main.Maestral -* maestral.constants * maestral.daemon +* maestral.constants * maestral.errors -* maestral.utils.appdirs -* maestral.utils.autostart .. toctree:: :caption: Table of Contents From 722439dfd2142395b00d7bb6d0690d7d532df5ec Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Mon, 11 May 2020 12:51:44 +0200 Subject: [PATCH 029/101] [utils.autostart] added comment about GUI support --- maestral/utils/autostart.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/maestral/utils/autostart.py b/maestral/utils/autostart.py index 37879286a..7b48d66fe 100644 --- a/maestral/utils/autostart.py +++ b/maestral/utils/autostart.py @@ -4,8 +4,12 @@ (c) Sam Schott; This work is licensed under the MIT licence. -This module handles starting for Maestral on user login and supports multiple backends, -depending on the platform and if we want to start the daemon or GUI. +This module handles starting Maestral on user login and supports multiple platform +specific backends such as launchd or systemd. Additionally, this module also provides +support for GUIs via launchd or xdg-desktop entries by passing the ``gui`` option to the +``maestral`` command or executable. Therefore, only GUIs which are explicitly supported by +the CLI with the `maestral gui` command or frozen executables which provide their own GUI +are supported. """ From 71bdfb22bf68824d0a29fec5c54646e2a03d7bd2 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Mon, 11 May 2020 18:39:10 +0200 Subject: [PATCH 030/101] [daemon] replace " with ' --- maestral/daemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maestral/daemon.py b/maestral/daemon.py index b477a67b5..8a9aa0d9b 100644 --- a/maestral/daemon.py +++ b/maestral/daemon.py @@ -79,7 +79,7 @@ def serpent_deserialize_api_error(class_name, d): # ==== helpers for daemon management ===================================================== def _escape_spaces(string): - return string.replace(" ", "_") + return string.replace(' ', '_') def _sigterm_handler(signal_number, frame): From 63f30bdb34eff4b0ad583cd6f26ac66e1cc515f3 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Mon, 11 May 2020 19:12:09 +0200 Subject: [PATCH 031/101] [daemon] remove thread from dict after termination --- maestral/daemon.py | 1 + 1 file changed, 1 insertion(+) diff --git a/maestral/daemon.py b/maestral/daemon.py index 8a9aa0d9b..21b0d87e5 100644 --- a/maestral/daemon.py +++ b/maestral/daemon.py @@ -413,6 +413,7 @@ def stop_maestral_daemon_thread(config_name='maestral', timeout=10): if t.is_alive(): return Exit.Failed else: + del threads[config_name] return Exit.Ok From 4a45e3baaf863dedc29bb61c56ab079f6256cfba Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Mon, 11 May 2020 19:17:23 +0200 Subject: [PATCH 032/101] [daemon] fix Startup.AlreadyRunning return code --- maestral/daemon.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/maestral/daemon.py b/maestral/daemon.py index 21b0d87e5..ae4c19a1c 100644 --- a/maestral/daemon.py +++ b/maestral/daemon.py @@ -264,6 +264,10 @@ def start_maestral_daemon_thread(config_name='maestral', log_to_stdout=False): :returns: ``Start.Ok`` if successful, ``Start.AlreadyRunning`` if the daemon was already running or ``Start.Failed`` if startup failed. """ + + if config_name in threads and threads[config_name].is_alive(): + return Start.AlreadyRunning + import threading t = threading.Thread( @@ -311,6 +315,9 @@ def start_maestral_daemon_process(config_name='maestral', log_to_stdout=False): # use nested Popen and multiprocessing.Process to effectively create double fork # see Unix 'double-fork magic' + if get_maestral_pid(config_name): + return Start.AlreadyRunning + if IS_FROZEN: def target(): From 74c94dcba08e2dc1de23c2b580db86eb857a11d7 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Tue, 12 May 2020 11:13:15 +0200 Subject: [PATCH 033/101] [utils.notify] thread safe singleton creation --- maestral/utils/notify.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/maestral/utils/notify.py b/maestral/utils/notify.py index 9cec79c38..a69cd9b23 100644 --- a/maestral/utils/notify.py +++ b/maestral/utils/notify.py @@ -27,6 +27,7 @@ import pkg_resources import logging from collections import deque +import threading # external imports import click @@ -359,7 +360,8 @@ class MaestralDesktopNotifier(logging.Handler): ``notify_level`` will be applied in addition to the log level. """ - _instances = {} + _instances = dict() + _lock = threading.Lock() @classmethod def for_config(cls, config_name): @@ -370,12 +372,13 @@ def for_config(cls, config_name): :param str config_name: Name of maestral config. """ - if config_name in cls._instances: - return cls._instances[config_name] - else: - instance = cls(config_name) - cls._instances[config_name] = instance - return instance + with cls._lock: + try: + return cls._instances[config_name] + except KeyError: + instance = cls(config_name) + cls._instances[config_name] = instance + return instance def __init__(self, config_name): super().__init__() From caffd8d3355898406b4372d8ac5cfe208c905a63 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Tue, 12 May 2020 11:13:32 +0200 Subject: [PATCH 034/101] [config] thread safe singleton creation --- maestral/config/main.py | 92 ++++++++++++++++++++++------------------- 1 file changed, 50 insertions(+), 42 deletions(-) diff --git a/maestral/config/main.py b/maestral/config/main.py index 218e3e005..fe913cd76 100644 --- a/maestral/config/main.py +++ b/maestral/config/main.py @@ -11,6 +11,7 @@ import copy import logging +import threading from .base import get_conf_path, get_data_path from .user import UserConfig @@ -95,8 +96,11 @@ # Factories # ============================================================================= -_config_instances = {} -_state_instances = {} +_config_instances = dict() +_state_instances = dict() + +_config_lock = threading.Lock() +_state_lock = threading.Lock() def MaestralConfig(config_name): @@ -111,30 +115,32 @@ def MaestralConfig(config_name): global _config_instances - if config_name in _config_instances: - return _config_instances[config_name] - else: - defaults = copy.deepcopy(DEFAULTS) - # set default dir name according to config - for sec, options in defaults: - if sec == 'main': - options['default_dir_name'] = f'Dropbox ({config_name.title()})' - - config_path = get_conf_path(CONFIG_DIR_NAME) + with _config_lock: try: - conf = UserConfig( - config_path, config_name, defaults=defaults, version=CONF_VERSION, - load=True, backup=True, raw_mode=True, remove_obsolete=True - ) - except OSError: - conf = UserConfig( - config_path, config_name, defaults=defaults, version=CONF_VERSION, - load=False, backup=True, raw_mode=True, remove_obsolete=True - ) - - _config_instances[config_name] = conf - return conf + return _config_instances[config_name] + except KeyError: + defaults = copy.deepcopy(DEFAULTS) + # set default dir name according to config + for sec, options in defaults: + if sec == 'main': + options['default_dir_name'] = f'Dropbox ({config_name.title()})' + + config_path = get_conf_path(CONFIG_DIR_NAME) + + try: + conf = UserConfig( + config_path, config_name, defaults=defaults, version=CONF_VERSION, + load=True, backup=True, raw_mode=True, remove_obsolete=True + ) + except OSError: + conf = UserConfig( + config_path, config_name, defaults=defaults, version=CONF_VERSION, + load=False, backup=True, raw_mode=True, remove_obsolete=True + ) + + _config_instances[config_name] = conf + return conf def MaestralState(config_name): @@ -149,23 +155,25 @@ def MaestralState(config_name): global _state_instances - if config_name in _state_instances: - return _state_instances[config_name] - else: - state_path = get_data_path(CONFIG_DIR_NAME) + with _state_lock: try: - state = UserConfig( - state_path, config_name, defaults=DEFAULTS_STATE, - version=CONF_VERSION, load=True, backup=True, raw_mode=True, - remove_obsolete=True, suffix='.state' - ) - except OSError: - state = UserConfig( - state_path, config_name, defaults=DEFAULTS_STATE, - version=CONF_VERSION, load=False, backup=True, raw_mode=True, - remove_obsolete=True, suffix='.state' - ) - - _state_instances[config_name] = state - return state + return _state_instances[config_name] + except KeyError: + state_path = get_data_path(CONFIG_DIR_NAME) + + try: + state = UserConfig( + state_path, config_name, defaults=DEFAULTS_STATE, + version=CONF_VERSION, load=True, backup=True, raw_mode=True, + remove_obsolete=True, suffix='.state' + ) + except OSError: + state = UserConfig( + state_path, config_name, defaults=DEFAULTS_STATE, + version=CONF_VERSION, load=False, backup=True, raw_mode=True, + remove_obsolete=True, suffix='.state' + ) + + _state_instances[config_name] = state + return state From 633e04a7aa1e06596ef3c097b0f0b42d2470764b Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Tue, 12 May 2020 11:16:26 +0200 Subject: [PATCH 035/101] [sync] cleanup duplicate code --- maestral/sync.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/maestral/sync.py b/maestral/sync.py index e721fe0d2..88e8f4b1e 100644 --- a/maestral/sync.py +++ b/maestral/sync.py @@ -3051,9 +3051,6 @@ def _wait_for_idle(self): with self.sync.sync_lock: pass - self.sync.sync_lock.acquire() - self.sync.sync_lock.release() - def _threads_alive(self): """Returns ``True`` if all threads are alive, ``False`` otherwise.""" From 20e9975b8f32e4ba6bb5060b8d52cc29a99eb74f Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Tue, 12 May 2020 17:03:59 +0200 Subject: [PATCH 036/101] updated changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88d0a075a..1abce6f56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ ## v1.0.3.dev +#### Changed: + +- Significantly reduced CPU usage of the GUI on macOS. +- Show both the daemon and GUI version in the GUI settings window. #### Fixed: From 4b03c2f157088aadb67b23e7f501a0474a20d394 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Wed, 13 May 2020 14:19:06 +0200 Subject: [PATCH 037/101] [tests] improve sync issue upload test --- tests/test_main.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_main.py b/tests/test_main.py index 912ca9148..3775f381b 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -166,6 +166,7 @@ def test_upload_sync_issues(self): # paths with backslash are not allowed on Dropbox test_path_local = self.test_folder_local + '/folder\\' + test_path_dbx = self.test_folder_dbx + '/folder\\' n_errors_initial = len(self.m.sync_errors) @@ -174,12 +175,14 @@ def test_upload_sync_issues(self): self.assertEqual(len(self.m.sync_errors), n_errors_initial + 1) self.assertTrue(any(e['local_path'] == test_path_local for e in self.m.sync_errors)) + self.assertTrue(any(e['dbx_path'] == test_path_dbx for e in self.m.sync_errors)) delete(test_path_local) self.wait_for_idle() self.assertEqual(len(self.m.sync_errors), n_errors_initial) self.assertFalse(any(e['local_path'] == test_path_local for e in self.m.sync_errors)) + self.assertFalse(any(e['dbx_path'] == test_path_dbx for e in self.m.sync_errors)) def test_download_sync_issues(self): # TODO: find a file with a reproducible download error From 5f72fc38fc8b3c5a4cffef368ec07087c2b9ca6c Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Wed, 13 May 2020 14:19:30 +0200 Subject: [PATCH 038/101] [client] minor cleanup --- maestral/client.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/maestral/client.py b/maestral/client.py index 0a737511e..9b3ba3e90 100644 --- a/maestral/client.py +++ b/maestral/client.py @@ -251,18 +251,18 @@ def list_revisions(self, dbx_path, mode='path', limit=10): return self.dbx.files_list_revisions(dbx_path, mode=mode, limit=limit) @to_maestral_error(dbx_path_arg=1) - def download(self, dbx_path, dst_path, **kwargs): + def download(self, dbx_path, local_path, **kwargs): """ Downloads file from Dropbox to our local folder. - :param str dbx_path: Path to file on Dropbox. - :param str dst_path: Path to local download destination. + :param str dbx_path: Path to file on Dropbox or rev number. + :param str local_path: Path to local download destination. :param kwargs: Keyword arguments for Dropbox SDK files_download_to_file. :returns: Metadata of downloaded item. :rtype: :class:`dropbox.files.FileMetadata` """ # create local directory if not present - dst_path_directory = osp.dirname(dst_path) + dst_path_directory = osp.dirname(local_path) try: os.makedirs(dst_path_directory) except FileExistsError: @@ -275,7 +275,7 @@ def download(self, dbx_path, dst_path, **kwargs): downloaded = 0 - with open(dst_path, 'wb') as f: + with open(local_path, 'wb') as f: with contextlib.closing(http_resp): for c in http_resp.iter_content(chunksize): if md.size > 5 * 10 ** 6: # 5 MB @@ -285,7 +285,7 @@ def download(self, dbx_path, dst_path, **kwargs): return md - @to_maestral_error(dbx_path_arg=2) + @to_maestral_error(local_path_arg=1, dbx_path_arg=2) def upload(self, local_path, dbx_path, chunk_size_mb=5, **kwargs): """ Uploads local file to Dropbox. From 34e7a16c4d70de08cb0c4135fe31ee204e60a906 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Wed, 13 May 2020 14:43:27 +0200 Subject: [PATCH 039/101] [sync] reformat imports --- maestral/sync.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/maestral/sync.py b/maestral/sync.py index 88e8f4b1e..93c3c71be 100644 --- a/maestral/sync.py +++ b/maestral/sync.py @@ -35,23 +35,28 @@ import dropbox from dropbox.files import Metadata, DeletedMetadata, FileMetadata, FolderMetadata from watchdog.events import FileSystemEventHandler -from watchdog.events import (EVENT_TYPE_CREATED, EVENT_TYPE_DELETED, - EVENT_TYPE_MODIFIED, EVENT_TYPE_MOVED) -from watchdog.events import (DirModifiedEvent, FileModifiedEvent, DirCreatedEvent, - FileCreatedEvent, DirDeletedEvent, FileDeletedEvent, - DirMovedEvent, FileMovedEvent) +from watchdog.events import ( + EVENT_TYPE_CREATED, EVENT_TYPE_DELETED, EVENT_TYPE_MODIFIED, EVENT_TYPE_MOVED +) +from watchdog.events import ( + DirModifiedEvent, FileModifiedEvent, DirCreatedEvent, FileCreatedEvent, + DirDeletedEvent, FileDeletedEvent, DirMovedEvent, FileMovedEvent +) from watchdog.utils.dirsnapshot import DirectorySnapshot from atomicwrites import atomic_write # local imports from maestral.config import MaestralConfig, MaestralState from maestral.fsevents import Observer -from maestral.constants import (IDLE, SYNCING, PAUSED, STOPPED, DISCONNECTED, - EXCLUDED_FILE_NAMES, EXCLUDED_DIR_NAMES, - MIGNORE_FILE, FILE_CACHE, IS_FS_CASE_SENSITIVE) -from maestral.errors import (RevFileError, NoDropboxDirError, CacheDirError, SyncError, - PathError, NotFoundError, FileConflictError, FolderConflictError, - fswatch_to_maestral_error, os_to_maestral_error) +from maestral.constants import ( + IDLE, SYNCING, PAUSED, STOPPED, DISCONNECTED, EXCLUDED_FILE_NAMES, EXCLUDED_DIR_NAMES, + MIGNORE_FILE, FILE_CACHE, IS_FS_CASE_SENSITIVE +) +from maestral.errors import ( + MaestralApiError, SyncError, RevFileError, NoDropboxDirError, CacheDirError, + PathError, NotFoundError, FileConflictError, FolderConflictError, + fswatch_to_maestral_error, os_to_maestral_error +) from maestral.utils.content_hasher import DropboxContentHasher from maestral.utils.notify import MaestralDesktopNotifier, FILECHANGE from maestral.utils.path import ( From c3fe67b480e1fcc1f0a639d958bd607ffa8c7a93 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Wed, 13 May 2020 14:43:54 +0200 Subject: [PATCH 040/101] [sync] replace 'rev' with dbx_path for download SyncError see #143 --- maestral/sync.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/maestral/sync.py b/maestral/sync.py index 93c3c71be..5b3336d7a 100644 --- a/maestral/sync.py +++ b/maestral/sync.py @@ -2459,7 +2459,13 @@ def _create_local_entry(self, entry): # we download to a temporary file first (this may take some time) tmp_fname = self._new_tmp_file() - md = self.client.download(f'rev:{entry.rev}', tmp_fname) + + try: + md = self.client.download(f'rev:{entry.rev}', tmp_fname) + except MaestralApiError as err: + # replace rev number with path + err.dbx_path = entry.path_display + raise err # re-check for conflict and move the conflict # out of the way if anything has changed From 19ccef666d7e8d31b1007a6c18c7633227f99803 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Wed, 13 May 2020 15:02:46 +0200 Subject: [PATCH 041/101] updated changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1abce6f56..250643420 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ ## v1.0.3.dev + #### Changed: - Significantly reduced CPU usage of the GUI on macOS. @@ -8,6 +9,8 @@ - Fixes an issue which could lead to the local Dropbox folder being moved before syncing has been paused. +- Fixes an issue where download errors would show a rev number instead of the Dropbox + path. ## v1.0.2 From e567719159c5c526fcf56ca74c8832a0b63c3ca3 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Wed, 13 May 2020 15:21:50 +0200 Subject: [PATCH 042/101] [errors] tweaked lookup error message --- maestral/errors.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/maestral/errors.py b/maestral/errors.py index 2b35c0f8d..da5a5438d 100644 --- a/maestral/errors.py +++ b/maestral/errors.py @@ -638,8 +638,7 @@ def _get_lookup_error_msg(lookup_error): err_cls = SyncError if lookup_error.is_malformed_path(): - text = ('The destination path is invalid. Paths may not end with a slash or ' - 'whitespace.') + text = 'The path is invalid. Paths may not end with a slash or whitespace.' err_cls = PathError elif lookup_error.is_not_file(): text = 'The given path refers to a folder.' From 9b28884d0dc4169aa3bffd58b2f1ab6c7edc5ff0 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Wed, 13 May 2020 18:24:58 +0200 Subject: [PATCH 043/101] updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 250643420..7e9845bfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ has been paused. - Fixes an issue where download errors would show a rev number instead of the Dropbox path. +- Fixes an issue in the macOS GUI where updating the displayed sync issues could fail. ## v1.0.2 From bb0748669282d8044ac81d304d484c36347ada98 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Wed, 13 May 2020 18:26:59 +0200 Subject: [PATCH 044/101] [tests] added test for download sync error see #143 --- tests/resources/dmca.gif | Bin 0 -> 321315 bytes tests/test_main.py | 52 ++++++++++++++++++++++++++++++++++----- 2 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 tests/resources/dmca.gif diff --git a/tests/resources/dmca.gif b/tests/resources/dmca.gif new file mode 100644 index 0000000000000000000000000000000000000000..23c7b6689e5bc5cbcdebc0ba66e06bc7e01ea20f GIT binary patch literal 321315 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�c(VTMXz9b$sb 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

5H4D%belfO3IaH+q2`rED$HIiJel=~Jp~ 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 z

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 Date: Wed, 13 May 2020 18:41:07 +0200 Subject: [PATCH 045/101] [sync] don't crash when `get_remote_item` cannot find any metadata --- maestral/sync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maestral/sync.py b/maestral/sync.py index 5b3336d7a..1ecd2d635 100644 --- a/maestral/sync.py +++ b/maestral/sync.py @@ -2026,7 +2026,7 @@ def get_remote_item(self, dbx_path): if isinstance(md, FolderMetadata): res = self.get_remote_folder(dbx_path) - else: # FileMetadata or DeletedMetadata + elif md: # FileMetadata or DeletedMetadata with InQueue(self.queue_downloading, md.path_display): res = self._create_local_entry(md) From 4039aa9c38830597e12e8f8a5c00cb51594d6f59 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Wed, 13 May 2020 12:56:31 +0200 Subject: [PATCH 046/101] fixed spelling --- maestral/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/maestral/cli.py b/maestral/cli.py index f8b039764..64e8eae00 100755 --- a/maestral/cli.py +++ b/maestral/cli.py @@ -868,8 +868,8 @@ def analytics(config_name: str, yes: bool, no: bool): """ Enables or disables sharing error reports. - Sharing is disabled by default. If enbled, error reports are shared with bugsnag and - no personal infortmation will typically be collected. Shared tracebacks may however + Sharing is disabled by default. If enabled, error reports are shared with bugsnag and + no personal information will typically be collected. Shared tracebacks may however include file names, depending on the error. """ From b5612e396868bccacd1d152dde520b52c0a5f7a6 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Thu, 14 May 2020 00:11:10 +0200 Subject: [PATCH 047/101] [sync] fix wait_for_idle --- maestral/sync.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/maestral/sync.py b/maestral/sync.py index 1ecd2d635..9666773df 100644 --- a/maestral/sync.py +++ b/maestral/sync.py @@ -3059,8 +3059,8 @@ def rebuild_index(self): def _wait_for_idle(self): - with self.sync.sync_lock: - pass + self.sync.sync_lock.acquire() + self.sync.sync_lock.release() def _threads_alive(self): """Returns ``True`` if all threads are alive, ``False`` otherwise.""" From e898069b5df700b5ed4a942812aec3b3b1fd3bc6 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Thu, 14 May 2020 10:32:49 +0200 Subject: [PATCH 048/101] [tests] fix cleanup after mignore test --- tests/test_sync.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/test_sync.py b/tests/test_sync.py index b250ca85b..0bd009907 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -463,7 +463,7 @@ def wait_for_idle(self, minimum=4): time.sleep(0.1) def clean_remote(self): - """Recreates a fresh test folder.""" + """Recreates a fresh test folder on remote Dropbox.""" try: self.m.client.remove(self.test_folder_dbx) except NotFoundError: @@ -476,6 +476,12 @@ def clean_remote(self): self.m.client.make_dir(self.test_folder_dbx) + def clean_local(self): + """Recreates a fresh test folder locally.""" + delete(self.m.dropbox_path + '/.mignore') + delete(self.test_folder_local) + os.mkdir(self.test_folder_local) + def assert_synced(self, local_folder, remote_folder): """Asserts that the `local_folder` and `remote_folder` are synced.""" remote_items = self.m.list_folder(remote_folder, recursive=True) @@ -1032,6 +1038,9 @@ def test_mignore(self): self.assert_exists(self.test_folder_dbx, 'folder') + self.clean_local() + self.wait_for_idle() + if __name__ == '__main__': unittest.main() From 9bc149fd9a478de4d6082208c9ffca31b1d7f508 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Thu, 14 May 2020 10:40:25 +0200 Subject: [PATCH 049/101] [tests] remove custom test order --- tests/test_sync.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/test_sync.py b/tests/test_sync.py index 0bd009907..d35b7c449 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -30,10 +30,6 @@ from unittest import TestCase -# run tests in declaration order and not alphabetically -unittest.TestLoader.sortTestMethodsUsing = None - - class DummyUpDownSync(UpDownSync): def __init__(self, dropbox_path=''): From e4388ee1a5ab8699e3a08055fb0d5e6485b9921d Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Mon, 11 May 2020 12:08:27 +0200 Subject: [PATCH 050/101] [daemon] migrate from lockfile to fasteners see #135 --- maestral/cli.py | 11 +- maestral/daemon.py | 307 +++++++++++++++++++++++++++++---------------- setup.py | 26 +--- 3 files changed, 204 insertions(+), 140 deletions(-) diff --git a/maestral/cli.py b/maestral/cli.py index 64e8eae00..1b2a27715 100755 --- a/maestral/cli.py +++ b/maestral/cli.py @@ -445,16 +445,10 @@ def gui(config_name): def start(config_name: str, foreground: bool, verbose: bool): """Starts the Maestral daemon.""" - from maestral.daemon import get_maestral_pid, get_maestral_proxy + from maestral.daemon import get_maestral_proxy from maestral.daemon import (start_maestral_daemon_thread, threads, start_maestral_daemon_process, Start) - # do nothing if already running - if get_maestral_pid(config_name): - click.echo('Maestral daemon is already running.') - return - - # start daemon click.echo('Starting Maestral...', nl=False) if foreground: @@ -464,6 +458,9 @@ def start(config_name: str, foreground: bool, verbose: bool): if res == Start.Ok: click.echo('\rStarting Maestral... ' + OK) + elif res == Start.AlreadyRunning: + click.echo('\rStarting Maestral... Already running.') + return else: click.echo('\rStarting Maestral... ' + FAILED) click.echo('Please check logs for more information.') diff --git a/maestral/daemon.py b/maestral/daemon.py index ae4c19a1c..ce71ec182 100644 --- a/maestral/daemon.py +++ b/maestral/daemon.py @@ -16,16 +16,22 @@ import signal import traceback import enum +import subprocess +import threading +import fcntl +import struct +import tempfile # external imports import Pyro5.errors from Pyro5.api import Daemon, Proxy, expose, oneway from Pyro5.serializers import SerpentSerializer -from lockfile.pidlockfile import PIDLockFile, AlreadyLocked +from fasteners import InterProcessLock # local imports from maestral.errors import SYNC_ERRORS, FATAL_ERRORS from maestral.constants import IS_FROZEN +from maestral.utils.appdirs import get_runtime_path threads = dict() @@ -76,6 +82,149 @@ def serpent_deserialize_api_error(class_name, d): ) +# ==== interprocess locking ============================================================== + +def _get_lockdata(): + + try: + os.O_LARGEFILE + except AttributeError: + start_len = 'll' + else: + start_len = 'qq' + + if (sys.platform.startswith(('netbsd', 'freebsd', 'openbsd')) + or sys.platform == 'darwin'): + if struct.calcsize('l') == 8: + off_t = 'l' + pid_t = 'i' + else: + off_t = 'lxxxx' + pid_t = 'l' + + fmt = off_t + off_t + pid_t + 'hh' + pid_index = 2 + lockdata = struct.pack(fmt, 0, 0, 0, fcntl.F_WRLCK, 0) + elif sys.platform.startswith('gnukfreebsd'): + fmt = 'qqihhi' + pid_index = 2 + lockdata = struct.pack(fmt, 0, 0, 0, fcntl.F_WRLCK, 0, 0) + elif sys.platform in ('hp-uxB', 'unixware7'): + fmt = 'hhlllii' + pid_index = 2 + lockdata = struct.pack(fmt, fcntl.F_WRLCK, 0, 0, 0, 0, 0, 0) + else: + fmt = 'hh' + start_len + 'ih' + pid_index = 4 + lockdata = struct.pack(fmt, fcntl.F_WRLCK, 0, 0, 0, 0, 0) + + return lockdata, fmt, pid_index + + +class Lock: + """ + A inter-process and inter-thread lock. This reuses uses code from + oslo.concurrency.lockutils but additionally allows non-blocking acquire. + """ + + _internal_locks = dict() + _external_locks = dict() + + _singleton_lock = threading.Lock() + + def __init__(self, name, lock_path=None): + + self.name = name + dirname = lock_path or tempfile.gettempdir() + lock_path = os.path.join(dirname, name) + + with self._singleton_lock: + try: + self._internal_lock = self._internal_locks[name] + except KeyError: + lock = threading.Semaphore() + self._internal_locks[name] = lock + self._internal_lock = lock + + try: + self._external_lock = self._external_locks[lock_path] + except KeyError: + lock = InterProcessLock(lock_path) + self._external_locks[lock_path] = lock + self._external_lock = lock + + def acquire(self, blocking=True): + """ + Attempts to acquire the given lock. + + :param bool blocking: Whether to wait forever to try to acquire the lock. + :returns: Whether or not the acquisition succeeded. + :rtype: bool + """ + locked_internal = self._internal_lock.acquire(blocking=blocking) + + if not locked_internal: + return False + + try: + locked_external = self._external_lock.acquire(blocking=blocking) + except Exception: + self._internal_lock.release() + raise + else: + + if locked_external: + return True + else: + self._internal_lock.release() + return False + + def release(self): + """Release the previously acquired lock.""" + self._external_lock.release() + self._internal_lock.release() + + def locking_pid(self): + """ + Returns the PID of the process which currently holds the lock or None. This is + atomic only when called from a different process than the lock holder. This method + is not thread safe: it may return None if the lock is acquired by the current + process during the call and may return a PID if the lock has been released by the + current process during the call. + + :returns: The PID of the process which currently holds the lock or None. + :rtype: int + """ + + if self._external_lock.acquired: + return os.getpid() + + try: + # don't close again in case we are the locking process + self._external_lock._do_open() + lockdata, fmt, pid_index = _get_lockdata() + lockdata = fcntl.fcntl(self._external_lock.lockfile, fcntl.F_GETLK, lockdata) + + lockdata_list = struct.unpack(fmt, lockdata) + pid = lockdata_list[pid_index] + + if pid > 0: + return pid + + except OSError: + pass + + def __enter__(self): + gotten = self.acquire() + if not gotten: + raise threading.ThreadError('Unable to acquire a lock') + + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.release() + + # ==== helpers for daemon management ===================================================== def _escape_spaces(string): @@ -93,12 +242,13 @@ def _send_term(pid): pass -def _process_exists(pid): - try: - os.kill(pid, signal.SIG_DFL) - return True - except ProcessLookupError: - return False +class MaestralLock(Lock): + """ + A inter-process and inter-thread lock for Maestral. + """ + + def __init__(self, config_name): + super().__init__(f'{config_name}.lock', get_runtime_path('maestral')) def sockpath_for_config(config_name): @@ -106,28 +256,11 @@ def sockpath_for_config(config_name): Returns the unix socket location to be used for the config. This should default to the apps runtime directory + '/maestral/CONFIG_NAME.sock'. """ - from maestral.utils.appdirs import get_runtime_path - return get_runtime_path('maestral', config_name + '.sock') - - -def pidpath_for_config(config_name): - from maestral.utils.appdirs import get_runtime_path - return get_runtime_path('maestral', config_name + '.pid') + return get_runtime_path('maestral', f'{config_name}.sock') -def is_pidfile_stale(pidfile): - """ - Determine whether a PID file is stale. Returns ``True`` if the PID file is stale, - ``False`` otherwise. The PID file is stale if its contents are valid but do not - match the PID of a currently-running process. - """ - result = False - - pid = pidfile.read_pid() - if pid: - return not _process_exists(pid) - else: - return result +def lockpath_for_config(config_name): + return get_runtime_path('maestral', f'{config_name}.lock') def get_maestral_pid(config_name): @@ -139,39 +272,16 @@ def get_maestral_pid(config_name): :rtype: int """ - lockfile = PIDLockFile(pidpath_for_config(config_name)) - pid = lockfile.read_pid() - - if pid and not is_pidfile_stale(lockfile): - return pid - else: - lockfile.break_lock() + return MaestralLock(config_name).locking_pid() def _wait_for_startup(config_name, timeout=8): - """Waits for the daemon to start and verifies Pyro communication. Returns ``Start.Ok`` - if startup and communication succeeds within timeout, ``Start.Failed`` otherwise.""" - t0 = time.time() - pid = None - - while not pid and time.time() - t0 < timeout / 2: - pid = get_maestral_pid(config_name) - time.sleep(0.2) - - if pid: - return _check_pyro_communication(config_name, timeout=int(timeout / 2)) - else: - return Start.Failed - - -def _check_pyro_communication(config_name, timeout=4): """Checks if we can communicate with the maestral daemon. Returns ``Start.Ok`` if communication succeeds within timeout, ``Start.Failed`` otherwise.""" sock_name = sockpath_for_config(config_name) maestral_daemon = Proxy(URI.format(_escape_spaces(config_name), './u:' + sock_name)) - # wait until we can communicate with daemon, timeout after :param:`timeout` while timeout > 0: try: maestral_daemon._pyroBind() @@ -201,24 +311,15 @@ def start_maestral_daemon(config_name='maestral', log_to_stdout=False): import threading from maestral.main import Maestral - sock_name = sockpath_for_config(config_name) - pid_name = pidpath_for_config(config_name) - - lockfile = PIDLockFile(pid_name) + sockpath = sockpath_for_config(config_name) if threading.current_thread() is threading.main_thread(): signal.signal(signal.SIGTERM, _sigterm_handler) # acquire PID lock file - - try: - lockfile.acquire(timeout=0.5) - except AlreadyLocked: - if is_pidfile_stale(lockfile): - lockfile.break_lock() - lockfile.acquire() - else: - return + lock = MaestralLock(config_name) + if not lock.acquire(blocking=False): + raise RuntimeError('Maestral daemon is already running') # Nice ourselves to give other processes priority. We will likely only # have significant CPU usage in case of many concurrent downloads. @@ -227,7 +328,7 @@ def start_maestral_daemon(config_name='maestral', log_to_stdout=False): try: # clean up old socket try: - os.remove(sock_name) + os.remove(sockpath) except FileNotFoundError: pass @@ -243,7 +344,7 @@ def start_maestral_daemon(config_name='maestral', log_to_stdout=False): m = ExposedMaestral(config_name, log_to_stdout=log_to_stdout) - with Daemon(unixsocket=sock_name) as daemon: + with Daemon(unixsocket=sockpath) as daemon: daemon.register(m, f'maestral.{_escape_spaces(config_name)}') daemon.requestLoop(loopCondition=m._loop_condition) @@ -252,7 +353,7 @@ def start_maestral_daemon(config_name='maestral', log_to_stdout=False): except (KeyboardInterrupt, SystemExit): sys.exit(0) finally: - lockfile.release() + lock.release() def start_maestral_daemon_thread(config_name='maestral', log_to_stdout=False): @@ -265,11 +366,9 @@ def start_maestral_daemon_thread(config_name='maestral', log_to_stdout=False): already running or ``Start.Failed`` if startup failed. """ - if config_name in threads and threads[config_name].is_alive(): + if MaestralLock(config_name).locking_pid(): return Start.AlreadyRunning - import threading - t = threading.Thread( target=start_maestral_daemon, args=(config_name, log_to_stdout), @@ -308,16 +407,16 @@ def start_maestral_daemon_process(config_name='maestral', log_to_stdout=False): :returns: ``Start.Ok`` if successful, ``Start.AlreadyRunning`` if the daemon was already running or ``Start.Failed`` if startup failed. """ - import subprocess + + if MaestralLock(config_name).locking_pid(): + return Start.AlreadyRunning + from shlex import quote import multiprocessing as mp # use nested Popen and multiprocessing.Process to effectively create double fork # see Unix 'double-fork magic' - if get_maestral_pid(config_name): - return Start.AlreadyRunning - if IS_FROZEN: def target(): @@ -356,39 +455,35 @@ def stop_maestral_daemon_process(config_name='maestral', timeout=10): if the daemon was not running. """ - lockfile = PIDLockFile(pidpath_for_config(config_name)) - pid = lockfile.read_pid() + pid = get_maestral_pid(config_name) - try: - if not pid or not _process_exists(pid): - return Exit.NotRunning + if not pid: + return Exit.NotRunning - try: - with MaestralProxy(config_name) as m: - m.stop_sync() - m.shutdown_pyro_daemon() - except Pyro5.errors.CommunicationError: - _send_term(pid) - finally: - while timeout > 0: - if not _process_exists(pid): - return Exit.Ok - else: - time.sleep(0.2) - timeout -= 0.2 + try: + with MaestralProxy(config_name) as m: + m.stop_sync() + m.shutdown_pyro_daemon() + except Pyro5.errors.CommunicationError: + _send_term(pid) + finally: + while timeout > 0: + if not get_maestral_pid(config_name): + return Exit.Ok + else: + time.sleep(0.2) + timeout -= 0.2 - # send SIGTERM after timeout and delete PID file - _send_term(pid) + # send SIGTERM after timeout and delete PID file + _send_term(pid) - time.sleep(1) + time.sleep(1) - if not _process_exists(pid): - return Exit.Ok - else: - os.kill(pid, signal.SIGKILL) - return Exit.Killed - finally: - lockfile.break_lock() + if not get_maestral_pid(config_name): + return Exit.Ok + else: + os.kill(pid, signal.SIGKILL) + return Exit.Killed def stop_maestral_daemon_thread(config_name='maestral', timeout=10): @@ -400,11 +495,7 @@ def stop_maestral_daemon_thread(config_name='maestral', timeout=10): ``Exit.Failed`` if it could not be stopped within timeout. """ - lockfile = PIDLockFile(pidpath_for_config(config_name)) - t = threads[config_name] - - if not t.is_alive(): - lockfile.break_lock() + if not MaestralLock(config_name).locking_pid(): return Exit.NotRunning # tell maestral daemon to shut down @@ -416,11 +507,11 @@ def stop_maestral_daemon_thread(config_name='maestral', timeout=10): return Exit.Failed # wait for maestral to carry out shutdown + t = threads.get(config_name) t.join(timeout=timeout) if t.is_alive(): return Exit.Failed else: - del threads[config_name] return Exit.Ok diff --git a/setup.py b/setup.py index 0e3e78f27..cb2b4be49 100644 --- a/setup.py +++ b/setup.py @@ -1,35 +1,11 @@ # -*- coding: utf-8 -*- # system imports -import sys -import os.path as osp from setuptools import setup, find_packages import importlib.util # local imports (must not depend on 3rd party packages) from maestral import __version__, __author__, __url__ -from maestral.utils.appdirs import get_runtime_path -from maestral.config.base import list_configs - - -# abort install if there are running daemons -running_daemons = [] - -for config in list_configs(): - pid_file = get_runtime_path('maestral', config + '.pid') - if osp.exists(pid_file): - running_daemons.append(config) - -if running_daemons: - sys.stderr.write(f""" -Maestral daemons with the following configs are running: - -{', '.join(running_daemons)} - -Please stop the daemons before updating to ensure a clean upgrade -of config files and compatibility been the CLI and daemon. - """) - sys.exit(1) # proceed with actual install @@ -42,7 +18,7 @@ 'jeepney;sys_platform=="linux"', 'keyring>=19.0.0', 'keyrings.alt>=3.1.0', - 'lockfile>=0.12.0', + 'fasteners', 'packaging', 'pathspec>=0.5.8', 'Pyro5>=5.7', From ce811b71166ca6a86b1bfe3a120865f59f73970d Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Tue, 12 May 2020 16:08:46 +0200 Subject: [PATCH 051/101] [daemon] remove support for obscure platforms --- maestral/daemon.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/maestral/daemon.py b/maestral/daemon.py index ce71ec182..8cad2169b 100644 --- a/maestral/daemon.py +++ b/maestral/daemon.py @@ -105,18 +105,20 @@ def _get_lockdata(): fmt = off_t + off_t + pid_t + 'hh' pid_index = 2 lockdata = struct.pack(fmt, 0, 0, 0, fcntl.F_WRLCK, 0) - elif sys.platform.startswith('gnukfreebsd'): - fmt = 'qqihhi' - pid_index = 2 - lockdata = struct.pack(fmt, 0, 0, 0, fcntl.F_WRLCK, 0, 0) - elif sys.platform in ('hp-uxB', 'unixware7'): - fmt = 'hhlllii' - pid_index = 2 - lockdata = struct.pack(fmt, fcntl.F_WRLCK, 0, 0, 0, 0, 0, 0) - else: + # elif sys.platform.startswith('gnukfreebsd'): + # fmt = 'qqihhi' + # pid_index = 2 + # lockdata = struct.pack(fmt, 0, 0, 0, fcntl.F_WRLCK, 0, 0) + # elif sys.platform in ('hp-uxB', 'unixware7'): + # fmt = 'hhlllii' + # pid_index = 2 + # lockdata = struct.pack(fmt, fcntl.F_WRLCK, 0, 0, 0, 0, 0, 0) + elif sys.platform.startswith('linux'): fmt = 'hh' + start_len + 'ih' pid_index = 4 lockdata = struct.pack(fmt, fcntl.F_WRLCK, 0, 0, 0, 0, 0) + else: + raise RuntimeError(f'Unsupported platform {sys.platform}') return lockdata, fmt, pid_index From de7108f7d08b50e2927faeb6043d13cebd7490a1 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Tue, 12 May 2020 17:01:37 +0200 Subject: [PATCH 052/101] update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e9845bfb..026605fcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,13 @@ has been paused. - Fixes an issue where download errors would show a rev number instead of the Dropbox path. +- Fixes a race condition when two processes try to start a sync daemon at the same time. - Fixes an issue in the macOS GUI where updating the displayed sync issues could fail. +#### Removed: + +- Removed `lockfile` dependency. + ## v1.0.2 This release fixes bugs in the command line interface. From f0e6646a718c0dbcaf4eef7f0026ecbe41e0682b Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Wed, 13 May 2020 13:01:21 +0200 Subject: [PATCH 053/101] [daemon] improve interprocess Lock - enable singleton creation of Lock - thread safe `locked` and `locking_pid` methods - removed context manager methods --- maestral/daemon.py | 134 +++++++++++++++++++++++---------------------- 1 file changed, 68 insertions(+), 66 deletions(-) diff --git a/maestral/daemon.py b/maestral/daemon.py index 8cad2169b..174bf3418 100644 --- a/maestral/daemon.py +++ b/maestral/daemon.py @@ -125,106 +125,106 @@ def _get_lockdata(): class Lock: """ - A inter-process and inter-thread lock. This reuses uses code from - oslo.concurrency.lockutils but additionally allows non-blocking acquire. + A inter-process and inter-thread lock. This reuses uses code from oslo.concurrency + but provides non-blocking acquire. Use the :meth:`singleton` class method to retrieve + an existing instance for thread-safe usage. """ - _internal_locks = dict() - _external_locks = dict() - + _instances = dict() _singleton_lock = threading.Lock() + @classmethod + def singleton(cls, name, lock_path=None): + + with cls._singleton_lock: + try: + instance = cls._instances[name] + except KeyError: + instance = cls(name, lock_path) + cls._instances[name] = instance + + return instance + def __init__(self, name, lock_path=None): self.name = name dirname = lock_path or tempfile.gettempdir() lock_path = os.path.join(dirname, name) - with self._singleton_lock: - try: - self._internal_lock = self._internal_locks[name] - except KeyError: - lock = threading.Semaphore() - self._internal_locks[name] = lock - self._internal_lock = lock + self._internal_lock = threading.Semaphore() + self._external_lock = InterProcessLock(lock_path) - try: - self._external_lock = self._external_locks[lock_path] - except KeyError: - lock = InterProcessLock(lock_path) - self._external_locks[lock_path] = lock - self._external_lock = lock + self._lock = threading.RLock() - def acquire(self, blocking=True): + def acquire(self): """ Attempts to acquire the given lock. - :param bool blocking: Whether to wait forever to try to acquire the lock. :returns: Whether or not the acquisition succeeded. :rtype: bool """ - locked_internal = self._internal_lock.acquire(blocking=blocking) - if not locked_internal: - return False + with self._lock: + locked_internal = self._internal_lock.acquire(blocking=False) - try: - locked_external = self._external_lock.acquire(blocking=blocking) - except Exception: - self._internal_lock.release() - raise - else: + if not locked_internal: + return False - if locked_external: - return True - else: + try: + locked_external = self._external_lock.acquire(blocking=False) + except Exception: self._internal_lock.release() - return False + raise + else: + + if locked_external: + return True + else: + self._internal_lock.release() + return False def release(self): """Release the previously acquired lock.""" - self._external_lock.release() - self._internal_lock.release() + with self._lock: + self._external_lock.release() + self._internal_lock.release() + + def locked(self): + """Checks if the lock is currently held by any thread or process.""" + with self._lock: + gotten = self.acquire() + if gotten: + self.release() + return not gotten def locking_pid(self): """ - Returns the PID of the process which currently holds the lock or None. This is - atomic only when called from a different process than the lock holder. This method - is not thread safe: it may return None if the lock is acquired by the current - process during the call and may return a PID if the lock has been released by the - current process during the call. + Returns the PID of the process which currently holds the lock or None. :returns: The PID of the process which currently holds the lock or None. :rtype: int """ - if self._external_lock.acquired: - return os.getpid() - - try: - # don't close again in case we are the locking process - self._external_lock._do_open() - lockdata, fmt, pid_index = _get_lockdata() - lockdata = fcntl.fcntl(self._external_lock.lockfile, fcntl.F_GETLK, lockdata) - - lockdata_list = struct.unpack(fmt, lockdata) - pid = lockdata_list[pid_index] + with self._lock: - if pid > 0: - return pid + if self._external_lock.acquired: + return os.getpid() - except OSError: - pass + try: + # don't close again in case we are the locking process + self._external_lock._do_open() + lockdata, fmt, pid_index = _get_lockdata() + lockdata = fcntl.fcntl(self._external_lock.lockfile, + fcntl.F_GETLK, lockdata) - def __enter__(self): - gotten = self.acquire() - if not gotten: - raise threading.ThreadError('Unable to acquire a lock') + lockdata_list = struct.unpack(fmt, lockdata) + pid = lockdata_list[pid_index] - return self + if pid > 0: + return pid - def __exit__(self, exc_type, exc_val, exc_tb): - self.release() + except OSError: + pass # ==== helpers for daemon management ===================================================== @@ -244,13 +244,15 @@ def _send_term(pid): pass -class MaestralLock(Lock): +class MaestralLock: """ A inter-process and inter-thread lock for Maestral. """ - def __init__(self, config_name): - super().__init__(f'{config_name}.lock', get_runtime_path('maestral')) + def __new__(cls, config_name): + name = f'{config_name}.lock' + path = get_runtime_path('maestral') + return Lock.singleton(name, path) def sockpath_for_config(config_name): @@ -320,7 +322,7 @@ def start_maestral_daemon(config_name='maestral', log_to_stdout=False): # acquire PID lock file lock = MaestralLock(config_name) - if not lock.acquire(blocking=False): + if not lock.acquire(): raise RuntimeError('Maestral daemon is already running') # Nice ourselves to give other processes priority. We will likely only From b0508fae9688499290d72d5b514123ab4d49c6ba Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Wed, 13 May 2020 13:03:38 +0200 Subject: [PATCH 054/101] [daemon] added `is_running` method for PID-free check --- maestral/cli.py | 4 ++-- maestral/daemon.py | 39 ++++++++++++++++++++++++++------------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/maestral/cli.py b/maestral/cli.py index 1b2a27715..ba3e536ed 100755 --- a/maestral/cli.py +++ b/maestral/cli.py @@ -838,14 +838,14 @@ def rebuild_index(config_name: str): @main.command(help_priority=16) def configs(): """Lists all configured Dropbox accounts.""" - from maestral.daemon import get_maestral_pid + from maestral.daemon import is_running # clean up stale configs config_names = list_configs() for name in config_names: dbid = MaestralConfig(name).get('account', 'account_id') - if dbid == '' and not get_maestral_pid(name): + if dbid == '' and not is_running(name): remove_configuration(name) # display remaining configs diff --git a/maestral/daemon.py b/maestral/daemon.py index 174bf3418..ad417714a 100644 --- a/maestral/daemon.py +++ b/maestral/daemon.py @@ -279,6 +279,18 @@ def get_maestral_pid(config_name): return MaestralLock(config_name).locking_pid() +def is_running(config_name): + """ + Checks if a daemon is currently running. + + :param str config_name: The name of the Maestral configuration. + :returns: Whether the daemon is running. + :rtype: bool + """ + + return MaestralLock(config_name).locked() + + def _wait_for_startup(config_name, timeout=8): """Checks if we can communicate with the maestral daemon. Returns ``Start.Ok`` if communication succeeds within timeout, ``Start.Failed`` otherwise.""" @@ -370,7 +382,7 @@ def start_maestral_daemon_thread(config_name='maestral', log_to_stdout=False): already running or ``Start.Failed`` if startup failed. """ - if MaestralLock(config_name).locking_pid(): + if is_running(config_name): return Start.AlreadyRunning t = threading.Thread( @@ -412,7 +424,7 @@ def start_maestral_daemon_process(config_name='maestral', log_to_stdout=False): already running or ``Start.Failed`` if startup failed. """ - if MaestralLock(config_name).locking_pid(): + if is_running(config_name): return Start.AlreadyRunning from shlex import quote @@ -459,20 +471,21 @@ def stop_maestral_daemon_process(config_name='maestral', timeout=10): if the daemon was not running. """ - pid = get_maestral_pid(config_name) - - if not pid: + if not is_running(config_name): return Exit.NotRunning + pid = get_maestral_pid(config_name) + try: with MaestralProxy(config_name) as m: m.stop_sync() m.shutdown_pyro_daemon() except Pyro5.errors.CommunicationError: - _send_term(pid) + if pid: + _send_term(pid) finally: while timeout > 0: - if not get_maestral_pid(config_name): + if not is_running(config_name): return Exit.Ok else: time.sleep(0.2) @@ -483,11 +496,13 @@ def stop_maestral_daemon_process(config_name='maestral', timeout=10): time.sleep(1) - if not get_maestral_pid(config_name): + if not is_running(config_name): return Exit.Ok - else: + elif pid: os.kill(pid, signal.SIGKILL) return Exit.Killed + else: + return Exit.Failed def stop_maestral_daemon_thread(config_name='maestral', timeout=10): @@ -499,7 +514,7 @@ def stop_maestral_daemon_thread(config_name='maestral', timeout=10): ``Exit.Failed`` if it could not be stopped within timeout. """ - if not MaestralLock(config_name).locking_pid(): + if not is_running(config_name): return Exit.NotRunning # tell maestral daemon to shut down @@ -531,9 +546,7 @@ def get_maestral_proxy(config_name='maestral', fallback=False): ``fallback`` is ``False``. """ - pid = get_maestral_pid(config_name) - - if pid: + if is_running(config_name): sock_name = sockpath_for_config(config_name) sys.excepthook = Pyro5.errors.excepthook From b6a5d220327f4c1ad4382ba303c26adebdedbd54 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Wed, 13 May 2020 13:03:56 +0200 Subject: [PATCH 055/101] [daemon] updated doc strings --- maestral/daemon.py | 43 ++++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/maestral/daemon.py b/maestral/daemon.py index ad417714a..33cc38475 100644 --- a/maestral/daemon.py +++ b/maestral/daemon.py @@ -315,14 +315,17 @@ def _wait_for_startup(config_name, timeout=8): def start_maestral_daemon(config_name='maestral', log_to_stdout=False): """ + Starts the Maestral daemon with event loop in the current thread. Startup is race + free: there will never be two daemons running for the same config. + Wraps :class:`main.Maestral` as Pyro daemon object, creates a new instance and starts Pyro's event loop to listen for requests on a unix domain socket. This call will block until the event loop shuts down. - This command will return silently if the daemon is already running. - :param str config_name: The name of the Maestral configuration to use. :param bool log_to_stdout: If ``True``, write logs to stdout. Defaults to ``False``. + :raises: :class:`RuntimeError` if a daemon for the given ``config_name`` is already + running. """ import threading from maestral.main import Maestral @@ -374,12 +377,14 @@ def start_maestral_daemon(config_name='maestral', log_to_stdout=False): def start_maestral_daemon_thread(config_name='maestral', log_to_stdout=False): """ - Starts the Maestral daemon in a thread (by calling :func:`start_maestral_daemon`). + Starts the Maestral daemon in a new thread by calling :func:`start_maestral_daemon`. + Startup is race free: there will never be two daemons running for the same config. :param str config_name: The name of the Maestral configuration to use. :param bool log_to_stdout: If ``True``, write logs to stdout. Defaults to ``False``. :returns: ``Start.Ok`` if successful, ``Start.AlreadyRunning`` if the daemon was - already running or ``Start.Failed`` if startup failed. + already running or ``Start.Failed`` if startup failed. It is possible that + Start.Ok is returned instead of Start.AlreadyRunning in case of a race. """ if is_running(config_name): @@ -403,17 +408,18 @@ def start_maestral_daemon_thread(config_name='maestral', log_to_stdout=False): def start_maestral_daemon_process(config_name='maestral', log_to_stdout=False): """ - Starts the Maestral daemon in a separate process by calling - :func:`start_maestral_daemon`. + Starts the Maestral daemon in a new process by calling :func:`start_maestral_daemon`. + Startup is race free: there will never be two daemons running for the same config. - This function assumes that ``sys.executable`` points to the Python executable. In - case of a frozen app, the executable must take the command line argument - ``--frozen-daemon to start`` a daemon process which is *not syncing*, .i.e., just run - :meth:`start_maestral_daemon`. This is currently supported through the - constole_script entry points of both `maestral` and `maestral_qt`. + This function assumes that ``sys.executable`` points to the Python executable or a + frozen executable. In case of a frozen executable, the executable must take the + command line argument ``--frozen-daemon to start`` to start a daemon process which is + *not syncing*, .i.e., just run :meth:`start_maestral_daemon`. This is currently + supported through the console_script entry points of both `maestral` and + `maestral_qt`. Starting a detached daemon process is difficult from a standalone executable since - the typical double-fork magic may fail on macOS and we do not have acccess to a + the typical double-fork magic may fail on macOS and we do not have access to a standalone Python interpreter to spawn a subprocess. Our approach mimics the "freeze support" implemented by the multiprocessing module but fully detaches the spawned process. @@ -421,7 +427,8 @@ def start_maestral_daemon_process(config_name='maestral', log_to_stdout=False): :param str config_name: The name of the Maestral configuration to use. :param bool log_to_stdout: If ``True``, write logs to stdout. Defaults to ``False``. :returns: ``Start.Ok`` if successful, ``Start.AlreadyRunning`` if the daemon was - already running or ``Start.Failed`` if startup failed. + already running or ``Start.Failed`` if startup failed. It is possible that + Start.Ok is returned instead of Start.AlreadyRunning in case of a race. """ if is_running(config_name): @@ -462,13 +469,15 @@ def target(): def stop_maestral_daemon_process(config_name='maestral', timeout=10): """Stops a maestral daemon process by finding its PID and shutting it down. - This function first tries to shut down Maestral gracefully. If this fails, it will - send SIGTERM. If that fails as well, it will send SIGKILL to the process. + This function first tries to shut down Maestral gracefully. If this fails and we know + its PID, it will send SIGTERM. If that fails as well, it will send SIGKILL to the + process. :param str config_name: The name of the Maestral configuration to use. :param float timeout: Number of sec to wait for daemon to shut down before killing it. - :returns: ``Exit.Ok`` if successful, ``Exit.Killed`` if killed and ``Exit.NotRunning`` - if the daemon was not running. + :returns: ``Exit.Ok`` if successful, ``Exit.Killed`` if killed, ``Exit.NotRunning`` if + the daemon was not running and ``Exit.Failed`` if killing the process failed + because we could not retrieve its PID. """ if not is_running(config_name): From 526b6b27f9b5d56f3b44de8258422e6216e16e30 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Wed, 13 May 2020 23:33:31 +0200 Subject: [PATCH 056/101] flake8 --- maestral/cli.py | 2 +- maestral/daemon.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/maestral/cli.py b/maestral/cli.py index ba3e536ed..a9ec528b0 100755 --- a/maestral/cli.py +++ b/maestral/cli.py @@ -1129,4 +1129,4 @@ def notify_snooze(config_name: str, minutes: int): click.echo(f'Notifications snoozed for {minutes} min. ' 'Set snooze to 0 to reset.') else: - click.echo(f'Notifications enabled.') + click.echo('Notifications enabled.') diff --git a/maestral/daemon.py b/maestral/daemon.py index 33cc38475..99a711096 100644 --- a/maestral/daemon.py +++ b/maestral/daemon.py @@ -264,7 +264,7 @@ def sockpath_for_config(config_name): def lockpath_for_config(config_name): - return get_runtime_path('maestral', f'{config_name}.lock') + return get_runtime_path('maestral', f'{config_name}.lock') def get_maestral_pid(config_name): From a9cfa4c1d927d9e35c1e6045a71b047b720ba2cf Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Thu, 14 May 2020 14:55:50 +0200 Subject: [PATCH 057/101] updated changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 026605fcc..1dfe98292 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,9 +14,10 @@ - Fixes a race condition when two processes try to start a sync daemon at the same time. - Fixes an issue in the macOS GUI where updating the displayed sync issues could fail. -#### Removed: +#### Dependencies: - Removed `lockfile` dependency. +- Added `fasteners` dependency. ## v1.0.2 From a34b113501298a8cd8ad49927ad8a19f90b2fef7 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Thu, 14 May 2020 14:56:17 +0200 Subject: [PATCH 058/101] bumped to 1.0.3.beta1 --- maestral/__init__.py | 2 +- setup.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/maestral/__init__.py b/maestral/__init__.py index e11848404..395b2f103 100644 --- a/maestral/__init__.py +++ b/maestral/__init__.py @@ -16,6 +16,6 @@ """ -__version__ = '1.0.3.dev1' +__version__ = '1.0.3.beta1' __author__ = 'Sam Schott' __url__ = 'https://github.com/SamSchott/maestral' diff --git a/setup.py b/setup.py index cb2b4be49..68bb0e7ac 100644 --- a/setup.py +++ b/setup.py @@ -30,8 +30,8 @@ ] gui_requires = [ - 'maestral_qt>=1.0.0;sys_platform=="linux"', - 'maestral_cocoa>=1.0.0;sys_platform=="darwin"', + 'maestral_qt>=1.0.3.beta1;sys_platform=="linux"', + 'maestral_cocoa>=1.0.3.beta1;sys_platform=="darwin"', ] syslog_requires = ['systemd-python'] From 729722c3ab33a819dc195fa7283c088f21389088 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Thu, 14 May 2020 15:09:12 +0200 Subject: [PATCH 059/101] [package] update git repository name --- package/build_linux.sh | 4 ++-- package/build_macos_cocoa.sh | 4 ++-- package/build_macos_qt.sh | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package/build_linux.sh b/package/build_linux.sh index cec18d574..3c113e66d 100755 --- a/package/build_linux.sh +++ b/package/build_linux.sh @@ -6,8 +6,8 @@ echo "**** INSTALLING DEPENDENCIES ****************************" pip install -U pyinstaller -git clone https://github.com/samschott/maestral-dropbox build/maestral-dropbox -cd build/maestral-dropbox +git clone https://github.com/samschott/maestral build/maestral +cd build/maestral git checkout develop git pull pip install . diff --git a/package/build_macos_cocoa.sh b/package/build_macos_cocoa.sh index 462bdace0..00ac321c1 100755 --- a/package/build_macos_cocoa.sh +++ b/package/build_macos_cocoa.sh @@ -20,8 +20,8 @@ cd .. pip install . cd ../.. -git clone https://github.com/samschott/maestral-dropbox build/maestral-dropbox -cd build/maestral-dropbox +git clone https://github.com/samschott/maestral build/maestral +cd build/maestral git checkout develop git pull pip install . diff --git a/package/build_macos_qt.sh b/package/build_macos_qt.sh index 1b7ede3c2..ce3ee846d 100755 --- a/package/build_macos_qt.sh +++ b/package/build_macos_qt.sh @@ -20,8 +20,8 @@ cd .. pip install . cd ../.. -git clone https://github.com/samschott/maestral-dropbox build/maestral-dropbox -cd build/maestral-dropbox +git clone https://github.com/samschott/maestral build/maestral +cd build/maestral git checkout develop git pull pip install . From 35579d79233f26f97a2473f242177af66b57f715 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Thu, 14 May 2020 15:59:38 +0200 Subject: [PATCH 060/101] [package] use hardened runtime on macOS --- package/build_macos_cocoa.sh | 2 +- package/build_macos_qt.sh | 2 +- package/entitlements.plist | 8 ++++++++ 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 package/entitlements.plist diff --git a/package/build_macos_cocoa.sh b/package/build_macos_cocoa.sh index 00ac321c1..c9a4a58fb 100755 --- a/package/build_macos_cocoa.sh +++ b/package/build_macos_cocoa.sh @@ -48,6 +48,6 @@ echo "**** RUNNING POST-BUILD SCRIPTS ************************" echo "**** SIGNING ******************************************" -codesign -s "Apple Development: sam.schott@outlook.com (FJNXBRUVWL)" --deep dist/Maestral.app +codesign -s "Apple Development: sam.schott@outlook.com (FJNXBRUVWL)" --entitlements entitlements.plist --deep -o runtime dist/Maestral.app echo "**** DONE *********************************************" diff --git a/package/build_macos_qt.sh b/package/build_macos_qt.sh index ce3ee846d..48dc345e9 100755 --- a/package/build_macos_qt.sh +++ b/package/build_macos_qt.sh @@ -48,6 +48,6 @@ python3 post_build_macos_qt.py echo "**** SIGNING ******************************************" -codesign -s "Apple Development: sam.schott@outlook.com (FJNXBRUVWL)" --deep dist/Maestral.app +codesign -s "Apple Development: sam.schott@outlook.com (FJNXBRUVWL)" --entitlements entitlements.plist --deep -o runtime dist/Maestral.app echo "**** DONE *********************************************" diff --git a/package/entitlements.plist b/package/entitlements.plist new file mode 100644 index 000000000..a1c430a57 --- /dev/null +++ b/package/entitlements.plist @@ -0,0 +1,8 @@ + + + + + com.apple.security.cs.allow-unsigned-executable-memory + + + From 4bbe1f2722a9323a3e57bf8a75044c9147e28907 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Thu, 14 May 2020 17:18:23 +0200 Subject: [PATCH 061/101] [package] create dmg for macos --- package/build_macos_cocoa.sh | 27 ++++++++++++++++++++------- package/build_macos_qt.sh | 25 +++++++++++++++++++++---- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/package/build_macos_cocoa.sh b/package/build_macos_cocoa.sh index c9a4a58fb..2062d6af3 100755 --- a/package/build_macos_cocoa.sh +++ b/package/build_macos_cocoa.sh @@ -3,7 +3,7 @@ SPEC_FILE=maestral_macos_cocoa.spec BUILD_NO=$(grep -E -o "[0-9]*" bundle_version_macos.txt) -echo "**** INSTALLING DEPENDENCIES ****************************" +echo "**** INSTALLING DEPENDENCIES ***************************" git clone https://github.com/pyinstaller/pyinstaller.git build/pyinstaller cd build/pyinstaller @@ -38,16 +38,29 @@ echo "**** BUILD NUMBER $BUILD_NO ****************************" python3 -m PyInstaller -y --clean -w $SPEC_FILE -echo "**** COPY ENTRY POINT **********************************" +echo "**** COPY CLI ENTRY POINT ******************************" cp bin/maestral_cli dist/Maestral.app/Contents/MacOS/maestral_cli -echo "**** RUNNING POST-BUILD SCRIPTS ************************" +echo "**** SIGNING *******************************************" -# pass +codesign -s "Apple Development: sam.schott@outlook.com (FJNXBRUVWL)" \ + --entitlements entitlements.plist --deep -o runtime dist/Maestral.app -echo "**** SIGNING ******************************************" +echo "**** CREATING DMG **************************************" -codesign -s "Apple Development: sam.schott@outlook.com (FJNXBRUVWL)" --entitlements entitlements.plist --deep -o runtime dist/Maestral.app +test -f dist/dmg-folder && rm -Rf dist/dmg-folder +mkdir dist/dmg-folder +cd dist/dmg-folder +ln -s /Applications +cd .. +cd .. +cp -R dist/Maestral.app dist/dmg-folder/ +hdiutil create -volname "Maestral" \ + -srcfolder dist/dmg-folder -ov -format UDBZ dist/Maestral.dmg +rm -Rf dist/dmg-folder + +codesign --verify --sign "Apple Development: sam.schott@outlook.com (FJNXBRUVWL)" dist/Maestral.dmg +md5 -r dist/Maestral.dmg -echo "**** DONE *********************************************" +echo "**** DONE **********************************************" diff --git a/package/build_macos_qt.sh b/package/build_macos_qt.sh index 48dc345e9..0e9addca3 100755 --- a/package/build_macos_qt.sh +++ b/package/build_macos_qt.sh @@ -3,7 +3,7 @@ SPEC_FILE=maestral_macos_qt.spec BUILD_NO=$(grep -E -o "[0-9]*" bundle_version_macos.txt) -echo "**** INSTALLING DEPENDENCIES ****************************" +echo "**** INSTALLING DEPENDENCIES ***************************" git clone https://github.com/pyinstaller/pyinstaller.git build/pyinstaller cd build/pyinstaller @@ -38,7 +38,7 @@ echo "**** BUILD NUMBER $BUILD_NO ****************************" python3 -m PyInstaller -y --clean -w $SPEC_FILE -echo "**** COPY ENTRY POINT **********************************" +echo "**** COPY CLI ENTRY POINT ******************************" cp bin/maestral_cli dist/Maestral.app/Contents/MacOS/maestral_cli @@ -46,8 +46,25 @@ echo "**** RUNNING POST-BUILD SCRIPTS ************************" python3 post_build_macos_qt.py -echo "**** SIGNING ******************************************" +echo "**** SIGNING *******************************************" -codesign -s "Apple Development: sam.schott@outlook.com (FJNXBRUVWL)" --entitlements entitlements.plist --deep -o runtime dist/Maestral.app +codesign --verify --sign "Apple Development: sam.schott@outlook.com (FJNXBRUVWL)" \ + --entitlements entitlements.plist --deep --options runtime 'dist/Maestral.app' + +echo "**** CREATING DMG *************************************" + +test -f dist/dmg-folder && rm -Rf dist/dmg-folder +mkdir dist/dmg-folder +cd dist/dmg-folder +ln -s /Applications +cd .. +cd .. +cp -R dist/Maestral.app dist/dmg-folder/ +hdiutil create -volname "Maestral" \ + -srcfolder dist/dmg-folder -ov -format UDBZ dist/Maestral.dmg +rm -Rf dist/dmg-folder + +codesign --verify --sign "Apple Development: sam.schott@outlook.com (FJNXBRUVWL)" dist/Maestral.dmg +md5 -r dist/Maestral.dmg echo "**** DONE *********************************************" From cd55f118ef422d56aa605f0e871ad094641a959c Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Thu, 14 May 2020 22:51:33 +0200 Subject: [PATCH 062/101] [daemon] update doc string --- maestral/daemon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/maestral/daemon.py b/maestral/daemon.py index 99a711096..0a123ea9a 100644 --- a/maestral/daemon.py +++ b/maestral/daemon.py @@ -413,8 +413,8 @@ def start_maestral_daemon_process(config_name='maestral', log_to_stdout=False): This function assumes that ``sys.executable`` points to the Python executable or a frozen executable. In case of a frozen executable, the executable must take the - command line argument ``--frozen-daemon to start`` to start a daemon process which is - *not syncing*, .i.e., just run :meth:`start_maestral_daemon`. This is currently + command line argument ``--frozen-daemon`` to start a daemon process which is *not + syncing*, .i.e., just run :meth:`start_maestral_daemon`. This is currently supported through the console_script entry points of both `maestral` and `maestral_qt`. From 4d64f2e0a5c46a2e5c702191d215f13c2f06470a Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Thu, 14 May 2020 23:51:16 +0200 Subject: [PATCH 063/101] [cli] add "-h" to help arguments --- maestral/cli.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/maestral/cli.py b/maestral/cli.py index a9ec528b0..5c22b1db3 100755 --- a/maestral/cli.py +++ b/maestral/cli.py @@ -383,8 +383,10 @@ def _run_daemon(ctx, param, value): help='For internal use only.' ) +CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) -@click.group(cls=SpecialHelpOrder) + +@click.group(cls=SpecialHelpOrder, context_settings=CONTEXT_SETTINGS) @frozen_daemon_option @hidden_config_option def main(): From 514566c78fb7ebfcdd7cecc796d42dcf68f50b6f Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Thu, 14 May 2020 23:52:29 +0200 Subject: [PATCH 064/101] [package] don't use "open" for cli --- package/bin/maestral_cli | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/package/bin/maestral_cli b/package/bin/maestral_cli index 46885bf7b..49de7f1b4 100755 --- a/package/bin/maestral_cli +++ b/package/bin/maestral_cli @@ -2,9 +2,4 @@ SCRIPTPATH="$(dirname "$(readlink "$0")")" -if [ "$1" = "gui" ] -then - open -n -a Maestral.app --args "$@" -else - "$SCRIPTPATH/main" --cli "$@" -fi +"$SCRIPTPATH/main" --cli "$@" From 413176486b6bccda387ffd23829ebd0103cd0f47 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Thu, 14 May 2020 23:54:13 +0200 Subject: [PATCH 065/101] updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dfe98292..d0eae94a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Significantly reduced CPU usage of the GUI on macOS. - Show both the daemon and GUI version in the GUI settings window. +- The command line tool on macOS now provides proper help output. #### Fixed: From 168d811cb107bc57cc7756b8b87bbd980433974b Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Fri, 15 May 2020 11:25:48 +0200 Subject: [PATCH 066/101] updated changelog --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0eae94a5..2dfb773ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,11 @@ #### Changed: -- Significantly reduced CPU usage of the GUI on macOS. +- Both "-h" and "--help" can now be used to print help output for a command. - Show both the daemon and GUI version in the GUI settings window. -- The command line tool on macOS now provides proper help output. +- The command line tool bundled with the macOS app now provides proper help output. +- Significantly reduced CPU usage of the GUI on macOS. +- The macOS app now uses a hardened runtime, in preparation for app notarization. #### Fixed: From 0a1cefec3f6c2003e427633dc3e54efbe363ca24 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Fri, 15 May 2020 16:14:01 +0200 Subject: [PATCH 067/101] [package] added notarisation step --- package/{build_linux.sh => build-linux.sh} | 0 ...ld_macos_cocoa.sh => build-macos-cocoa.sh} | 6 +- .../{build_macos_qt.sh => build-macos-qt.sh} | 14 ++-- package/macos-notarize-app.sh | 76 +++++++++++++++++++ 4 files changed, 88 insertions(+), 8 deletions(-) rename package/{build_linux.sh => build-linux.sh} (100%) rename package/{build_macos_cocoa.sh => build-macos-cocoa.sh} (92%) rename package/{build_macos_qt.sh => build-macos-qt.sh} (79%) create mode 100644 package/macos-notarize-app.sh diff --git a/package/build_linux.sh b/package/build-linux.sh similarity index 100% rename from package/build_linux.sh rename to package/build-linux.sh diff --git a/package/build_macos_cocoa.sh b/package/build-macos-cocoa.sh similarity index 92% rename from package/build_macos_cocoa.sh rename to package/build-macos-cocoa.sh index 2062d6af3..2600a5433 100755 --- a/package/build_macos_cocoa.sh +++ b/package/build-macos-cocoa.sh @@ -42,11 +42,13 @@ echo "**** COPY CLI ENTRY POINT ******************************" cp bin/maestral_cli dist/Maestral.app/Contents/MacOS/maestral_cli -echo "**** SIGNING *******************************************" +echo "**** SIGN AND NOTARIZE *********************************" codesign -s "Apple Development: sam.schott@outlook.com (FJNXBRUVWL)" \ --entitlements entitlements.plist --deep -o runtime dist/Maestral.app +macos-notarize-app.sh dist/Maestral.app + echo "**** CREATING DMG **************************************" test -f dist/dmg-folder && rm -Rf dist/dmg-folder @@ -56,7 +58,7 @@ ln -s /Applications cd .. cd .. cp -R dist/Maestral.app dist/dmg-folder/ -hdiutil create -volname "Maestral" \ +hdiutil create -volname Maestral \ -srcfolder dist/dmg-folder -ov -format UDBZ dist/Maestral.dmg rm -Rf dist/dmg-folder diff --git a/package/build_macos_qt.sh b/package/build-macos-qt.sh similarity index 79% rename from package/build_macos_qt.sh rename to package/build-macos-qt.sh index 0e9addca3..d2d228a42 100755 --- a/package/build_macos_qt.sh +++ b/package/build-macos-qt.sh @@ -46,12 +46,14 @@ echo "**** RUNNING POST-BUILD SCRIPTS ************************" python3 post_build_macos_qt.py -echo "**** SIGNING *******************************************" +echo "**** SIGN AND NOTARIZE *********************************" -codesign --verify --sign "Apple Development: sam.schott@outlook.com (FJNXBRUVWL)" \ - --entitlements entitlements.plist --deep --options runtime 'dist/Maestral.app' +codesign -s "Apple Development: sam.schott@outlook.com (FJNXBRUVWL)" \ + --entitlements entitlements.plist --deep -o runtime dist/Maestral.app -echo "**** CREATING DMG *************************************" +macos-notarize-app.sh dist/Maestral.app + +echo "**** CREATING DMG **************************************" test -f dist/dmg-folder && rm -Rf dist/dmg-folder mkdir dist/dmg-folder @@ -60,11 +62,11 @@ ln -s /Applications cd .. cd .. cp -R dist/Maestral.app dist/dmg-folder/ -hdiutil create -volname "Maestral" \ +hdiutil create -volname Maestral \ -srcfolder dist/dmg-folder -ov -format UDBZ dist/Maestral.dmg rm -Rf dist/dmg-folder codesign --verify --sign "Apple Development: sam.schott@outlook.com (FJNXBRUVWL)" dist/Maestral.dmg md5 -r dist/Maestral.dmg -echo "**** DONE *********************************************" +echo "**** DONE **********************************************" diff --git a/package/macos-notarize-app.sh b/package/macos-notarize-app.sh new file mode 100644 index 000000000..df90de703 --- /dev/null +++ b/package/macos-notarize-app.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash + +if [ -z "$1" ]; then + echo "Specify app bundle as first parameter" + exit 1 +fi + +if [ -z "$APPLE_ID_USER" ] || [ -z "$APPLE_ID_PASSWORD" ]; then + echo "You need to set your Apple ID credentials with \$APPLE_ID_USER and \$APPLE_ID_PASSWORD." + exit 1 +fi + +APP_BUNDLE=$(basename "$1") +APP_BUNDLE_DIR=$(dirname "$1") + +cd "$APP_BUNDLE_DIR" || exit 1 + +# Package app for submission +echo "Generating ZIP archive ${APP_BUNDLE}.zip..." +ditto -c -k --rsrc --keepParent "$APP_BUNDLE" "${APP_BUNDLE}.zip" + +# Submit for notarization +echo "Submitting $APP_BUNDLE for notarization..." +RESULT=$(xcrun altool --notarize-app --type osx \ + --file "${APP_BUNDLE}.zip" \ + --primary-bundle-id org.musicbrainz.Picard \ + --username $APPLE_ID_USER \ + --password @env:APPLE_ID_PASSWORD \ + -itc_provider MetaBrainzFoundationInc \ + --output-format xml) + +if [ $? -ne 0 ]; then + echo "Submitting $APP_BUNDLE failed:" + echo "$RESULT" + exit 1 +fi + +REQUEST_UUID=$(echo "$RESULT" | xpath \ + "//key[normalize-space(text()) = 'RequestUUID']/following-sibling::string[1]/text()" 2> /dev/null) + +if [ -z "$REQUEST_UUID" ]; then + echo "Submitting $APP_BUNDLE failed:" + echo "$RESULT" + exit 1 +fi + +echo "$(echo "$RESULT" | xpath \ + "//key[normalize-space(text()) = 'success-message']/following-sibling::string[1]/text()" 2> /dev/null)" + +# Poll for notarization status +echo "Submitted notarization request $REQUEST_UUID, waiting for response..." +sleep 60 +while : +do + RESULT=$(xcrun altool --notarization-info "$REQUEST_UUID" \ + --username "$APPLE_ID_USER" \ + --password @env:APPLE_ID_PASSWORD \ + --output-format xml) + STATUS=$(echo "$RESULT" | xpath \ + "//key[normalize-space(text()) = 'Status']/following-sibling::string[1]/text()" 2> /dev/null) + + if [ "$STATUS" = "success" ]; then + echo "Notarization of $APP_BUNDLE succeeded!" + break + elif [ "$STATUS" = "in progress" ]; then + echo "Notarization in progress..." + sleep 20 + else + echo "Notarization of $APP_BUNDLE failed:" + echo "$RESULT" + exit 1 + fi +done + +# Staple the notary ticket +xcrun stapler staple "$APP_BUNDLE" From ebe162dda2b58b69a16c1ef44e2e24964a84cc61 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Fri, 15 May 2020 16:30:45 +0200 Subject: [PATCH 068/101] [docs] updated doc dependencies --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index e61f40a41..d5b5d4805 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -2,10 +2,10 @@ atomicwrites bugsnag click dropbox +fasteners importlib_metadata keyring keyrings.alt -lockfile packaging pathspec Pyro5 From d2519c959e6fb5c7c5f0cb6fdd448eed3f2d3938 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Fri, 15 May 2020 16:30:54 +0200 Subject: [PATCH 069/101] updated order of dependencies --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 68bb0e7ac..fc5aa017a 100644 --- a/setup.py +++ b/setup.py @@ -14,11 +14,11 @@ 'bugsnag>=3.4.0', 'click>=7.1.1', 'dropbox>=10.0.0', + 'fasteners', 'importlib_metadata;python_version<"3.8"', 'jeepney;sys_platform=="linux"', 'keyring>=19.0.0', 'keyrings.alt>=3.1.0', - 'fasteners', 'packaging', 'pathspec>=0.5.8', 'Pyro5>=5.7', From b7976d79c312a893cb70f45e0a3831777c7df06a Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Fri, 15 May 2020 17:03:48 +0200 Subject: [PATCH 070/101] updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dfb773ed..74d633205 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ path. - Fixes a race condition when two processes try to start a sync daemon at the same time. - Fixes an issue in the macOS GUI where updating the displayed sync issues could fail. +- Fixes truncated text in the macOS setup dialog. #### Dependencies: From 936f7a4c94c6f366b7accaa5b8596ba241f2f6b1 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Fri, 15 May 2020 23:33:06 +0200 Subject: [PATCH 071/101] [autostart] create ~/Library/LaunchAgents if it does not exist --- maestral/utils/autostart.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/maestral/utils/autostart.py b/maestral/utils/autostart.py index 7b48d66fe..84b07c3fe 100644 --- a/maestral/utils/autostart.py +++ b/maestral/utils/autostart.py @@ -219,8 +219,9 @@ def __init__(self, config_name, gui): with open(osp.join(_resources, 'com.samschott.maestral.plist')) as f: plist_template = f.read() - - self.destination = osp.join(get_home_dir(), 'Library', 'LaunchAgents', filename) + + self.path = osp.join(get_home_dir(), 'Library', 'LaunchAgents') + self.destination = osp.join(self.path, filename) arguments = [f'\t\t{arg}' for arg in self.start_cmd] @@ -230,6 +231,8 @@ def __init__(self, config_name, gui): ) def _enable(self): + os.makedirs(self.path, exist_ok=True) + with open(self.destination, 'w+') as f: f.write(self.contents) From bee914167a0718f0632f6799ed6b8e44c2903f27 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Sun, 17 May 2020 20:00:38 +0200 Subject: [PATCH 072/101] updated changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74d633205..1dc2ea201 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,11 @@ - Fixes a race condition when two processes try to start a sync daemon at the same time. - Fixes an issue in the macOS GUI where updating the displayed sync issues could fail. - Fixes truncated text in the macOS setup dialog. +- Fixes an issue on fresh macOS installs where creating autostart entries could fai if + /Library/LaunchAgents does not yet exist. +- Fixes an issue in the macOS app bundle where installing the command line could tool + would fail if /usr/local/bin is owned by root (as is default). Now, the user is asked + for permission instead. #### Dependencies: From 2a5cc00350de115731466d17678060c39e13d84b Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Mon, 18 May 2020 11:53:31 +0200 Subject: [PATCH 073/101] [package] use create-dmg for a fancy dmg layout --- package/build-macos-cocoa.sh | 41 +++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/package/build-macos-cocoa.sh b/package/build-macos-cocoa.sh index 2600a5433..f881260ad 100755 --- a/package/build-macos-cocoa.sh +++ b/package/build-macos-cocoa.sh @@ -3,6 +3,12 @@ SPEC_FILE=maestral_macos_cocoa.spec BUILD_NO=$(grep -E -o "[0-9]*" bundle_version_macos.txt) +export MACOSX_DEPLOYMENT_TARGET=10.13 +export CFLAGS=-mmacosx-version-min=10.13 +export CPPFLAGS=-mmacosx-version-min=10.13 +export LDFLAGS=-mmacosx-version-min=10.13 +export LINKFLAGS=-mmacosx-version-min=10.13 + echo "**** INSTALLING DEPENDENCIES ***************************" git clone https://github.com/pyinstaller/pyinstaller.git build/pyinstaller @@ -10,28 +16,23 @@ cd build/pyinstaller git checkout master git pull cd bootloader -export MACOSX_DEPLOYMENT_TARGET=10.13 -export CFLAGS=-mmacosx-version-min=10.13 -export CPPFLAGS=-mmacosx-version-min=10.13 -export LDFLAGS=-mmacosx-version-min=10.13 -export LINKFLAGS=-mmacosx-version-min=10.13 -python ./waf all +python3 ./waf all cd .. -pip install . +pip3 install . cd ../.. git clone https://github.com/samschott/maestral build/maestral cd build/maestral git checkout develop git pull -pip install . +pip3 install . cd ../.. git clone https://github.com/samschott/maestral-cocoa build/maestral-cocoa cd build/maestral-cocoa git checkout develop git pull -pip install . +pip3 install . cd ../.. echo "**** BUILD NUMBER $BUILD_NO ****************************" @@ -51,16 +52,18 @@ macos-notarize-app.sh dist/Maestral.app echo "**** CREATING DMG **************************************" -test -f dist/dmg-folder && rm -Rf dist/dmg-folder -mkdir dist/dmg-folder -cd dist/dmg-folder -ln -s /Applications -cd .. -cd .. -cp -R dist/Maestral.app dist/dmg-folder/ -hdiutil create -volname Maestral \ - -srcfolder dist/dmg-folder -ov -format UDBZ dist/Maestral.dmg -rm -Rf dist/dmg-folder +test -f dist/Maestral.dmg && rm dist/Maestral.dmg + +create-dmg \ + --volname "Maestral" \ + --window-size 300 150 \ + --icon-size 64 \ + --text-size 11 \ + --icon "Maestral.app" 75 75 \ + --hide-extension "Maestral.app" \ + --app-drop-link 225 75 \ + "dist/Maestral.dmg" \ + "dist/Maestral.app" codesign --verify --sign "Apple Development: sam.schott@outlook.com (FJNXBRUVWL)" dist/Maestral.dmg md5 -r dist/Maestral.dmg From a5c1abcc643d0c78fd355d7cb5f556d3a0df94ee Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Mon, 18 May 2020 15:50:58 +0200 Subject: [PATCH 074/101] [sync] renamed some classes --- maestral/sync.py | 22 +++++++++++----------- tests/test_sync.py | 10 +++++----- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/maestral/sync.py b/maestral/sync.py index 9666773df..e69a69441 100644 --- a/maestral/sync.py +++ b/maestral/sync.py @@ -120,7 +120,7 @@ class FSEventHandler(FileSystemEventHandler): :param Event syncing: Set when syncing is running. :param Event startup: Set when startup is running. - :param UpDownSync sync: UpDownSync instance. + :param SyncEngine sync: UpDownSync instance. :cvar int ignore_timeout: Timeout in seconds after which ignored paths will be discarded. @@ -251,13 +251,13 @@ def on_any_event(self, event): self.local_file_event_queue.put(event) -class MaestralStateWrapper(abc.MutableSet): +class PersistentStateMutableSet(abc.MutableSet): """ A wrapper for a list of strings in the saved state that implements a MutableSet interface. All strings are stored as lower-case, reflecting Dropbox's case-insensitive file system. - :param str config_name: Name of config. + :param str config_name: Name of config (determines name of state file). :param str section: Section name in state file. :param str option: Option name in state file. """ @@ -359,7 +359,7 @@ def wrapper(self, *args, **kwargs): return decorator -class UpDownSync: +class SyncEngine: """ Class that contains methods to sync local file events with Dropbox and vice versa. @@ -452,10 +452,10 @@ def __init__(self, client): self._state = MaestralState(self.config_name) self._notifier = MaestralDesktopNotifier.for_config(self.config_name) - self.download_errors = MaestralStateWrapper( + self.download_errors = PersistentStateMutableSet( self.config_name, section='sync', option='download_errors' ) - self.pending_downloads = MaestralStateWrapper( + self.pending_downloads = PersistentStateMutableSet( self.config_name, section='sync', option='pending_downloads' ) @@ -2623,7 +2623,7 @@ def download_worker(sync, syncing, running, connected): """ Worker to sync changes of remote Dropbox with local folder. - :param UpDownSync sync: Instance of :class:`UpDownSync`. + :param SyncEngine sync: Instance of :class:`SyncEngine`. :param Event syncing: Event that indicates if workers are running or paused. :param Event running: Event to shutdown local file event handler and worker threads. :param Event connected: Event that indicates if we can connect to Dropbox. @@ -2669,7 +2669,7 @@ def download_worker_added_item(sync, syncing, running, connected): """ Worker to download items which have been newly included in sync. - :param UpDownSync sync: Instance of :class:`UpDownSync`. + :param SyncEngine sync: Instance of :class:`SyncEngine`. :param Event syncing: Event that indicates if workers are running or paused. :param Event running: Event to shutdown local file event handler and worker threads. :param Event connected: Event that indicates if we can connect to Dropbox. @@ -2707,7 +2707,7 @@ def upload_worker(sync, syncing, running, connected): """ Worker to sync local changes to remote Dropbox. - :param UpDownSync sync: Instance of :class:`UpDownSync`. + :param SyncEngine sync: Instance of :class:`SyncEngine`. :param Event syncing: Event that indicates if workers are running or paused. :param Event running: Event to shutdown local file event handler and worker threads. :param Event connected: Event that indicates if we can connect to Dropbox. @@ -2748,7 +2748,7 @@ def startup_worker(sync, syncing, running, connected, startup, paused_by_user): """ Worker to sync local changes to remote Dropbox. - :param UpDownSync sync: Instance of :class:`UpDownSync`. + :param SyncEngine sync: Instance of :class:`SyncEngine`. :param Event syncing: Event that indicates if workers are running or paused. :param Event running: Event to shutdown local file event handler and worker threads. :param Event connected: Event that indicates if we can connect to Dropbox. @@ -2845,7 +2845,7 @@ def __init__(self, client): self.client = client self.config_name = self.client.config_name - self.sync = UpDownSync(self.client) + self.sync = SyncEngine(self.client) self.connected = Event() self.syncing = Event() diff --git a/tests/test_sync.py b/tests/test_sync.py index d35b7c449..512543e2f 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -20,7 +20,7 @@ from maestral.sync import delete, move from maestral.sync import is_child from maestral.sync import get_local_hash, DirectorySnapshot -from maestral.sync import UpDownSync, Observer, FSEventHandler +from maestral.sync import SyncEngine, Observer, FSEventHandler from maestral.errors import NotFoundError, FolderConflictError from maestral.main import Maestral from maestral.main import get_log_path @@ -30,7 +30,7 @@ from unittest import TestCase -class DummyUpDownSync(UpDownSync): +class DummySyncEngine(SyncEngine): def __init__(self, dropbox_path=''): self._dropbox_path = dropbox_path @@ -50,7 +50,7 @@ def path(i): class TestCleanLocalEvents(TestCase): def setUp(self): - self.sync = DummyUpDownSync() + self.sync = DummySyncEngine() def test_single_file_events(self): @@ -286,7 +286,7 @@ def test_performance(self): class TestIgnoreLocalEvents(TestCase): def setUp(self): - self.sync = DummyUpDownSync() + self.sync = DummySyncEngine() self.dummy_dir = Path(os.getcwd()).parent / 'dropbox_dir' delete(self.dummy_dir) @@ -296,7 +296,7 @@ def setUp(self): startup = Event() syncing.set() - self.sync = DummyUpDownSync(self.dummy_dir) + self.sync = DummySyncEngine(self.dummy_dir) self.fs_event_handler = FSEventHandler(syncing, startup, self.sync) self.observer = Observer() From 8d4eabe79c3513a34529aafe7ad47300e426cb3e Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Mon, 18 May 2020 15:50:58 +0200 Subject: [PATCH 075/101] some refactoring --- maestral/client.py | 2 +- maestral/main.py | 10 +++++----- maestral/sync.py | 30 +++++++++++++++--------------- tests/test_sync.py | 10 +++++----- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/maestral/client.py b/maestral/client.py index 9b3ba3e90..ed1e14622 100644 --- a/maestral/client.py +++ b/maestral/client.py @@ -118,7 +118,7 @@ def wrapper(*args, **kwargs): return decorator -class MaestralApiClient: +class DropboxClient: """Client for the Dropbox SDK. This client defines basic methods to wrap Dropbox Python SDK calls, such as creating, diff --git a/maestral/main.py b/maestral/main.py index cec34acb9..781adf38e 100644 --- a/maestral/main.py +++ b/maestral/main.py @@ -37,8 +37,8 @@ # local imports from maestral import __version__ from maestral.oauth import OAuth2Session -from maestral.client import MaestralApiClient, to_maestral_error -from maestral.sync import MaestralMonitor +from maestral.client import DropboxClient, to_maestral_error +from maestral.sync import SyncMonitor from maestral.errors import ( MaestralApiError, NotLinkedError, NoDropboxDirError, NotFoundError, PathError @@ -241,8 +241,8 @@ def __init__(self, config_name='maestral', log_to_stdout=False): self._setup_logging() self._auth = OAuth2Session(config_name) - self.client = MaestralApiClient(config_name=self.config_name, access_token='none') - self.monitor = MaestralMonitor(self.client) + self.client = DropboxClient(config_name=self.config_name, access_token='none') + self.monitor = SyncMonitor(self.client) self.sync = self.monitor.sync # periodically check for updates and refresh account info @@ -821,7 +821,7 @@ def get_profile_pic(self): def list_folder(self, dbx_path, **kwargs): """ List all items inside the folder given by ``dbx_path``. Keyword arguments are - passed on the the Dropbox API call :meth:`client.MaestralApiClient.list_folder`. + passed on the the Dropbox API call :meth:`client.DropboxClient.list_folder`. :param dbx_path: Path to folder on Dropbox. :returns: List of Dropbox item metadata as dicts or ``False`` if listing failed diff --git a/maestral/sync.py b/maestral/sync.py index 9666773df..7eaba6d29 100644 --- a/maestral/sync.py +++ b/maestral/sync.py @@ -120,7 +120,7 @@ class FSEventHandler(FileSystemEventHandler): :param Event syncing: Set when syncing is running. :param Event startup: Set when startup is running. - :param UpDownSync sync: UpDownSync instance. + :param SyncEngine sync: UpDownSync instance. :cvar int ignore_timeout: Timeout in seconds after which ignored paths will be discarded. @@ -251,13 +251,13 @@ def on_any_event(self, event): self.local_file_event_queue.put(event) -class MaestralStateWrapper(abc.MutableSet): +class PersistentStateMutableSet(abc.MutableSet): """ A wrapper for a list of strings in the saved state that implements a MutableSet interface. All strings are stored as lower-case, reflecting Dropbox's case-insensitive file system. - :param str config_name: Name of config. + :param str config_name: Name of config (determines name of state file). :param str section: Section name in state file. :param str option: Option name in state file. """ @@ -359,7 +359,7 @@ def wrapper(self, *args, **kwargs): return decorator -class UpDownSync: +class SyncEngine: """ Class that contains methods to sync local file events with Dropbox and vice versa. @@ -431,7 +431,7 @@ class UpDownSync: finally confirm the successful upload and check if Dropbox has renamed the item to a conflicting copy. In the latter case, we apply those changes locally. - :param MaestralApiClient client: Dropbox API client instance. + :param DropboxClient client: Dropbox API client instance. """ @@ -452,10 +452,10 @@ def __init__(self, client): self._state = MaestralState(self.config_name) self._notifier = MaestralDesktopNotifier.for_config(self.config_name) - self.download_errors = MaestralStateWrapper( + self.download_errors = PersistentStateMutableSet( self.config_name, section='sync', option='download_errors' ) - self.pending_downloads = MaestralStateWrapper( + self.pending_downloads = PersistentStateMutableSet( self.config_name, section='sync', option='pending_downloads' ) @@ -2595,7 +2595,7 @@ def helper(mm): and syncing has not been paused by the user. 3) Triggers weekly reindexing. - :param MaestralMonitor mm: MaestralMonitor instance. + :param SyncMonitor mm: MaestralMonitor instance. """ while mm.running.is_set(): @@ -2623,7 +2623,7 @@ def download_worker(sync, syncing, running, connected): """ Worker to sync changes of remote Dropbox with local folder. - :param UpDownSync sync: Instance of :class:`UpDownSync`. + :param SyncEngine sync: Instance of :class:`SyncEngine`. :param Event syncing: Event that indicates if workers are running or paused. :param Event running: Event to shutdown local file event handler and worker threads. :param Event connected: Event that indicates if we can connect to Dropbox. @@ -2669,7 +2669,7 @@ def download_worker_added_item(sync, syncing, running, connected): """ Worker to download items which have been newly included in sync. - :param UpDownSync sync: Instance of :class:`UpDownSync`. + :param SyncEngine sync: Instance of :class:`SyncEngine`. :param Event syncing: Event that indicates if workers are running or paused. :param Event running: Event to shutdown local file event handler and worker threads. :param Event connected: Event that indicates if we can connect to Dropbox. @@ -2707,7 +2707,7 @@ def upload_worker(sync, syncing, running, connected): """ Worker to sync local changes to remote Dropbox. - :param UpDownSync sync: Instance of :class:`UpDownSync`. + :param SyncEngine sync: Instance of :class:`SyncEngine`. :param Event syncing: Event that indicates if workers are running or paused. :param Event running: Event to shutdown local file event handler and worker threads. :param Event connected: Event that indicates if we can connect to Dropbox. @@ -2748,7 +2748,7 @@ def startup_worker(sync, syncing, running, connected, startup, paused_by_user): """ Worker to sync local changes to remote Dropbox. - :param UpDownSync sync: Instance of :class:`UpDownSync`. + :param SyncEngine sync: Instance of :class:`SyncEngine`. :param Event syncing: Event that indicates if workers are running or paused. :param Event running: Event to shutdown local file event handler and worker threads. :param Event connected: Event that indicates if we can connect to Dropbox. @@ -2827,7 +2827,7 @@ def startup_worker(sync, syncing, running, connected, startup, paused_by_user): # Main Monitor class to start, stop and coordinate threads # ======================================================================================== -class MaestralMonitor: +class SyncMonitor: """ Class to sync changes between Dropbox and a local folder. It creates five threads: `observer` to retrieve local file system events, `startup_thread` to carry out any @@ -2835,7 +2835,7 @@ class MaestralMonitor: Dropbox, `download_thread` to query for and download remote changes, and `helper_thread` which periodically checks the connection to Dropbox servers. - :param MaestralApiClient client: The Dropbox API client, a wrapper around the Dropbox + :param DropboxClient client: The Dropbox API client, a wrapper around the Dropbox Python SDK. """ @@ -2845,7 +2845,7 @@ def __init__(self, client): self.client = client self.config_name = self.client.config_name - self.sync = UpDownSync(self.client) + self.sync = SyncEngine(self.client) self.connected = Event() self.syncing = Event() diff --git a/tests/test_sync.py b/tests/test_sync.py index d35b7c449..512543e2f 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -20,7 +20,7 @@ from maestral.sync import delete, move from maestral.sync import is_child from maestral.sync import get_local_hash, DirectorySnapshot -from maestral.sync import UpDownSync, Observer, FSEventHandler +from maestral.sync import SyncEngine, Observer, FSEventHandler from maestral.errors import NotFoundError, FolderConflictError from maestral.main import Maestral from maestral.main import get_log_path @@ -30,7 +30,7 @@ from unittest import TestCase -class DummyUpDownSync(UpDownSync): +class DummySyncEngine(SyncEngine): def __init__(self, dropbox_path=''): self._dropbox_path = dropbox_path @@ -50,7 +50,7 @@ def path(i): class TestCleanLocalEvents(TestCase): def setUp(self): - self.sync = DummyUpDownSync() + self.sync = DummySyncEngine() def test_single_file_events(self): @@ -286,7 +286,7 @@ def test_performance(self): class TestIgnoreLocalEvents(TestCase): def setUp(self): - self.sync = DummyUpDownSync() + self.sync = DummySyncEngine() self.dummy_dir = Path(os.getcwd()).parent / 'dropbox_dir' delete(self.dummy_dir) @@ -296,7 +296,7 @@ def setUp(self): startup = Event() syncing.set() - self.sync = DummyUpDownSync(self.dummy_dir) + self.sync = DummySyncEngine(self.dummy_dir) self.fs_event_handler = FSEventHandler(syncing, startup, self.sync) self.observer = Observer() From 7a34e102e46654a993bdde1cb4fa8fe4acfd0c06 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Mon, 18 May 2020 16:11:07 +0200 Subject: [PATCH 076/101] [sync] clean cache dir with sync_lock --- maestral/sync.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/maestral/sync.py b/maestral/sync.py index 7eaba6d29..785dcb1bd 100644 --- a/maestral/sync.py +++ b/maestral/sync.py @@ -880,11 +880,14 @@ def _ensure_cache_dir_present(self): f'{self.file_cache_path}.') def clean_cache_dir(self): - err = delete(self._file_cache_path) - if err and not isinstance(err, FileNotFoundError): - raise CacheDirError(f'Cannot create cache directory (errno {err.errno})', - 'Please check if you have write permissions for ' - f'{self._file_cache_path}.') + """Removes all items in the cache directory.""" + + with self.sync_lock: + err = delete(self._file_cache_path) + if err and not isinstance(err, FileNotFoundError): + raise CacheDirError(f'Cannot create cache directory (errno {err.errno})', + 'Please check if you have write permissions for ' + f'{self._file_cache_path}.') def _new_tmp_file(self): self._ensure_cache_dir_present() From a8a0fa43e115bb1635b01d61833d92df0d572054 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Mon, 18 May 2020 16:57:02 +0200 Subject: [PATCH 077/101] [package] remove macos-qt build scripts --- package/build-macos-qt.sh | 72 ------------ .../{build-macos-cocoa.sh => build-macos.sh} | 5 +- ...l_macos_cocoa.spec => maestral_macos.spec} | 0 package/maestral_macos_qt.spec | 110 ------------------ package/post_build_macos_qt.py | 34 ------ 5 files changed, 2 insertions(+), 219 deletions(-) delete mode 100755 package/build-macos-qt.sh rename package/{build-macos-cocoa.sh => build-macos.sh} (94%) rename package/{maestral_macos_cocoa.spec => maestral_macos.spec} (100%) delete mode 100644 package/maestral_macos_qt.spec delete mode 100644 package/post_build_macos_qt.py diff --git a/package/build-macos-qt.sh b/package/build-macos-qt.sh deleted file mode 100755 index d2d228a42..000000000 --- a/package/build-macos-qt.sh +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env bash - -SPEC_FILE=maestral_macos_qt.spec -BUILD_NO=$(grep -E -o "[0-9]*" bundle_version_macos.txt) - -echo "**** INSTALLING DEPENDENCIES ***************************" - -git clone https://github.com/pyinstaller/pyinstaller.git build/pyinstaller -cd build/pyinstaller -git checkout master -git pull -cd bootloader -export MACOSX_DEPLOYMENT_TARGET=10.13 -export CFLAGS=-mmacosx-version-min=10.13 -export CPPFLAGS=-mmacosx-version-min=10.13 -export LDFLAGS=-mmacosx-version-min=10.13 -export LINKFLAGS=-mmacosx-version-min=10.13 -python ./waf all -cd .. -pip install . -cd ../.. - -git clone https://github.com/samschott/maestral build/maestral -cd build/maestral -git checkout develop -git pull -pip install . -cd ../.. - -git clone https://github.com/samschott/maestral-cocoa build/maestral-cocoa -cd build/maestral-cocoa -git checkout develop -git pull -pip install . -cd ../.. - -echo "**** BUILD NUMBER $BUILD_NO ****************************" - -python3 -m PyInstaller -y --clean -w $SPEC_FILE - -echo "**** COPY CLI ENTRY POINT ******************************" - -cp bin/maestral_cli dist/Maestral.app/Contents/MacOS/maestral_cli - -echo "**** RUNNING POST-BUILD SCRIPTS ************************" - -python3 post_build_macos_qt.py - -echo "**** SIGN AND NOTARIZE *********************************" - -codesign -s "Apple Development: sam.schott@outlook.com (FJNXBRUVWL)" \ - --entitlements entitlements.plist --deep -o runtime dist/Maestral.app - -macos-notarize-app.sh dist/Maestral.app - -echo "**** CREATING DMG **************************************" - -test -f dist/dmg-folder && rm -Rf dist/dmg-folder -mkdir dist/dmg-folder -cd dist/dmg-folder -ln -s /Applications -cd .. -cd .. -cp -R dist/Maestral.app dist/dmg-folder/ -hdiutil create -volname Maestral \ - -srcfolder dist/dmg-folder -ov -format UDBZ dist/Maestral.dmg -rm -Rf dist/dmg-folder - -codesign --verify --sign "Apple Development: sam.schott@outlook.com (FJNXBRUVWL)" dist/Maestral.dmg -md5 -r dist/Maestral.dmg - -echo "**** DONE **********************************************" diff --git a/package/build-macos-cocoa.sh b/package/build-macos.sh similarity index 94% rename from package/build-macos-cocoa.sh rename to package/build-macos.sh index f881260ad..6e8cb352d 100755 --- a/package/build-macos-cocoa.sh +++ b/package/build-macos.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -SPEC_FILE=maestral_macos_cocoa.spec +SPEC_FILE=maestral_macos.spec BUILD_NO=$(grep -E -o "[0-9]*" bundle_version_macos.txt) export MACOSX_DEPLOYMENT_TARGET=10.13 @@ -59,8 +59,7 @@ create-dmg \ --window-size 300 150 \ --icon-size 64 \ --text-size 11 \ - --icon "Maestral.app" 75 75 \ - --hide-extension "Maestral.app" \ + --icon "dist/Maestral.app" 75 75 \ --app-drop-link 225 75 \ "dist/Maestral.dmg" \ "dist/Maestral.app" diff --git a/package/maestral_macos_cocoa.spec b/package/maestral_macos.spec similarity index 100% rename from package/maestral_macos_cocoa.spec rename to package/maestral_macos.spec diff --git a/package/maestral_macos_qt.spec b/package/maestral_macos_qt.spec deleted file mode 100644 index f5f9191e4..000000000 --- a/package/maestral_macos_qt.spec +++ /dev/null @@ -1,110 +0,0 @@ -# -*- mode: python ; coding: utf-8 -*- - -block_cipher = None - - -import time -import pkg_resources as pkgr -from maestral import __version__, __author__ - - -try: - with open('bundle_version_macos.txt', 'r') as f: - bundle_version = str(int(f.read()) + 1) -except FileNotFoundError: - bundle_version = '1' - -with open('bundle_version_macos.txt', 'w') as f: - f.write(bundle_version) - - -def Entrypoint(dist, group, name, **kwargs): - - packages = [] - - kwargs.setdefault('pathex', []) - # get the entry point - ep = pkgr.get_entry_info(dist, group, name) - # insert path of the egg at the verify front of the search path - kwargs['pathex'] = [ep.dist.location] + kwargs['pathex'] - # script name must not be a valid module name to avoid name clashes on import - script_path = os.path.join(workpath, name + '-script.py') - print("creating script for entry point", dist, group, name) - with open(script_path, 'w') as fh: - print("import", ep.module_name, file=fh) - print("%s.%s()" % (ep.module_name, '.'.join(ep.attrs)), file=fh) - for package in packages: - print("import", package, file=fh) - - return Analysis( - [script_path] + kwargs.get('scripts', []), - **kwargs - ) - - -a = Entrypoint( - 'maestral_qt', 'console_scripts', 'maestral_qt', - binaries=None, - datas= [ - (pkgr.resource_filename('maestral_qt', 'resources/tray-icons-svg/*.svg'), 'maestral_qt/resources/tray-icons-svg'), - (pkgr.resource_filename('maestral_qt', 'resources/maestral.png'), 'maestral_qt/resources'), - (pkgr.resource_filename('maestral_qt', 'resources/faceholder.png'), 'maestral_qt/resources'), - (pkgr.resource_filename('maestral_qt', 'resources/*.ui'), 'maestral_qt/resources'), - (pkgr.resource_filename('maestral', 'resources/*.plist'), 'maestral/resources'), - (pkgr.resource_filename('maestral', 'resources/*.desktop'), 'maestral/resources'), - (pkgr.resource_filename('maestral', 'resources/*.png'), 'maestral/resources'), - (pkgr.resource_filename('maestral', 'resources/*.service'), 'maestral/resources'), - ], - hiddenimports=['pkg_resources.py2_warn'], - hookspath=['hooks'], - runtime_hooks=[], - excludes=['_tkinter'], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher, - noarchive=False -) - -pyz = PYZ( - a.pure, a.zipped_data, - cipher=block_cipher -) - -exe = EXE( - pyz, - a.scripts, - [], - exclude_binaries=True, - name='main', - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - console=False -) - -coll = COLLECT( - exe, - a.binaries, - a.zipfiles, - a.datas, - strip=False, - upx=True, - upx_exclude=[], - name='main' -) - -app = BUNDLE( - coll, - name='Maestral.app', - icon=pkgr.resource_filename('maestral_qt', 'resources/maestral.icns'), - bundle_identifier='com.samschott.maestral', - info_plist={ - 'NSHighResolutionCapable': 'True', - 'LSUIElement': '1', - 'CFBundleVersion': bundle_version, - 'CFBundleShortVersionString': __version__, - 'NSHumanReadableCopyright': 'Copyright © {} {}. All rights reserved.'.format(time.strftime('%Y'), __author__), - 'LSMinimumSystemVersion': '10.13.0', - }, -) diff --git a/package/post_build_macos_qt.py b/package/post_build_macos_qt.py deleted file mode 100644 index e0a67d50f..000000000 --- a/package/post_build_macos_qt.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/local/bin/python3 - -import shutil -from pathlib import Path - - -bundle_path = Path(__file__).parent / Path('dist/Maestral.app/Contents/macOS') - -items_to_remove = [ - 'QtQml', - 'QtQuick', - 'QtNetwork', - 'QtWebSockets', - 'QtQmlModels', - 'PyQt5/Qt/translations', - 'PyQt5/Qt/plugins/imageformats/libqgif.dylib', - 'PyQt5/Qt/plugins/imageformats/libqtiff.dylib', - 'PyQt5/Qt/plugins/imageformats/libqwebp.dylib', - 'PyQt5/Qt/plugins/platforms/libqwebgl.dylib', - 'PyQt5/Qt/plugins/platforms/libqoffscreen.dylib', - 'PyQt5/Qt/plugins/platforms/libqminimal.dylib', - 'libsqlite3.0.dylib', -] - -print("Removing unneeded Qt modules...") - -for path in items_to_remove: - lib_path = bundle_path / path - if lib_path.is_file(): - lib_path.unlink() - elif lib_path.is_dir(): - shutil.rmtree(lib_path) - -print("Done.") From 201cc441baf339605db23a1fcb94ac2abbb5017b Mon Sep 17 00:00:00 2001 From: SamSchott Date: Mon, 18 May 2020 22:58:19 +0200 Subject: [PATCH 078/101] [sync] catch all OSErrors when loading mignore... ... and print debug message --- maestral/sync.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/maestral/sync.py b/maestral/sync.py index 785dcb1bd..9028ca859 100644 --- a/maestral/sync.py +++ b/maestral/sync.py @@ -843,7 +843,8 @@ def _load_mignore_rules_form_file(self): try: with open(self.mignore_path) as f: spec = f.read() - except FileNotFoundError: + except OSError as exc: + logger.debug(f'Could not load mignore rules from {self.mignore_path}: {exc}') spec = '' return pathspec.PathSpec.from_lines('gitwildmatch', spec.splitlines()) From 681bbbd6d8772d89e8c74c5ede7a798cf9b60ba5 Mon Sep 17 00:00:00 2001 From: SamSchott Date: Mon, 18 May 2020 22:56:19 +0200 Subject: [PATCH 079/101] [sync] updated doc strings --- maestral/sync.py | 54 +++++++++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/maestral/sync.py b/maestral/sync.py index 9028ca859..05f81f928 100644 --- a/maestral/sync.py +++ b/maestral/sync.py @@ -114,13 +114,13 @@ def __exit__(self, err_type, err_value, err_traceback): class FSEventHandler(FileSystemEventHandler): """ - Handles captured file events and adds them to :class:`UpDownSync`'s file event queue + Handles captured file events and adds them to :class:`SyncEngine`'s file event queue to be uploaded by :meth:`upload_worker`. This acts as a translation layer between - :class:`watchdog.Observer` and :class:`UpDownSync`. + :class:`watchdog.Observer` and :class:`SyncEngine`. :param Event syncing: Set when syncing is running. :param Event startup: Set when startup is running. - :param SyncEngine sync: UpDownSync instance. + :param SyncEngine sync: SyncEngine instance. :cvar int ignore_timeout: Timeout in seconds after which ignored paths will be discarded. @@ -155,7 +155,7 @@ def ignore(self, *events, recursive=True): :param iterable events: Local events to ignore. :param bool recursive: If ``True``, all child events of a directory event will be - ignored as well. + ignored as well. This parameter will be ignored for file events. """ with self._ignored_events_mutex: @@ -312,7 +312,7 @@ def __repr__(self): def catch_sync_issues(download=False): """ Returns a decorator that catches all SyncErrors and logs them. - Should only be used to decorate methods of :class:`UpDownSync`. + Should only be used to decorate methods of :class:`SyncEngine`. :param bool download: If ``True``, sync issues will be added to the `download_errors` list to retry later. @@ -517,7 +517,9 @@ def rev_file_path(self): @property def file_cache_path(self): - """Path to sync index with rev numbers (read only).""" + """Path to cache folder for temporary files (read only). The cache folder + '.maestral.cache' is located inside the local Dropbox folder to prevent + file transfer between different partitions or drives during sync.""" return self._file_cache_path @property @@ -562,8 +564,8 @@ def clean_excluded_items_list(folder_list): @property def max_cpu_percent(self): """Maximum CPU usage for parallel downloads or uploads in percent of the total - available CPU time. Individual workers in a thread pool will pause until the - usage drops below this value. Tasks in the main thread such as indexing file + available CPU time per core. Individual workers in a thread pool will pause until + the usage drops below this value. Tasks in the main thread such as indexing file changes may still use more CPU time. Setting this to 100% means that no limits on CPU usage will be applied.""" return self._max_cpu_percent @@ -767,8 +769,8 @@ def _handle_rev_write_exceptions(self, raise_exception=False): def _load_rev_dict_from_file(self, raise_exception=False): """ Loads Maestral's rev index from ``rev_file_path``. Every line contains the rev - number for a single path, saved in a json format. Only the last entry for every - path is kept since it is the newest. + number for a single path, saved in a json format. Only the last entry for each + path is kept, overriding possible (older) previous entries. :param bool raise_exception: If ``True``, raises an exception when loading fails. If ``False``, an error message is logged instead. @@ -810,13 +812,16 @@ def _save_rev_dict_to_file(self, raise_exception=False): def _append_rev_to_file(self, path, rev, raise_exception=False): """ - Appends a new line with '{path}: {rev}\n' to the rev file. This is quicker than + Appends a new line with a rev entry to the rev file. This is quicker than saving the entire rev index. When loading the rev file, older entries will be - overwritten with newer ones and all entries with rev == None will be discarded. + overwritten with newer ones and all entries with ``rev == None`` will be + discarded. :param str path: Path for rev. :param str rev: Dropbox rev or ``None``. - :raises: RevFileError if ``raise_exception`` is ``True``. + :param bool raise_exception: If ``True``, raises an exception when saving fails. + If ``False``, an error message is logged instead. + :raises: :class:`errors.RevFileError` """ with self._rev_lock: @@ -833,12 +838,14 @@ def mignore_path(self): @property def mignore_rules(self): - """List of mignore rules following git wildmatch syntax.""" + """List of mignore rules following git wildmatch syntax (read only).""" if self._get_ctime(self.mignore_path) != self._mignore_ctime_loaded: self._mignore_rules = self._load_mignore_rules_form_file() return self._mignore_rules def _load_mignore_rules_form_file(self): + """Loads rules from mignore file. No rules are loaded if the file does + not exist or cannot be read.""" self._mignore_ctime_loaded = self._get_ctime(self.mignore_path) try: with open(self.mignore_path) as f: @@ -924,7 +931,7 @@ def to_local_path(self, dbx_path): The ``path_display`` attribute returned by the Dropbox API only guarantees correct casing of the basename and not of the full path. This is because Dropbox itself is - not case sensitive and stores all paths in lowercase internally. To the extend + not case sensitive and stores all paths in lowercase internally. To the extent where parent directories of ``dbx_path`` exist on the local drive, their casing will be used. Otherwise, the casing from ``dbx_path`` is used. This aims to preserve the correct casing of file and folder names and prevents the creation of @@ -1544,8 +1551,8 @@ def _handle_case_conflict(self, event): def _handle_selective_sync_conflict(self, event): """ - Checks for other items in the same directory with same name but a different - case. Only needed for case sensitive file systems. + Checks for items in the local directory with same path as an item which is + excluded by selective sync. Renames items if necessary. :param FileSystemEvent event: Created or moved event. :returns: ``True`` or ``False``. @@ -2016,7 +2023,7 @@ def get_remote_item(self, dbx_path): performed by :meth:`_create_local_entry`. This method can be used to fetch individual items outside of the regular sync - cycle, for instance when including a new file or folder. + cycle, for instance when including a previously excluded file or folder. :param str dbx_path: Path relative to Dropbox folder. :returns: ``True`` on success, ``False`` otherwise. @@ -2077,11 +2084,10 @@ def apply_remote_changes(self, changes, save_cursor=True): has been successfully applied. Entries in the local index are created after successful completion. - :param changes: :class:`dropbox.files.ListFolderResult` instance or ``False`` if - requests failed. - :param bool save_cursor: If True, :attr:`last_cursor` will be updated from the - last applied changes. Take care to only save a cursors which represent the - state of the entire Dropbox + :param changes: :class:`dropbox.files.ListFolderResult` instance. + :param bool save_cursor: If ``True``, :attr:`last_cursor` will be updated from + the last applied changes. Take care to only save a cursors which represent + the state of the entire Dropbox. :returns: List of changes that were made to local files and bool indicating if all download syncs were successful. :rtype: (list, bool) @@ -2301,7 +2307,7 @@ def _check_download_conflict(self, md): def _get_ctime(self, local_path, ignore_excluded=True): """ Returns the ctime of a local item or -1.0 if there is nothing at the path. If - the item is a directory, return the largest ctime of itself and its children. + the item is a directory, return the largest ctime of it and its children. :param str local_path: Absolute path on local drive. :param bool ignore_excluded: If ``True``, the ctimes of children for which From 8e66a49cf21ebdebd5dc0cd9d4b5b12bc400a586 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Tue, 19 May 2020 10:20:26 +0200 Subject: [PATCH 080/101] [daemon] added option to not detach daemon process --- maestral/daemon.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/maestral/daemon.py b/maestral/daemon.py index 0a123ea9a..334fe21d8 100644 --- a/maestral/daemon.py +++ b/maestral/daemon.py @@ -406,7 +406,7 @@ def start_maestral_daemon_thread(config_name='maestral', log_to_stdout=False): return _wait_for_startup(config_name) -def start_maestral_daemon_process(config_name='maestral', log_to_stdout=False): +def start_maestral_daemon_process(config_name='maestral', log_to_stdout=False, detach=True): """ Starts the Maestral daemon in a new process by calling :func:`start_maestral_daemon`. Startup is race free: there will never be two daemons running for the same config. @@ -418,7 +418,7 @@ def start_maestral_daemon_process(config_name='maestral', log_to_stdout=False): supported through the console_script entry points of both `maestral` and `maestral_qt`. - Starting a detached daemon process is difficult from a standalone executable since + Starting a detached daemon process is difficult from a frozen executable since the typical double-fork magic may fail on macOS and we do not have access to a standalone Python interpreter to spawn a subprocess. Our approach mimics the "freeze support" implemented by the multiprocessing module but fully detaches the spawned @@ -426,6 +426,7 @@ def start_maestral_daemon_process(config_name='maestral', log_to_stdout=False): :param str config_name: The name of the Maestral configuration to use. :param bool log_to_stdout: If ``True``, write logs to stdout. Defaults to ``False``. + :param bool detach: If ``True``, the daemon process will be detached by double-forking. :returns: ``Start.Ok`` if successful, ``Start.AlreadyRunning`` if the daemon was already running or ``Start.Failed`` if startup failed. It is possible that Start.Ok is returned instead of Start.AlreadyRunning in case of a race. @@ -442,12 +443,12 @@ def start_maestral_daemon_process(config_name='maestral', log_to_stdout=False): if IS_FROZEN: - def target(): + def launcher(): subprocess.Popen([sys.executable, '--frozen-daemon', '-c', config_name]) else: - def target(): + def launcher(): # protect against injection cc = quote(config_name).strip("'") std_log = bool(log_to_stdout) @@ -457,11 +458,19 @@ def target(): subprocess.Popen([sys.executable, '-c', cmd]) - mp.Process( - target=target, - name='maestral-daemon-launcher', - daemon=True, - ).start() + if detach: + mp.Process( + target=launcher, + name='maestral-daemon-launcher', + daemon=True, + ).start() + else: + mp.Process( + target=start_maestral_daemon, + args=(config_name, log_to_stdout), + name='maestral-daemon', + daemon=True, + ).start() return _wait_for_startup(config_name) From f8e0bcebb2017eeeb29060ff580addaf00c05724 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Tue, 19 May 2020 11:05:38 +0200 Subject: [PATCH 081/101] [daemon] use spawn instead of fork on macOS this may fix crashes on startup as in #137 --- maestral/daemon.py | 49 ++++++++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/maestral/daemon.py b/maestral/daemon.py index 334fe21d8..3ee38537e 100644 --- a/maestral/daemon.py +++ b/maestral/daemon.py @@ -17,6 +17,8 @@ import traceback import enum import subprocess +import multiprocessing as mp +from shlex import quote import threading import fcntl import struct @@ -30,7 +32,7 @@ # local imports from maestral.errors import SYNC_ERRORS, FATAL_ERRORS -from maestral.constants import IS_FROZEN +from maestral.constants import IS_FROZEN, IS_MACOS from maestral.utils.appdirs import get_runtime_path @@ -406,6 +408,20 @@ def start_maestral_daemon_thread(config_name='maestral', log_to_stdout=False): return _wait_for_startup(config_name) +def _launcher(config_name, log_to_stdout): + + if IS_FROZEN: + subprocess.Popen([sys.executable, '--frozen-daemon', '-c', config_name]) + else: + cc = quote(config_name).strip("'") # protect against injection + std_log = bool(log_to_stdout) + + cmd = (f'import maestral.daemon; ' + f'maestral.daemon.start_maestral_daemon("{cc}", {std_log})') + + subprocess.Popen([sys.executable, '-c', cmd]) + + def start_maestral_daemon_process(config_name='maestral', log_to_stdout=False, detach=True): """ Starts the Maestral daemon in a new process by calling :func:`start_maestral_daemon`. @@ -435,37 +451,18 @@ def start_maestral_daemon_process(config_name='maestral', log_to_stdout=False, d if is_running(config_name): return Start.AlreadyRunning - from shlex import quote - import multiprocessing as mp - - # use nested Popen and multiprocessing.Process to effectively create double fork - # see Unix 'double-fork magic' - - if IS_FROZEN: - - def launcher(): - subprocess.Popen([sys.executable, '--frozen-daemon', '-c', config_name]) - - else: - - def launcher(): - # protect against injection - cc = quote(config_name).strip("'") - std_log = bool(log_to_stdout) - - cmd = (f'import maestral.daemon; ' - f'maestral.daemon.start_maestral_daemon("{cc}", {std_log})') - - subprocess.Popen([sys.executable, '-c', cmd]) + ctx = mp.get_context('spawn' if IS_MACOS else 'fork') if detach: - mp.Process( - target=launcher, + ctx.Process( + target=_launcher, + args=(config_name, log_to_stdout), name='maestral-daemon-launcher', daemon=True, ).start() + else: - mp.Process( + ctx.Process( target=start_maestral_daemon, args=(config_name, log_to_stdout), name='maestral-daemon', From b83cbd81dca2d87fc58f02355b0f296f8d256883 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Tue, 19 May 2020 18:10:18 +0200 Subject: [PATCH 082/101] [package] fix create-dmg command --- package/build-macos.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/build-macos.sh b/package/build-macos.sh index 6e8cb352d..07edc9ba5 100755 --- a/package/build-macos.sh +++ b/package/build-macos.sh @@ -59,7 +59,7 @@ create-dmg \ --window-size 300 150 \ --icon-size 64 \ --text-size 11 \ - --icon "dist/Maestral.app" 75 75 \ + --icon "Maestral.app" 75 75 \ --app-drop-link 225 75 \ "dist/Maestral.dmg" \ "dist/Maestral.app" From 4ff88149cf30cb8ff041ea21ceddb6fd33e0fa34 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Tue, 19 May 2020 18:16:19 +0200 Subject: [PATCH 083/101] [package] updated signing and notarisation scripts --- package/build-macos.sh | 4 ++-- package/macos-notarize-app.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package/build-macos.sh b/package/build-macos.sh index 07edc9ba5..dcf1f4bc3 100755 --- a/package/build-macos.sh +++ b/package/build-macos.sh @@ -45,7 +45,7 @@ cp bin/maestral_cli dist/Maestral.app/Contents/MacOS/maestral_cli echo "**** SIGN AND NOTARIZE *********************************" -codesign -s "Apple Development: sam.schott@outlook.com (FJNXBRUVWL)" \ +codesign -s "Developer ID Application: Sam Schott" \ --entitlements entitlements.plist --deep -o runtime dist/Maestral.app macos-notarize-app.sh dist/Maestral.app @@ -64,7 +64,7 @@ create-dmg \ "dist/Maestral.dmg" \ "dist/Maestral.app" -codesign --verify --sign "Apple Development: sam.schott@outlook.com (FJNXBRUVWL)" dist/Maestral.dmg +codesign --verify --sign "Developer ID Application: Sam Schott" dist/Maestral.dmg md5 -r dist/Maestral.dmg echo "**** DONE **********************************************" diff --git a/package/macos-notarize-app.sh b/package/macos-notarize-app.sh index df90de703..eb6f2b695 100644 --- a/package/macos-notarize-app.sh +++ b/package/macos-notarize-app.sh @@ -23,7 +23,7 @@ ditto -c -k --rsrc --keepParent "$APP_BUNDLE" "${APP_BUNDLE}.zip" echo "Submitting $APP_BUNDLE for notarization..." RESULT=$(xcrun altool --notarize-app --type osx \ --file "${APP_BUNDLE}.zip" \ - --primary-bundle-id org.musicbrainz.Picard \ + --primary-bundle-id com.samschott.maestral \ --username $APPLE_ID_USER \ --password @env:APPLE_ID_PASSWORD \ -itc_provider MetaBrainzFoundationInc \ From 15d2eb4c2dc1f2d04dfaeea033e5dd4c18c9975d Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Tue, 19 May 2020 23:04:25 +0200 Subject: [PATCH 084/101] [daemon] added `freeze_support` function --- maestral/daemon.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/maestral/daemon.py b/maestral/daemon.py index 3ee38537e..8c74853a6 100644 --- a/maestral/daemon.py +++ b/maestral/daemon.py @@ -40,6 +40,22 @@ URI = 'PYRO:maestral.{0}@{1}' +def freeze_support(): + """Hook to support '--multiprocessing-fork' and '--frozen-daemon' flags.""" + import argparse + + mp.freeze_support() + + parser = argparse.ArgumentParser(add_help=False) + parser.add_argument('-c', '--config-name', default='maestral') + parser.add_argument('--frozen-daemon', action='store_true') + parsed_args, remaining = parser.parse_known_args() + + if parsed_args.frozen_daemon: + start_maestral_daemon(parsed_args.config_name) + sys.exit() + + class Exit(enum.Enum): """Enumeration of daemon exit results.""" Ok = 0 @@ -430,9 +446,9 @@ def start_maestral_daemon_process(config_name='maestral', log_to_stdout=False, d This function assumes that ``sys.executable`` points to the Python executable or a frozen executable. In case of a frozen executable, the executable must take the command line argument ``--frozen-daemon`` to start a daemon process which is *not - syncing*, .i.e., just run :meth:`start_maestral_daemon`. This is currently - supported through the console_script entry points of both `maestral` and - `maestral_qt`. + syncing*, .i.e., just run :meth:`start_maestral_daemon`. This is currently supported + through the console_script entry points of ``maestral``, ``maestral_qt`` and + ``maestral_cocoa`` by calling :func:`freeze_support`. Starting a detached daemon process is difficult from a frozen executable since the typical double-fork magic may fail on macOS and we do not have access to a From 931dc7826aca9e4596bbdbe998d10f6d1e184a11 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Tue, 19 May 2020 23:04:46 +0200 Subject: [PATCH 085/101] [cli] use the daemon's freeze_support --- maestral/cli.py | 25 ++----------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/maestral/cli.py b/maestral/cli.py index 5c22b1db3..dc839f6c6 100755 --- a/maestral/cli.py +++ b/maestral/cli.py @@ -24,6 +24,7 @@ import Pyro5.errors # local imports +from maestral.daemon import freeze_support from maestral.config import MaestralConfig, MaestralState, list_configs from maestral.utils.housekeeping import remove_configuration @@ -362,35 +363,13 @@ def _run_daemon(ctx, param, value): ) -hidden_config_option = click.option( - '-c', '--config-name', - default='maestral', - is_eager=True, - expose_value=False, - help='For internal use only.', - hidden=True, -) - - -frozen_daemon_option = click.option( - '--frozen-daemon', - is_flag=True, - default=False, - is_eager=True, - expose_value=False, - callback=_run_daemon, - hidden=True, - help='For internal use only.' -) - CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) @click.group(cls=SpecialHelpOrder, context_settings=CONTEXT_SETTINGS) -@frozen_daemon_option -@hidden_config_option def main(): """Maestral Dropbox Client for Linux and macOS.""" + freeze_support() check_for_updates() From 1b7e362d75aa4923ffdc159e9665bbf193236c63 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Wed, 20 May 2020 10:27:19 +0200 Subject: [PATCH 086/101] [daemon] only use `subprocess` when starting a detached daemon --- maestral/daemon.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/maestral/daemon.py b/maestral/daemon.py index 8c74853a6..7385957c5 100644 --- a/maestral/daemon.py +++ b/maestral/daemon.py @@ -427,7 +427,8 @@ def start_maestral_daemon_thread(config_name='maestral', log_to_stdout=False): def _launcher(config_name, log_to_stdout): if IS_FROZEN: - subprocess.Popen([sys.executable, '--frozen-daemon', '-c', config_name]) + subprocess.Popen([sys.executable, '--frozen-daemon', '-c', config_name], + start_new_session=True) else: cc = quote(config_name).strip("'") # protect against injection std_log = bool(log_to_stdout) @@ -435,7 +436,7 @@ def _launcher(config_name, log_to_stdout): cmd = (f'import maestral.daemon; ' f'maestral.daemon.start_maestral_daemon("{cc}", {std_log})') - subprocess.Popen([sys.executable, '-c', cmd]) + subprocess.Popen([sys.executable, '-c', cmd], start_new_session=True) def start_maestral_daemon_process(config_name='maestral', log_to_stdout=False, detach=True): @@ -470,12 +471,7 @@ def start_maestral_daemon_process(config_name='maestral', log_to_stdout=False, d ctx = mp.get_context('spawn' if IS_MACOS else 'fork') if detach: - ctx.Process( - target=_launcher, - args=(config_name, log_to_stdout), - name='maestral-daemon-launcher', - daemon=True, - ).start() + _launcher(config_name, log_to_stdout) else: ctx.Process( From 7417a98476034480857edeb5ba768e68189b9411 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Wed, 20 May 2020 10:40:48 +0200 Subject: [PATCH 087/101] [daemon] update doc strings --- maestral/daemon.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/maestral/daemon.py b/maestral/daemon.py index 7385957c5..26e2b0f54 100644 --- a/maestral/daemon.py +++ b/maestral/daemon.py @@ -41,7 +41,11 @@ def freeze_support(): - """Hook to support '--multiprocessing-fork' and '--frozen-daemon' flags.""" + """Freeze support for multiprocessing and daemon startup. This works by checking for + '--multiprocessing-fork' and '--frozen-daemon' command line arguments. Call this + function at the entry point of the executable, as soon as possible and ideally before + any heavy imports.""" + import argparse mp.freeze_support() @@ -446,16 +450,10 @@ def start_maestral_daemon_process(config_name='maestral', log_to_stdout=False, d This function assumes that ``sys.executable`` points to the Python executable or a frozen executable. In case of a frozen executable, the executable must take the - command line argument ``--frozen-daemon`` to start a daemon process which is *not - syncing*, .i.e., just run :meth:`start_maestral_daemon`. This is currently supported - through the console_script entry points of ``maestral``, ``maestral_qt`` and - ``maestral_cocoa`` by calling :func:`freeze_support`. - - Starting a detached daemon process is difficult from a frozen executable since - the typical double-fork magic may fail on macOS and we do not have access to a - standalone Python interpreter to spawn a subprocess. Our approach mimics the "freeze - support" implemented by the multiprocessing module but fully detaches the spawned - process. + command line argument '--frozen-daemon' to start a daemon process which is *not + syncing* by calling :func:`start_maestral_daemon`. This is currently supported through + :func:`freeze_support` which should be called from the main entry point, as soon as + possible after startup. :param str config_name: The name of the Maestral configuration to use. :param bool log_to_stdout: If ``True``, write logs to stdout. Defaults to ``False``. From ecf4e53c5f4cf8a065b4dcb95c72e55dc03a44b3 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Wed, 20 May 2020 10:41:03 +0200 Subject: [PATCH 088/101] [daemon] move multiprocessing to local imports --- maestral/daemon.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/maestral/daemon.py b/maestral/daemon.py index 26e2b0f54..41b906d9d 100644 --- a/maestral/daemon.py +++ b/maestral/daemon.py @@ -17,7 +17,6 @@ import traceback import enum import subprocess -import multiprocessing as mp from shlex import quote import threading import fcntl @@ -47,6 +46,7 @@ def freeze_support(): any heavy imports.""" import argparse + import multiprocessing as mp mp.freeze_support() @@ -466,12 +466,13 @@ def start_maestral_daemon_process(config_name='maestral', log_to_stdout=False, d if is_running(config_name): return Start.AlreadyRunning - ctx = mp.get_context('spawn' if IS_MACOS else 'fork') - if detach: _launcher(config_name, log_to_stdout) else: + import multiprocessing as mp + ctx = mp.get_context('spawn' if IS_MACOS else 'fork') + ctx.Process( target=start_maestral_daemon, args=(config_name, log_to_stdout), From 41df89aa8c817492628bf373de0b05d8b7768709 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Wed, 20 May 2020 11:03:23 +0200 Subject: [PATCH 089/101] [daemon] renamed _launcher to _subprocess_launcher --- maestral/daemon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/maestral/daemon.py b/maestral/daemon.py index 41b906d9d..c2eb3a187 100644 --- a/maestral/daemon.py +++ b/maestral/daemon.py @@ -428,7 +428,7 @@ def start_maestral_daemon_thread(config_name='maestral', log_to_stdout=False): return _wait_for_startup(config_name) -def _launcher(config_name, log_to_stdout): +def _subprocess_launcher(config_name, log_to_stdout): if IS_FROZEN: subprocess.Popen([sys.executable, '--frozen-daemon', '-c', config_name], @@ -467,7 +467,7 @@ def start_maestral_daemon_process(config_name='maestral', log_to_stdout=False, d return Start.AlreadyRunning if detach: - _launcher(config_name, log_to_stdout) + _subprocess_launcher(config_name, log_to_stdout) else: import multiprocessing as mp From ad75284a664016a0e77facc5b87b32ae43433228 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Wed, 20 May 2020 11:05:04 +0200 Subject: [PATCH 090/101] updated changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dc2ea201..37e9f94be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ - Show both the daemon and GUI version in the GUI settings window. - The command line tool bundled with the macOS app now provides proper help output. - Significantly reduced CPU usage of the GUI on macOS. -- The macOS app now uses a hardened runtime, in preparation for app notarization. +- The macOS app now uses a hardened runtime and is properly signed and notarized. #### Fixed: From 0765f19d7466ed4c49dc07747a498dc5bf1993a2 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Wed, 20 May 2020 11:08:30 +0200 Subject: [PATCH 091/101] [package] fixed notarisation script call --- package/build-macos.sh | 2 +- package/macos-notarize-app.sh | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) mode change 100644 => 100755 package/macos-notarize-app.sh diff --git a/package/build-macos.sh b/package/build-macos.sh index dcf1f4bc3..8cf1bb699 100755 --- a/package/build-macos.sh +++ b/package/build-macos.sh @@ -48,7 +48,7 @@ echo "**** SIGN AND NOTARIZE *********************************" codesign -s "Developer ID Application: Sam Schott" \ --entitlements entitlements.plist --deep -o runtime dist/Maestral.app -macos-notarize-app.sh dist/Maestral.app +./macos-notarize-app.sh dist/Maestral.app echo "**** CREATING DMG **************************************" diff --git a/package/macos-notarize-app.sh b/package/macos-notarize-app.sh old mode 100644 new mode 100755 index eb6f2b695..abf027e74 --- a/package/macos-notarize-app.sh +++ b/package/macos-notarize-app.sh @@ -26,7 +26,6 @@ RESULT=$(xcrun altool --notarize-app --type osx \ --primary-bundle-id com.samschott.maestral \ --username $APPLE_ID_USER \ --password @env:APPLE_ID_PASSWORD \ - -itc_provider MetaBrainzFoundationInc \ --output-format xml) if [ $? -ne 0 ]; then From 2a8cb0cd8c03d56252e5302c4fad7398b3b24de4 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Wed, 20 May 2020 11:57:49 +0200 Subject: [PATCH 092/101] [package] notarise dmg instead of app --- package/build-macos.sh | 14 +++++-- package/macos-notarize-dmg.sh | 71 +++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 4 deletions(-) create mode 100755 package/macos-notarize-dmg.sh diff --git a/package/build-macos.sh b/package/build-macos.sh index 8cf1bb699..71bc48cc4 100755 --- a/package/build-macos.sh +++ b/package/build-macos.sh @@ -43,13 +43,15 @@ echo "**** COPY CLI ENTRY POINT ******************************" cp bin/maestral_cli dist/Maestral.app/Contents/MacOS/maestral_cli -echo "**** SIGN AND NOTARIZE *********************************" +echo "**** SIGNING APP ***************************************" +echo "removing xattr" +xattr -cr dist/Maestral.app + +echo "signing app" codesign -s "Developer ID Application: Sam Schott" \ --entitlements entitlements.plist --deep -o runtime dist/Maestral.app -./macos-notarize-app.sh dist/Maestral.app - echo "**** CREATING DMG **************************************" test -f dist/Maestral.dmg && rm dist/Maestral.dmg @@ -64,7 +66,11 @@ create-dmg \ "dist/Maestral.dmg" \ "dist/Maestral.app" +echo "signing dmg" codesign --verify --sign "Developer ID Application: Sam Schott" dist/Maestral.dmg -md5 -r dist/Maestral.dmg + +echo "**** NOTARISING DMG ************************************" + +./macos-notarize-dmg.sh dist/Maestral.dmg echo "**** DONE **********************************************" diff --git a/package/macos-notarize-dmg.sh b/package/macos-notarize-dmg.sh new file mode 100755 index 000000000..bb85e7c67 --- /dev/null +++ b/package/macos-notarize-dmg.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash + +if [ -z "$1" ]; then + echo "Specify dmg as first parameter" + exit 1 +fi + +if [ -z "$APPLE_ID_USER" ] || [ -z "$APPLE_ID_PASSWORD" ]; then + echo "You need to set your Apple ID credentials with \$APPLE_ID_USER and \$APPLE_ID_PASSWORD." + exit 1 +fi + +APP_BUNDLE=$(basename "$1") +APP_BUNDLE_DIR=$(dirname "$1") + +cd "$APP_BUNDLE_DIR" || exit 1 + +# Submit for notarization +echo "Submitting $APP_BUNDLE for notarization..." +RESULT=$(xcrun altool --notarize-app --type osx \ + --file "${APP_BUNDLE}" \ + --primary-bundle-id com.samschott.maestral \ + --username $APPLE_ID_USER \ + --password @env:APPLE_ID_PASSWORD \ + --output-format xml) + +if [ $? -ne 0 ]; then + echo "Submitting $APP_BUNDLE failed:" + echo "$RESULT" + exit 1 +fi + +REQUEST_UUID=$(echo "$RESULT" | xpath \ + "//key[normalize-space(text()) = 'RequestUUID']/following-sibling::string[1]/text()" 2> /dev/null) + +if [ -z "$REQUEST_UUID" ]; then + echo "Submitting $APP_BUNDLE failed:" + echo "$RESULT" + exit 1 +fi + +echo "$(echo "$RESULT" | xpath \ + "//key[normalize-space(text()) = 'success-message']/following-sibling::string[1]/text()" 2> /dev/null)" + +# Poll for notarization status +echo "Submitted notarization request $REQUEST_UUID, waiting for response..." +sleep 60 +while : +do + RESULT=$(xcrun altool --notarization-info "$REQUEST_UUID" \ + --username "$APPLE_ID_USER" \ + --password @env:APPLE_ID_PASSWORD \ + --output-format xml) + STATUS=$(echo "$RESULT" | xpath \ + "//key[normalize-space(text()) = 'Status']/following-sibling::string[1]/text()" 2> /dev/null) + + if [ "$STATUS" = "success" ]; then + echo "Notarization of $APP_BUNDLE succeeded!" + break + elif [ "$STATUS" = "in progress" ]; then + echo "Notarization in progress..." + sleep 20 + else + echo "Notarization of $APP_BUNDLE failed:" + echo "$RESULT" + exit 1 + fi +done + +# Staple the notary ticket +xcrun stapler staple "$APP_BUNDLE" From 0e70c786f9f61fb9532db98f824930883a9f12cf Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Wed, 20 May 2020 12:11:54 +0200 Subject: [PATCH 093/101] [daemon] improved doc strings --- maestral/daemon.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/maestral/daemon.py b/maestral/daemon.py index c2eb3a187..f123bc952 100644 --- a/maestral/daemon.py +++ b/maestral/daemon.py @@ -157,6 +157,14 @@ class Lock: @classmethod def singleton(cls, name, lock_path=None): + """ + Retrieve an existing lock object with a given 'name' or create a new one. Use this + method for thread-safe locks. + + :param str name: Name of lock file. + :param str lock_path: Directory for lock files. Defaults to the temporary + directory returned by :func:`tempfile.gettempdir()` if not given. + """ with cls._singleton_lock: try: @@ -268,7 +276,11 @@ def _send_term(pid): class MaestralLock: """ - A inter-process and inter-thread lock for Maestral. + A inter-process and inter-thread lock for Maestral. This is a wrapper around + :class:`Lock` which fills out the appropriate lockfile name and directory for the + given config name. + + :param str config_name: The name of the Maestral configuration. """ def __new__(cls, config_name): @@ -293,7 +305,7 @@ def get_maestral_pid(config_name): """ Returns Maestral's PID if the daemon is running, ``None`` otherwise. - :param str config_name: The name of the Maestral configuration to use. + :param str config_name: The name of the Maestral configuration. :returns: The daemon's PID. :rtype: int """ From e00c0f014c1389971ec398d9f5828c7c8f5e9907 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Wed, 20 May 2020 14:32:07 +0200 Subject: [PATCH 094/101] [daemon] further doc string improvements --- maestral/daemon.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/maestral/daemon.py b/maestral/daemon.py index f123bc952..0873ebb4f 100644 --- a/maestral/daemon.py +++ b/maestral/daemon.py @@ -229,9 +229,11 @@ def locked(self): def locking_pid(self): """ - Returns the PID of the process which currently holds the lock or None. + Returns the PID of the process which currently holds the lock or ``None``. This + should work on macOS, OpenBSD and Linux but may fail on some platforms. Always use + :meth:`locked` to check if the lock is held by any process. - :returns: The PID of the process which currently holds the lock or None. + :returns: The PID of the process which currently holds the lock or ``None``. :rtype: int """ @@ -276,7 +278,7 @@ def _send_term(pid): class MaestralLock: """ - A inter-process and inter-thread lock for Maestral. This is a wrapper around + An inter-process and inter-thread lock for Maestral. This is a wrapper around :class:`Lock` which fills out the appropriate lockfile name and directory for the given config name. From 9f9e294bdbe12941e3b6adb0d9749f2b8fcadf8b Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Wed, 20 May 2020 14:34:01 +0200 Subject: [PATCH 095/101] update roadmap --- ROADMAP.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 98e5b32da..76329502b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,15 +1,12 @@ #### Short term: -* Switch from implicit grant to PKCE OAuth flow as soon as Dropbox supports it (DONE). * Snap package once core20 is released and kde-neon works on core20. * CLI autocomplete for paths once there is better support from upstream `click`. -* Update macOS app bundle to Python 3.8 and Qt 5.14. +* Update macOS app bundle to Python 3.8. #### Long term: * deb and rpm packages: either with Pyinstaller executable or as Python package. * GUI support for multiple Dropbox accounts. -* Option to install command line scripts from macOS app bundle (DONE). -* Work with upstream `toga` to fix remaining issues for native macOS GUI, - notably memory leak in `rubicon.objc` (DONE). +* Consider using sqlite for index and state. From 66fe7a562e65080fd6f1e674f912d1b16c128423 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Wed, 20 May 2020 14:36:18 +0200 Subject: [PATCH 096/101] updated changelog --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37e9f94be..4b245686a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,11 +17,11 @@ - Fixes a race condition when two processes try to start a sync daemon at the same time. - Fixes an issue in the macOS GUI where updating the displayed sync issues could fail. - Fixes truncated text in the macOS setup dialog. -- Fixes an issue on fresh macOS installs where creating autostart entries could fai if +- Fixes an issue on fresh macOS installs where creating autostart entries could fail if /Library/LaunchAgents does not yet exist. - Fixes an issue in the macOS app bundle where installing the command line could tool - would fail if /usr/local/bin is owned by root (as is default). Now, the user is asked - for permission instead. + would fail if /usr/local/bin is owned by root (as is default on a fresh install). Now, + the user is asked for permission instead. #### Dependencies: From 3964dc9ac0d75e304b203f9813f1a1fd8119a469 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Wed, 20 May 2020 15:16:21 +0200 Subject: [PATCH 097/101] [docs] added docs for config and state --- docs/config_files.rst | 68 +++++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 2 ++ docs/state_files.rst | 65 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+) create mode 100644 docs/config_files.rst create mode 100644 docs/state_files.rst diff --git a/docs/config_files.rst b/docs/config_files.rst new file mode 100644 index 000000000..5626b4c09 --- /dev/null +++ b/docs/config_files.rst @@ -0,0 +1,68 @@ + +Config files +============ + +The config files are located at ``$XDG_CONFIG_HOME/maestral`` on Linux (typically +``~/.config/maestral``) and ``~/Library/Application Support/maestral`` on macOS. Each +configuration will get its own INI file with the settings documented below. + +Config values in the sections ``main`` and ``account`` should not be edited manually but +rather through the corresponding CLI commands or GUI options. This is because changes of +these settings require Maestral to perform accompanying actions, e.g., download items +which have been removed from the excluded list or move the local Dropbox directory. +Those will not be performed if the user edits the options manually. + +Changes to the other sections may be performed manually but will only take effect once +Maestral is restarted. Maestral will overwrite the entire config file if any change is +made to one of the options through the ``maestral.config`` module. + +.. code-block:: ini + + [main] + + # The current Dropbox directory + path = /Users/samschott/Dropbox (Maestral) + + # Default directory name for the config + default_dir_name = Dropbox (Maestral) + + # List of excluded files and folders + excluded_items = ['/test_folder', '/sub/folder'] + + # Config file version (not the Maestral version!) + version = 12.0.0 + + [account] + + # Unique Dropbox account ID. The account's email + # address may change and is therefore not stored here. + account_id = dbid:AABP7CC5bpYd8cGHqIColDFrMoc9SdhACA4 + + [app] + + # Level for desktop notifications: + # 15 = FILECHAANGE + # 30 = SYNCISSUE + # 40 = ERROR + # 100 = NONE + notification_level = 15 + + # Level for log messages: + # 10 = DEBUG + # 20 = INFO + # 30 = WARNING + # 40 = ERR0R + log_level = 20 + + # Interval in sec to check for updates + update_notification_interval = 604800 + + # Enable or disable automatic error reports + analytics = False + + [sync] + + # Interval in sec to perform a full reindexing + reindex_interval = 604800 + # Maximum CPU usage per core + max_cpu_percent = 20.0 diff --git a/docs/index.rst b/docs/index.rst index e958cbb97..312aea908 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -18,6 +18,8 @@ The following APIs should remain stable for frontends such as the GUI or CLI: :caption: Table of Contents :maxdepth: 2 + config_files + state_files api/index .. toctree:: diff --git a/docs/state_files.rst b/docs/state_files.rst new file mode 100644 index 000000000..84a9ad5f3 --- /dev/null +++ b/docs/state_files.rst @@ -0,0 +1,65 @@ + +State files +=========== + +Maestral saves its persistent state in two files: "{config_name}.index" for the file +index and "{config_name}.state" for anything else. Both files are located at +$XDG_DATA_DIR/maestral on Linux (typically ~/.local/share/maestral) and +~/Library/Application Support/maestral on macOS. Each configuration will get its own +state file. + + +Index file +********** + +The index file contains all the tracked files and folders with their lower-case path +relative to the Dropbox folder and their "rev". Each line contains a single entry, written +as a dictionary ``{path: rev}`` in json format, for example: + +.. code-block:: python + + {"/test folder/subfolder/file.txt": "015a4ae1f15853400000001695a6c40"} + +If there are multiple entries (lines) which refer to the same path, the last entry +overwrites any previous entries. This allows rapidly updating the rev for a file or folder +by appending a new line to the index file without needing to write an entire file. + +An entry with ``rev == None`` means that any previous entries for this path and its +children should be discarded. + +After a sync cycle has completed, the file is cleaned up and all duplicate or empty +entries are removed. + + +State file +********** + +The state file has the following sections: + +.. code-block:: ini + + [account] + email = foo@bar.com + display_name = Foo Bar + abbreviated_name = FB + type = business + usage = 39.2% of 1312.8TB used + usage_type = team + + [app] + update_notification_last = 0.0 + latest_release = 1.0.3 + + [sync] + cursor = ... + lastsync = 1589979736.623609 + last_reindex = 1589577566.8533309 + download_errors = [] + pending_downloads = [] + recent_changes = [] + + [main] + version = 12.0.0 + +Notably, account info which can be changed by the user such as the email address is saved +in the state file while only the fixed Dropbox ID is saved in the config file. From 5fd68ca083d40a90a525ca6dbac8c8b2bb34aca8 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Wed, 20 May 2020 16:31:34 +0200 Subject: [PATCH 098/101] [cli] fixed typo in help text --- maestral/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maestral/cli.py b/maestral/cli.py index dc839f6c6..766b9f598 100755 --- a/maestral/cli.py +++ b/maestral/cli.py @@ -368,7 +368,7 @@ def _run_daemon(ctx, param, value): @click.group(cls=SpecialHelpOrder, context_settings=CONTEXT_SETTINGS) def main(): - """Maestral Dropbox Client for Linux and macOS.""" + """Maestral Dropbox client for Linux and macOS.""" freeze_support() check_for_updates() From 12aaaccd2fcec0f93aecdfca8777d69b449d47e8 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Wed, 20 May 2020 16:55:59 +0200 Subject: [PATCH 099/101] [docs] removed modules --- docs/api/utils/backend.rst | 7 ------- docs/api/utils/index.rst | 2 -- docs/api/utils/oauth_implicit.rst | 7 ------- 3 files changed, 16 deletions(-) delete mode 100644 docs/api/utils/backend.rst delete mode 100644 docs/api/utils/oauth_implicit.rst diff --git a/docs/api/utils/backend.rst b/docs/api/utils/backend.rst deleted file mode 100644 index 960995ae9..000000000 --- a/docs/api/utils/backend.rst +++ /dev/null @@ -1,7 +0,0 @@ - -Backend functions -================= - -.. automodule:: utils.backend - :members: - :show-inheritance: diff --git a/docs/api/utils/index.rst b/docs/api/utils/index.rst index e891be541..393537e3c 100644 --- a/docs/api/utils/index.rst +++ b/docs/api/utils/index.rst @@ -7,10 +7,8 @@ maestral.utils utils.appdirs utils.autostart - utils.backend utils.housekeeping utils.notify - utils.oauth_implicit utils.path utils.serializer utils.updates diff --git a/docs/api/utils/oauth_implicit.rst b/docs/api/utils/oauth_implicit.rst deleted file mode 100644 index 80ec4630b..000000000 --- a/docs/api/utils/oauth_implicit.rst +++ /dev/null @@ -1,7 +0,0 @@ - -OAuth implicit flow -=================== - -.. automodule:: utils.oauth_implicit - :members: - :show-inheritance: From 767aefa5ef9312c0f15f6e72264c7045117607ab Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Wed, 20 May 2020 21:31:37 +0200 Subject: [PATCH 100/101] bump to v1.0.3 --- maestral/__init__.py | 2 +- setup.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/maestral/__init__.py b/maestral/__init__.py index 395b2f103..ff0afcf60 100644 --- a/maestral/__init__.py +++ b/maestral/__init__.py @@ -16,6 +16,6 @@ """ -__version__ = '1.0.3.beta1' +__version__ = '1.0.3' __author__ = 'Sam Schott' __url__ = 'https://github.com/SamSchott/maestral' diff --git a/setup.py b/setup.py index fc5aa017a..fe52c5e87 100644 --- a/setup.py +++ b/setup.py @@ -30,8 +30,8 @@ ] gui_requires = [ - 'maestral_qt>=1.0.3.beta1;sys_platform=="linux"', - 'maestral_cocoa>=1.0.3.beta1;sys_platform=="darwin"', + 'maestral_qt>=1.0.3;sys_platform=="linux"', + 'maestral_cocoa>=1.0.3;sys_platform=="darwin"', ] syslog_requires = ['systemd-python'] From 928834b478a9558816e39bfdb352acf7c123d906 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Wed, 20 May 2020 21:46:46 +0200 Subject: [PATCH 101/101] [autostart] flake8 --- maestral/utils/autostart.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/maestral/utils/autostart.py b/maestral/utils/autostart.py index 84b07c3fe..57fadf8f0 100644 --- a/maestral/utils/autostart.py +++ b/maestral/utils/autostart.py @@ -219,7 +219,7 @@ def __init__(self, config_name, gui): with open(osp.join(_resources, 'com.samschott.maestral.plist')) as f: plist_template = f.read() - + self.path = osp.join(get_home_dir(), 'Library', 'LaunchAgents') self.destination = osp.join(self.path, filename) @@ -232,7 +232,7 @@ def __init__(self, config_name, gui): def _enable(self): os.makedirs(self.path, exist_ok=True) - + with open(self.destination, 'w+') as f: f.write(self.contents)