예제 #1
0
def main():
    create_required_directories()
    init_logging()
    log = logging.getLogger(LOGGER_NAME)
    instance = SingleInstance(DAEMON_LOCK_PATH)
    try:
        instance.start()
    except InstanceAlreadyRunning:
        log.info('Daemon: Could not acquire single instance lock. Aborting.')
    else:
        try:
            with open(DAEMON_PID_PATH, 'w') as f:
                f.write(str(os.getpid()))
            # TODO find out why the core isn't killing itself when the daemon is killed
            # TODO maybe I should handle the SIGTERM signal?
            monkey.patch_all()
            gevent.spawn(check_own_version, log)

            # Singletons
            core_client = CoreClient()
            ui_config = UIConfig()

            notifs_handler = NotificationsHandler()

            ui_listener = UIListener(UI_LISTENER_SOCKET_ADDRESS, core_client,
                                     ui_config, notifs_handler)
            core_listener = CoreListener(CORE_LISTENER_SOCKET_ADDRESS,
                                         core_client, ui_config,
                                         notifs_handler)
            core = Core(core_client)

            # Attempt to stop core, just in case there is a dangling core running
            core.stop()
            ui_listener_greenlet = gevent.spawn(ui_listener.start)
            core_listener_greenlet = gevent.spawn(core_listener.start)
            watchdog_greenlet = gevent.spawn(core.watchdog)

            Events.shutdown_required.wait()

            log.info('Daemon: Shutting down')

            ui_listener_greenlet.kill(block=True)
            core_listener_greenlet.kill(block=True)
            watchdog_greenlet.kill(block=True)

            # Sending a shutdown to the core should be enough to gracefully kill it,
            # but it might ignore us so after a little while, kill it forcefully
            try:
                core_client.shutdown()
                gevent.sleep(1)
            except Exception:
                log.warning(
                    'Daemon: Gracefull shutdown has thrown an exception. '
                    ' Will ignore it just shut the core down forcefully.')
            core.stop()
        except Exception:
            log.exception('Daemon: An uncatched error occurred!')
        finally:
            instance.stop()
예제 #2
0
 def __init__(self, daemon_client):
     self.daemon_client = daemon_client
     self.ui_config = UIConfig()
예제 #3
0
class CLIHandler(object):
    def __init__(self, daemon_client):
        self.daemon_client = daemon_client
        self.ui_config = UIConfig()

    def _daemon_already_running(self):
        return test_already_running(DAEMON_PID_PATH, DAEMON_BINARY_FILENAME)

    def _core_already_running(self):
        return test_already_running(CORE_PID_PATH, CORE_BINARY_FILENAME)

    def _start_daemon(self):
        pid = self._daemon_already_running()
        if pid:
            if self._core_already_running():
                raise AlreadyRunningException(pid)
            else:
                log.warning('CLIHandler._start_daemon: Core is not running and we don\'t know why. '
                            'Just in case, we\'ll stop the daemon too and continue as if nothing happened.')
                self.stop(quiet=True)
        self._handle_dying_message()
        log.info('Starting daemon')
        # assumes daemon binary is in same dir as cli
        daemon_binary_dir = get_own_dir(__file__)
        daemon_binary_path = os.path.join(daemon_binary_dir, DAEMON_BINARY_FILENAME)
        self.process = Popen([daemon_binary_path], preexec_fn=lambda: os.setpgrp())
        # Try pinging daemon to make sure the listener is on
        self.daemon_client.attemptFirstConnection()

    def _handle_dying_message(self):
        dying_message = self.ui_config.get('dying_message')
        if dying_message:
            self.out('It looks like the daemon has gone and died while you were not looking...\n'
                     'Fortunately it left you with the following dying message:\n\n'
                     '---------------- DYING MESSAGE ----------------\n{0}\n'
                     '---------------- END DYING MESSAGE ----------------\n'.format(dying_message))
            self.ui_config.unset('dying_message')

    def _handle_cloud_home(self):
        cloud_home = self.ui_config.get('cloud_home')
        if not cloud_home:
            success = False
            default_cloud_home = os.path.join(os.getenv('HOME'), CLOUD_HOME_DEFAULT_PATH)
            while not success:
                cloud_home = self.ask('Which folder do you want to use as your MEO Cloud folder', default=default_cloud_home)
                force_create = False
                force_merge = False
                force_relative = False
                last_state = None
                # Keep trying the same path as long as some option has changed
                while last_state != (force_create, force_merge, force_relative):
                    last_state = (force_create, force_merge, force_relative)
                    result = self.setCloudHome(cloud_home, force_create, force_merge, force_relative)
                    if result == 'OK':
                        self.out('The selected folder will be used as your MEO Cloud folder')
                        success = True
                    elif result == 'OK_FOLDER_CREATED':
                        self.out('The selected folder was created and will be used as your MEO Cloud folder')
                        success = True
                    elif result == 'PATH_IS_FILE':
                        self.out('The given path contains a file which is preventing the creation of you MEO Cloud folder.')
                    elif result == 'FOLDER_NOT_FOUND':
                        self.out('The selected folder does not exist.')
                        force_create = self.ask_yes_no('Create it', default_yes=True)
                    elif result == 'PATH_TO_FOLDER_NOT_FOUND':
                        self.out('Multiple folders in the given path do not exist.')
                        force_create = self.ask_yes_no('Do you want them to be created')
                    elif result == 'RELATIVE_PATH':
                        abspath = os.path.abspath(cloud_home)
                        self.out('You provided a relative path which corresponds to the absolute path: {0}'.format(abspath))
                        force_relative = self.ask_yes_no('Are you sure this is the path you want')
                    elif result == 'PERMISSION_DENIED':
                        self.out('You do not have permissions to create a folder in that path.')
                    elif result == 'INVALID_PATH':
                        self.out('The given path is not a correct path, please review it.')
                    elif result == 'FOLDER_HAS_CONTENT':
                        self.out('The selected folder has content inside. If you want to use it, the contents will be synchronized to your account.')
                        force_merge = self.ask_yes_no('Are you sure you want to use it')
                    else:
                        # TODO improve error handling
                        assert False

    def setCloudHome(self, cloud_home, force_create, force_merge, force_relative):
        must_create = False
        cloud_home = os.path.normpath(cloud_home)

        if not force_relative and not cloud_home.startswith('/'):
            return 'RELATIVE_PATH'

        cloud_home = os.path.abspath(cloud_home)

        try:
            current_path = cloud_home
            while current_path != '/':
                if os.path.exists(current_path):
                    if not os.path.isdir(current_path):
                        return 'PATH_IS_FILE'
                    break
                current_path = os.path.dirname(current_path)

            if os.path.exists(cloud_home):
                if os.listdir(cloud_home):
                    if force_merge:
                        rv = 'OK'
                    else:
                        return 'FOLDER_HAS_CONTENT'
                else:
                    rv = 'OK'
            else:
                parent_dir = os.path.dirname(cloud_home)
                if force_create:
                    must_create = True
                    rv = 'OK_FOLDER_CREATED'
                else:
                    if os.path.exists(parent_dir):
                        return 'FOLDER_NOT_FOUND'
                    else:
                        return 'PATH_TO_FOLDER_NOT_FOUND'

            if must_create:
                os.makedirs(cloud_home)

        except OSError as os_err:
            if os_err.errno == errno.EACCES:
                return 'PERMISSION_DENIED'
            return 'INVALID_PATH'

        if rv in ('OK', 'OK_FOLDER_CREATED'):
            self.ui_config.set('cloud_home', cloud_home)
        return rv

    def _handle_client_registration(self):
        device_name = self.ask('What\'s your device name', default=socket.gethostname())
        url = self.daemon_client.authorizeWithDeviceName(device_name)
        if not url:
            # Some error occurred, we're probably offline
            raise CoreOfflineException()
        self.out('Please open this url in your browser: {0}'.format(url))
        init_result = self.daemon_client.waitForAuthorization()
        if init_result == InitResult.OFFLINE:
            raise CoreOfflineException()
        elif init_result == InitResult.TIMEDOUT:
            raise TimeoutException()
        elif init_result != InitResult.AUTHORIZATION_OK:
            # TODO improve error handling
            assert False
        self.out('Your client has successfully been associated with your account.')

    @exceptions_handled
    def start(self):
        log.debug('CLIHandler.start()')
        try:
            # TODO test killing in the middle of each action
            while True:
                self._start_daemon()
                self._handle_cloud_home()
                self.daemon_client.startCore()
                init_result = self.daemon_client.init()
                if init_result == InitResult.OFFLINE:
                    raise CoreOfflineException()
                elif init_result == InitResult.AUTHORIZATION_REQUIRED:
                    self._handle_client_registration()
                elif init_result == InitResult.ROOT_FOLDER_MISSING:
                    cloud_home = self.daemon_client.getCloudHome()
                    self.out('Could not find your MEO Cloud\'s folder in path "{0}".'.format(cloud_home))
                    self.ui_config.unset('cloud_home')
                    with open(PURGEMETA_PATH, 'w'):
                        # touch
                        pass
                    self.stop(quiet=True)
                    continue
                elif init_result == InitResult.TIMEDOUT:
                    raise TimeoutException()
                elif init_result != InitResult.AUTHORIZATION_OK:
                    # TODO improve error handling
                    assert False
                break
            # TODO wait for sync start?
            self.out('MEO Cloud is ready to use.')
            return True
        except CoreOfflineException:
            self.out('It was not possible to establish a connection to the MEO Cloud servers.')
            self.out('Please check your connection, or if you\'re behind a proxy, '
                     'set it using the http_proxy environment variable.')
            self.stop()
        except AlreadyRunningException:
            self.out('Failed to start MEO Cloud.')
            self.out('MEO Cloud was running already.')
        except (socket.timeout, ListenerConnectionFailedException, EOFError, KeyboardInterrupt):
            self.out('Failed to start MEO Cloud.')
            self.stop(quiet=True)
            raise

    def stop(self, quiet=False):
        log.debug('CLIHandler.stop()')
        stopped = False

        pid = self._daemon_already_running()
        if pid:
            try:
                log.info('CLIHandler.stop: gracefull daemon shutdown')
                self.daemon_client.shutdown()
                stopped = True
                # Sleep a bit to give time for the daemon and the core
                # to gracefully shut themselves
                time.sleep(2)
            except (ListenerConnectionFailedException, socket.timeout):
                pass

            if not stopped or self._daemon_already_running():
                # Connection to daemon_client might have been lost
                # or the gracefull kill wasn't enough.
                # Will try to kill using pid
                log.info('CLIHandler.stop: forcefull daemon shutdown')
                try:
                    os.kill(pid, signal.SIGTERM)
                except OSError:
                    # If we failed to kill it there's a good chance that the reason
                    # is that he's already dead
                    pass
                stopped = True

        pid = self._core_already_running()
        if pid:
            # Apparently we have a dangling core...
            # Kill it!
            log.info('CLIHandler.stop: forcefull core shutdown')
            try:
                os.kill(pid, signal.SIGTERM)
            except OSError:
                # If we failed to kill it there's a good chance that the reason
                # is that he's already dead
                pass
            stopped = True

        if not quiet:
            if stopped:
                self.out('MEO Cloud has been stopped.')
            else:
                self.out('MEO Cloud does not seem to be running.')
        return True

    @exceptions_handled
    @daemon_must_be_running
    def status(self):
        log.debug('CLIHandler.status()')
        status_result = self.daemon_client.status()
        status = status_result.status

        if status_result.persistentNotifs:
            for persistent_notif in status_result.persistentNotifs:
                self.out('WARNING: {0}'.format(persistent_notif))

        if status.totalQuota:
            percentage = float(status.usedQuota) / status.totalQuota
            used_quota_human_readable = human_readable_bytes(status.usedQuota)
            total_quota_human_readable = human_readable_bytes(status.totalQuota)
            self.out('Quota: {0:.2%} ({1} of {2})'.format(
                percentage, used_quota_human_readable, total_quota_human_readable))

        if status.state == State.READY:
            self.out('Status: IDLE')
        else:
            self.out('Status: {0}'.format(State._VALUES_TO_NAMES[status.state]))

        sync_status = status_result.syncStatus
        if sync_status:
            # TODO Handle sync_code
            log.debug('CLIHandler.status: sync_code: {0}'.format(sync_status.syncCode))
            if sync_status.pendingUploads:
                if sync_status.uploadRate:
                    self.out('Uploading {0} files, at {1} ({2} left)'.format(
                        sync_status.pendingUploads,
                        human_readable_bytes(sync_status.uploadRate, per_second=True),
                        human_readable_seconds(sync_status.uploadETASecs)
                    ))
                else:
                    self.out('Uploading {0} files'.format(sync_status.pendingUploads))
            if sync_status.pendingDownloads:
                if sync_status.downloadRate:
                    self.out('Downloading {0} files, at {1} ({2} left)'.format(
                        sync_status.pendingDownloads,
                        human_readable_bytes(sync_status.downloadRate, per_second=True),
                        human_readable_seconds(sync_status.downloadETASecs)
                    ))
                else:
                    self.out('Downloading {0} files'.format(sync_status.pendingDownloads))
            if sync_status.pendingIndexes:
                self.out('Indexing {0} files'.format(sync_status.pendingIndexes))
        return True

    @exceptions_handled
    @daemon_must_be_running
    def notifications(self, n_lines):
        log.debug('CLIHandler.notifications()')
        try:
            with open(NOTIFICATIONS_LOG_PATH, 'r') as f:
                lines = tail(f, n_lines)
        except IOError:
            lines = None
        if lines:
            self.out('\n'.join(lines))
        else:
            self.out('You have no notifications')
        return True

    @exceptions_handled
    @daemon_must_be_running
    def list_sync(self):
        log.debug('CLIHandler.list_sync()')
        ignored_directories = self.daemon_client.ignoredDirectories()
        if not ignored_directories:
            self.out('All your directories are being synchronized.')
        else:
            self.out('All directories are being synchronized except these:')
            self.out('\n'.join(ignored_directories))
        return True

    @exceptions_handled
    @daemon_must_be_running
    def add_sync(self, path):
        log.debug('CLIHandler.add_sync({0})'.format(path))
        if path == '__ALL__':
            self.daemon_client.setIgnoredDirectories([])
            self.out('Synchronization was resumed for every path whose synchronization was stopped.')
        else:
            orig_path = path
            normalized_path = path.rstrip('/')
            if normalized_path == '':
                self.out('"/" is the root of your MEO Cloud folder whose synchronization '
                         'cannot be stopped and thus does not need to be resumed.')
            else:
                normalized_path = self.test_or_add_leading_slash(normalized_path, orig_path)
                ignored_directories = self.daemon_client.ignoredDirectories()
                if normalized_path in ignored_directories:
                    if self.test_valid_path(normalized_path, orig_path, quiet=True):
                        self.out('"{0}" will now be added back to your MEO Cloud folder '
                                 'and its synchronization will resume.'.format(orig_path))
                    else:
                        self.out('"{0}" has been removed from the '
                                 'stopped synchronization list'.format(orig_path))
                    ignored_directories.remove(normalized_path)
                    self.daemon_client.setIgnoredDirectories(ignored_directories)
                elif self.test_valid_path(normalized_path, orig_path):
                    self.out('Nothing to do. "{0}" was already being synchronized.'.format(orig_path))
        return True

    @exceptions_handled
    @daemon_must_be_running
    def remove_sync(self, path, force):
        log.debug('CLIHandler.remove_sync({0})'.format(path))
        orig_path = path
        normalized_path = path.rstrip('/')
        if normalized_path == '':
            self.out('Cannot stop synchronizing the root of your MEO Cloud folder.')
        else:
            normalized_path = self.test_or_add_leading_slash(normalized_path, orig_path)
            ignored_directories = self.daemon_client.ignoredDirectories()
            if normalized_path in ignored_directories:
                self.out('Nothing to do. Synchronization of "{0}" was already stopped.'.format(orig_path))
            elif force or self.test_valid_path(normalized_path, orig_path):
                    ignored_directories.append(normalized_path)
                    self.daemon_client.setIgnoredDirectories(ignored_directories)
                    self.out('"{0}" was removed from the synced paths and will be removed '
                             'from your filesystem when syncing is done.'.format(orig_path))
        return True

    def test_or_add_leading_slash(self, normalized_path, orig_path):
        # No leading slash
        if not normalized_path.startswith('/'):
            self.out('Your path did not start with a slash (/).')
            yes = self.ask_yes_no('Did you mean {0}'.format('/' + orig_path))
            if yes:
                normalized_path = '/' + normalized_path
        return normalized_path

    def test_valid_path(self, normalized_path, orig_path, quiet=False):
        if normalized_path.startswith('/'):
            parent_dir = os.path.dirname(normalized_path)
        else:
            parent_dir = '/'
        remote_directories = self.daemon_client.remoteDirectoryListing(parent_dir).listing

        if remote_directories is None:
            if quiet:
                return True
            self.out('Could not contact the server to validate that path "{0}"'
                     ' is a valid path in your MEO Cloud folder.'.format(orig_path))
            return self.ask_yes_no('Do you want to continue?')
        # TODO handle remoteDirectoryListing statusCode
        if normalized_path not in remote_directories:
            if quiet:
                return False
            self.out('Path "{0}" is not a correct path in your MEO Cloud folder'.format(orig_path))
            if not remote_directories and parent_dir != '/':
                # Either the folder was empty or the parent folder was invalid.
                # Since there is no easy way of knowing what happened, we'll just list the examples for the root folder
                parent_dir = '/'
                remote_directories = self.daemon_client.remoteDirectoryListing(parent_dir).listing
            if remote_directories:
                self.out('Here are some examples of correct paths:')
                for correct_path in remote_directories[:20]:
                    self.out(correct_path)
            elif parent_dir == '/':
                self.out('You have no folders in MEO Cloud, which means there is no correct path.')
            else:
                assert False
            return False
        return True

    @exceptions_handled
    @daemon_must_be_running
    def pause(self):
        log.debug('CLIHandler.pause()')
        self.daemon_client.pause()
        self.out('MEO Cloud synchronization was stopped. Enter meocloud resume to restart sync.')
        return True
        # TODO Warn if already paused

    @exceptions_handled
    @daemon_must_be_running
    def resume(self):
        log.debug('CLIHandler.resume()')
        self.daemon_client.unpause()
        self.out('MEO Cloud synchronization was resumed.')
        return True
        # TODO Warn if already unpaused

    @exceptions_handled
    @check_daemon_version
    def version(self):
        log.debug('CLIHandler.version()')
        try:
            core_version = self.daemon_client.coreVersion()
        except ListenerConnectionFailedException:
            self.out('{0}'.format(VERSION))
        else:
            self.out('{0} (core version: {1})'.format(VERSION, core_version))
        return True

    @exceptions_handled
    @daemon_must_be_running
    def unlink(self):
        log.debug('CLIHandler.unlink()')
        # Make sure user wants to unlink
        really_unlink = self.ask_yes_no('Are you sure you want to unlink your account')

        # Actually unlink
        if really_unlink:
            unlink_success = self.daemon_client.unlink()
            if unlink_success:
                self.out('Successfully unlinked account.')
            else:
                self.out('No account to unlink.')
            self.stop()
        return True

    def ask(self, msg='', default=None, *args, **kwargs):
        if default:
            msg = msg + ' [{0}]? '.format(default)
        else:
            msg = msg + '? '
        self.out(msg, end='')
        answer = self.get_input()
        if not answer and default is not None:
            answer = default
        return answer

    def ask_yes_no(self, msg='', default_yes=False, *args, **kwargs):
        if default_yes:
            msg = msg + ' [n/Y]? '
            default = 'y'
        else:
            msg = msg + ' [N/y]? '
            default = 'n'
        while True:
            self.out(msg, end='')
            answer = self.get_input()
            if not answer:
                answer = default
            if answer.lower() == 'y':
                return True
            if answer.lower() == 'n':
                return False
            self.out('Did not recognize answer "{0}". Please answer again.'.format(answer))

    def get_input(self):
        if not sys.stdin.isatty():
            log.warning('CLI: Must get input but stdin is not a terminal.')
            self.out()
            self.out('ERROR: Must get input but stdin is not a terminal.')
            self.stop(quiet=True)
            self.out('Goodbye!')
            sys.exit(1)
        return input().strip()

    def out(self, msg='', *args, **kwargs):
        print_(msg, *args, **kwargs)
예제 #4
0
 def __init__(self, daemon_client):
     self.daemon_client = daemon_client
     self.ui_config = UIConfig()
예제 #5
0
class CLIHandler(object):
    def __init__(self, daemon_client):
        self.daemon_client = daemon_client
        self.ui_config = UIConfig()

    def _daemon_already_running(self):
        return test_already_running(DAEMON_PID_PATH, DAEMON_BINARY_FILENAME)

    def _core_already_running(self):
        return test_already_running(CORE_PID_PATH, CORE_BINARY_FILENAME)

    def _start_daemon(self):
        pid = self._daemon_already_running()
        if pid:
            if self._core_already_running():
                raise AlreadyRunningException(pid)
            else:
                log.warning(
                    'CLIHandler._start_daemon: Core is not running and we don\'t know why. '
                    'Just in case, we\'ll stop the daemon too and continue as if nothing happened.'
                )
                self.stop(quiet=True)
        self._handle_dying_message()
        log.info('Starting daemon')
        # assumes daemon binary is in same dir as cli
        daemon_binary_dir = get_own_dir(__file__)
        daemon_binary_path = os.path.join(daemon_binary_dir,
                                          DAEMON_BINARY_FILENAME)
        self.process = Popen([daemon_binary_path],
                             preexec_fn=lambda: os.setpgrp())
        # Try pinging daemon to make sure the listener is on
        self.daemon_client.attemptFirstConnection()

    def _handle_dying_message(self):
        dying_message = self.ui_config.get('dying_message')
        if dying_message:
            self.out(
                'It looks like the daemon has gone and died while you were not looking...\n'
                'Fortunately it left you with the following dying message:\n\n'
                '---------------- DYING MESSAGE ----------------\n{0}\n'
                '---------------- END DYING MESSAGE ----------------\n'.format(
                    dying_message))
            self.ui_config.unset('dying_message')

    def _handle_cloud_home(self):
        cloud_home = self.ui_config.get('cloud_home')
        if not cloud_home:
            success = False
            default_cloud_home = os.path.join(os.getenv('HOME'),
                                              CLOUD_HOME_DEFAULT_PATH)
            while not success:
                cloud_home = self.ask(
                    'Which folder do you want to use as your MEO Cloud folder',
                    default=default_cloud_home)
                force_create = False
                force_merge = False
                force_relative = False
                last_state = None
                # Keep trying the same path as long as some option has changed
                while last_state != (force_create, force_merge,
                                     force_relative):
                    last_state = (force_create, force_merge, force_relative)
                    result = self.setCloudHome(cloud_home, force_create,
                                               force_merge, force_relative)
                    if result == 'OK':
                        self.out(
                            'The selected folder will be used as your MEO Cloud folder'
                        )
                        success = True
                    elif result == 'OK_FOLDER_CREATED':
                        self.out(
                            'The selected folder was created and will be used as your MEO Cloud folder'
                        )
                        success = True
                    elif result == 'PATH_IS_FILE':
                        self.out(
                            'The given path contains a file which is preventing the creation of you MEO Cloud folder.'
                        )
                    elif result == 'FOLDER_NOT_FOUND':
                        self.out('The selected folder does not exist.')
                        force_create = self.ask_yes_no('Create it',
                                                       default_yes=True)
                    elif result == 'PATH_TO_FOLDER_NOT_FOUND':
                        self.out(
                            'Multiple folders in the given path do not exist.')
                        force_create = self.ask_yes_no(
                            'Do you want them to be created')
                    elif result == 'RELATIVE_PATH':
                        abspath = os.path.abspath(cloud_home)
                        self.out(
                            'You provided a relative path which corresponds to the absolute path: {0}'
                            .format(abspath))
                        force_relative = self.ask_yes_no(
                            'Are you sure this is the path you want')
                    elif result == 'PERMISSION_DENIED':
                        self.out(
                            'You do not have permissions to create a folder in that path.'
                        )
                    elif result == 'INVALID_PATH':
                        self.out(
                            'The given path is not a correct path, please review it.'
                        )
                    elif result == 'FOLDER_HAS_CONTENT':
                        self.out(
                            'The selected folder has content inside. If you want to use it, the contents will be synchronized to your account.'
                        )
                        force_merge = self.ask_yes_no(
                            'Are you sure you want to use it')
                    else:
                        # TODO improve error handling
                        assert False

    def setCloudHome(self, cloud_home, force_create, force_merge,
                     force_relative):
        must_create = False
        cloud_home = os.path.normpath(cloud_home)

        if not force_relative and not cloud_home.startswith('/'):
            return 'RELATIVE_PATH'

        cloud_home = os.path.abspath(cloud_home)

        try:
            current_path = cloud_home
            while current_path != '/':
                if os.path.exists(current_path):
                    if not os.path.isdir(current_path):
                        return 'PATH_IS_FILE'
                    break
                current_path = os.path.dirname(current_path)

            if os.path.exists(cloud_home):
                if os.listdir(cloud_home):
                    if force_merge:
                        rv = 'OK'
                    else:
                        return 'FOLDER_HAS_CONTENT'
                else:
                    rv = 'OK'
            else:
                parent_dir = os.path.dirname(cloud_home)
                if force_create:
                    must_create = True
                    rv = 'OK_FOLDER_CREATED'
                else:
                    if os.path.exists(parent_dir):
                        return 'FOLDER_NOT_FOUND'
                    else:
                        return 'PATH_TO_FOLDER_NOT_FOUND'

            if must_create:
                os.makedirs(cloud_home)
                os.chmod(cloud_home, 0700)

        except OSError as os_err:
            if os_err.errno == errno.EACCES:
                return 'PERMISSION_DENIED'
            return 'INVALID_PATH'

        if rv in ('OK', 'OK_FOLDER_CREATED'):
            self.ui_config.set('cloud_home', cloud_home)
        return rv

    def _handle_client_registration(self):
        device_name = self.ask('What\'s your device name',
                               default=socket.gethostname())
        url = self.daemon_client.authorizeWithDeviceName(device_name)
        if not url:
            # Some error occurred, we're probably offline
            raise CoreOfflineException()
        self.out('Please open this url in your browser: {0}'.format(url))
        init_result = self.daemon_client.waitForAuthorization()
        if init_result == InitResult.OFFLINE:
            raise CoreOfflineException()
        elif init_result == InitResult.TIMEDOUT:
            raise TimeoutException()
        elif init_result != InitResult.AUTHORIZATION_OK:
            # TODO improve error handling
            assert False
        self.out(
            'Your client has successfully been associated with your account.')

    @exceptions_handled
    def start(self):
        log.debug('CLIHandler.start()')
        try:
            # TODO test killing in the middle of each action
            while True:
                self._start_daemon()
                self._handle_cloud_home()
                self.daemon_client.startCore()
                init_result = self.daemon_client.init()
                if init_result == InitResult.OFFLINE:
                    raise CoreOfflineException()
                elif init_result == InitResult.AUTHORIZATION_REQUIRED:
                    self._handle_client_registration()
                elif init_result == InitResult.ROOT_FOLDER_MISSING:
                    cloud_home = self.ui_config.get('cloud_home')
                    self.out(
                        'Could not find your MEO Cloud\'s folder in path "{0}".'
                        .format(cloud_home))
                    self.ui_config.unset('cloud_home')
                    with open(PURGEMETA_PATH, 'w'):
                        # touch
                        pass
                    self.stop(quiet=True)
                    continue
                elif init_result == InitResult.TIMEDOUT:
                    raise TimeoutException()
                elif init_result != InitResult.AUTHORIZATION_OK:
                    # TODO improve error handling
                    assert False
                break
            # TODO wait for sync start?
            self.out('MEO Cloud is ready to use.')
            return True
        except CoreOfflineException:
            self.out(
                'It was not possible to establish a connection to the MEO Cloud servers.'
            )
            self.out(
                'Please check your connection, or if you\'re behind a proxy, '
                'set it using the http_proxy environment variable or the proxy command.'
            )
            self.stop()
        except AlreadyRunningException:
            self.out('Failed to start MEO Cloud.')
            self.out('MEO Cloud was running already.')
        except (socket.timeout, ListenerConnectionFailedException, EOFError,
                KeyboardInterrupt):
            self.out('Failed to start MEO Cloud.')
            self.stop(quiet=True)
            raise

    def stop(self, quiet=False):
        log.debug('CLIHandler.stop()')
        stopped = False

        pid = self._daemon_already_running()
        if pid:
            try:
                log.info('CLIHandler.stop: gracefull daemon shutdown')
                self.daemon_client.shutdown()
                stopped = True
                # Sleep a bit to give time for the daemon and the core
                # to gracefully shut themselves
                time.sleep(2)
            except (ListenerConnectionFailedException, socket.timeout):
                pass

            if not stopped or self._daemon_already_running():
                # Connection to daemon_client might have been lost
                # or the gracefull kill wasn't enough.
                # Will try to kill using pid
                log.info('CLIHandler.stop: forcefull daemon shutdown')
                try:
                    os.kill(pid, signal.SIGTERM)
                except OSError:
                    # If we failed to kill it there's a good chance that the reason
                    # is that he's already dead
                    pass
                stopped = True

        pid = self._core_already_running()
        if pid:
            # Apparently we have a dangling core...
            # Kill it!
            log.info('CLIHandler.stop: forcefull core shutdown')
            try:
                os.kill(pid, signal.SIGTERM)
            except OSError:
                # If we failed to kill it there's a good chance that the reason
                # is that he's already dead
                pass
            stopped = True

        if not quiet:
            if stopped:
                self.out('MEO Cloud has been stopped.')
            else:
                self.out('MEO Cloud does not seem to be running.')
        return True

    @exceptions_handled
    @daemon_must_be_running
    def status(self):
        log.debug('CLIHandler.status()')
        status_result = self.daemon_client.status()
        status = status_result.status

        if status_result.persistentNotifs:
            for persistent_notif in status_result.persistentNotifs:
                self.out('WARNING: {0}'.format(persistent_notif))

        if status.totalQuota:
            percentage = float(status.usedQuota) / status.totalQuota
            used_quota_human_readable = human_readable_bytes(status.usedQuota)
            total_quota_human_readable = human_readable_bytes(
                status.totalQuota)
            self.out('Quota: {0:.2%} ({1} of {2})'.format(
                percentage, used_quota_human_readable,
                total_quota_human_readable))

        if status.state == State.READY:
            self.out('Status: IDLE')
        else:
            self.out('Status: {0}'.format(
                State._VALUES_TO_NAMES[status.state]))

        sync_status = status_result.syncStatus
        if sync_status:
            # TODO Handle sync_code
            log.debug('CLIHandler.status: sync_code: {0}'.format(
                sync_status.syncCode))
            if sync_status.pendingUploads:
                if sync_status.uploadRate:
                    self.out('Uploading {0} files, at {1} ({2} left)'.format(
                        sync_status.pendingUploads,
                        human_readable_bytes(sync_status.uploadRate,
                                             per_second=True),
                        human_readable_seconds(sync_status.uploadETASecs)))
                else:
                    self.out('Uploading {0} files'.format(
                        sync_status.pendingUploads))
            if sync_status.pendingDownloads:
                if sync_status.downloadRate:
                    self.out('Downloading {0} files, at {1} ({2} left)'.format(
                        sync_status.pendingDownloads,
                        human_readable_bytes(sync_status.downloadRate,
                                             per_second=True),
                        human_readable_seconds(sync_status.downloadETASecs)))
                else:
                    self.out('Downloading {0} files'.format(
                        sync_status.pendingDownloads))
            if sync_status.pendingIndexes:
                self.out('Indexing {0} files'.format(
                    sync_status.pendingIndexes))
        return True

    @exceptions_handled
    @daemon_must_be_running
    def notifications(self, n_lines):
        log.debug('CLIHandler.notifications()')
        try:
            with open(NOTIFICATIONS_LOG_PATH, 'r') as f:
                lines = tail(f, n_lines)
        except IOError:
            lines = None
        if lines:
            self.out('\n'.join(lines))
        else:
            self.out('You have no notifications')
        return True

    @exceptions_handled
    @daemon_must_be_running
    def list_sync(self):
        log.debug('CLIHandler.list_sync()')
        ignored_directories = self.daemon_client.ignoredDirectories()
        if not ignored_directories:
            self.out('All your directories are being synchronized.')
        else:
            self.out('All directories are being synchronized except these:')
            self.out('\n'.join(ignored_directories))
        return True

    @exceptions_handled
    @daemon_must_be_running
    def add_sync(self, path):
        log.debug('CLIHandler.add_sync({0})'.format(path))
        if path == '__ALL__':
            self.daemon_client.setIgnoredDirectories([])
            self.out(
                'Synchronization was resumed for every path whose synchronization was stopped.'
            )
        else:
            orig_path = path
            normalized_path = path.rstrip('/')
            if normalized_path == '':
                self.out(
                    '"/" is the root of your MEO Cloud folder whose synchronization '
                    'cannot be stopped and thus does not need to be resumed.')
            else:
                normalized_path = self.test_or_add_leading_slash(
                    normalized_path, orig_path)
                ignored_directories = self.daemon_client.ignoredDirectories()
                if normalized_path in ignored_directories:
                    if self.test_valid_path(normalized_path,
                                            orig_path,
                                            quiet=True):
                        self.out(
                            '"{0}" will now be added back to your MEO Cloud folder '
                            'and its synchronization will resume.'.format(
                                orig_path))
                    else:
                        self.out(
                            '"{0}" has been removed from the '
                            'stopped synchronization list'.format(orig_path))
                    ignored_directories.remove(normalized_path)
                    self.daemon_client.setIgnoredDirectories(
                        ignored_directories)
                elif self.test_valid_path(normalized_path, orig_path):
                    self.out(
                        'Nothing to do. "{0}" was already being synchronized.'.
                        format(orig_path))
        return True

    @exceptions_handled
    @daemon_must_be_running
    def remove_sync(self, path, force):
        log.debug('CLIHandler.remove_sync({0})'.format(path))
        orig_path = path
        normalized_path = path.rstrip('/')
        if normalized_path == '':
            self.out(
                'Cannot stop synchronizing the root of your MEO Cloud folder.')
        else:
            normalized_path = self.test_or_add_leading_slash(
                normalized_path, orig_path)
            ignored_directories = self.daemon_client.ignoredDirectories()
            if normalized_path in ignored_directories:
                self.out(
                    'Nothing to do. Synchronization of "{0}" was already stopped.'
                    .format(orig_path))
            elif force or self.test_valid_path(normalized_path, orig_path):
                ignored_directories.append(normalized_path)
                self.daemon_client.setIgnoredDirectories(ignored_directories)
                self.out(
                    '"{0}" was removed from the synced paths and will be removed '
                    'from your filesystem when syncing is done.'.format(
                        orig_path))
        return True

    def test_or_add_leading_slash(self, normalized_path, orig_path):
        # No leading slash
        if not normalized_path.startswith('/'):
            self.out('Your path did not start with a slash (/).')
            yes = self.ask_yes_no('Did you mean {0}'.format('/' + orig_path))
            if yes:
                normalized_path = '/' + normalized_path
        return normalized_path

    def test_valid_path(self, normalized_path, orig_path, quiet=False):
        if normalized_path.startswith('/'):
            parent_dir = os.path.dirname(normalized_path)
        else:
            parent_dir = '/'
        remote_directories = self.daemon_client.remoteDirectoryListing(
            parent_dir).listing

        if remote_directories is None:
            if quiet:
                return True
            self.out(
                'Could not contact the server to validate that path "{0}"'
                ' is a valid path in your MEO Cloud folder.'.format(orig_path))
            return self.ask_yes_no('Do you want to continue?')
        # TODO handle remoteDirectoryListing statusCode
        if normalized_path not in remote_directories:
            if quiet:
                return False
            self.out(
                'Path "{0}" is not a correct path in your MEO Cloud folder'.
                format(orig_path))
            if not remote_directories and parent_dir != '/':
                # Either the folder was empty or the parent folder was invalid.
                # Since there is no easy way of knowing what happened, we'll just list the examples for the root folder
                parent_dir = '/'
                remote_directories = self.daemon_client.remoteDirectoryListing(
                    parent_dir).listing
            if remote_directories:
                self.out('Here are some examples of correct paths:')
                for correct_path in remote_directories[:20]:
                    self.out(correct_path)
            elif parent_dir == '/':
                self.out(
                    'You have no folders in MEO Cloud, which means there is no correct path.'
                )
            else:
                assert False
            return False
        return True

    @exceptions_handled
    def proxy(self, proxy_url):
        log.debug('CLIHandler.proxy()')
        if proxy_url is None:
            current_proxy = get_proxy(self.ui_config)
            if current_proxy:
                self.out('Current Proxy: {0}'.format(current_proxy))
            else:
                self.out('No proxy in use.')
        else:
            if proxy_url == 'default':
                set_proxy(self.ui_config, None)
                self.out('Proxy settings reset.')
                current_proxy = get_proxy(self.ui_config)
                if current_proxy:
                    self.out(
                        'Current Proxy: {0} (from http_proxy or https_proxy environment variables)'
                        .format(current_proxy))
            else:
                set_proxy(self.ui_config, proxy_url)
                self.out('Proxy was set to: {0}'.format(proxy_url))
            if self._daemon_already_running():
                self.daemon_client.networkSettingsChanged()
        return True

    @exceptions_handled
    def ratelimit(self, direction, limit):
        log.debug('CLIHandler.ratelimit()')
        download_limit, upload_limit = get_ratelimits(self.ui_config)
        if direction is not None:
            if limit > 1000000:
                self.out('Limit cannot exceed 1000000 KB/s (1 GB/s).')
                return False
            if direction == 'up':
                upload_limit = limit
            elif direction == 'down':
                download_limit = limit
            else:
                self.out('First parameter must be \'up\' or \'down\'.')
                return False
            set_ratelimits(self.ui_config, download_limit, upload_limit)
            if self._daemon_already_running():
                self.daemon_client.networkSettingsChanged()
        if upload_limit == 0:
            self.out('Upload limit: N/A')
        else:
            self.out('Upload limit: {0} KB/s'.format(upload_limit))
        if download_limit == 0:
            self.out('Download limit: N/A')
        else:
            self.out('Download limit: {0} KB/s'.format(download_limit))
        return True

    @exceptions_handled
    @daemon_must_be_running
    def pause(self):
        log.debug('CLIHandler.pause()')
        self.daemon_client.pause()
        self.out(
            'MEO Cloud synchronization was stopped. Enter meocloud resume to restart sync.'
        )
        return True
        # TODO Warn if already paused

    @exceptions_handled
    @daemon_must_be_running
    def resume(self):
        log.debug('CLIHandler.resume()')
        self.daemon_client.unpause()
        self.out('MEO Cloud synchronization was resumed.')
        return True
        # TODO Warn if already unpaused

    @exceptions_handled
    @check_daemon_version
    def version(self):
        log.debug('CLIHandler.version()')
        try:
            core_version = self.daemon_client.coreVersion()
        except ListenerConnectionFailedException:
            self.out('{0}'.format(VERSION))
        else:
            self.out('{0} (core version: {1})'.format(VERSION, core_version))
        return True

    @exceptions_handled
    @daemon_must_be_running
    def unlink(self):
        log.debug('CLIHandler.unlink()')
        # Make sure user wants to unlink
        really_unlink = self.ask_yes_no(
            'Are you sure you want to unlink your account')

        # Actually unlink
        if really_unlink:
            unlink_success = self.daemon_client.unlink()
            if unlink_success:
                self.out('Successfully unlinked account.')
            else:
                self.out('No account to unlink.')
            self.stop()
        return True

    def ask(self, msg='', default=None, *args, **kwargs):
        if default:
            msg = msg + ' [{0}]? '.format(default)
        else:
            msg = msg + '? '
        self.out(msg, end='')
        answer = self.get_input()
        if not answer and default is not None:
            answer = default
        return answer

    def ask_yes_no(self, msg='', default_yes=False, *args, **kwargs):
        if default_yes:
            msg = msg + ' [n/Y]? '
            default = 'y'
        else:
            msg = msg + ' [N/y]? '
            default = 'n'
        while True:
            self.out(msg, end='')
            answer = self.get_input()
            if not answer:
                answer = default
            if answer.lower() == 'y':
                return True
            if answer.lower() == 'n':
                return False
            self.out(
                'Did not recognize answer "{0}". Please answer again.'.format(
                    answer))

    def get_input(self):
        if not sys.stdin.isatty():
            log.warning('CLI: Must get input but stdin is not a terminal.')
            self.out()
            self.out('ERROR: Must get input but stdin is not a terminal.')
            self.stop(quiet=True)
            self.out('Goodbye!')
            sys.exit(1)
        return input().strip()

    def out(self, msg='', *args, **kwargs):
        print_(msg, *args, **kwargs)