def stop(self): self.retries = -1 utils.cancel_timeout(self.reconnect_timeout) self.reconnect_timeout = None self.cleanup() msg.log('Disconnected.') sublime.status_message('Disconnected.')
def connect(self, cb=None): self.stop(False) self.empty_selects = 0 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) if self.secure: if ssl: cert_path = os.path.join(G.COLAB_DIR, "startssl-ca.pem") with open(cert_path, "wb") as cert_fd: cert_fd.write(cert.CA_CERT.encode("utf-8")) self.sock = ssl.wrap_socket(self.sock, ca_certs=cert_path, cert_reqs=ssl.CERT_REQUIRED) else: msg.log("No SSL module found. Connection will not be encrypted.") if self.port == G.DEFAULT_PORT: self.port = 3148 # plaintext port msg.debug("Connecting to %s:%s" % (self.host, self.port)) try: self.sock.connect((self.host, self.port)) if self.secure and ssl: self.sock.do_handshake() except socket.error as e: msg.error("Error connecting:", e) self.reconnect() return self.sock.setblocking(0) msg.debug("Connected!") self.reconnect_delay = self.INITIAL_RECONNECT_DELAY self.send_auth() if cb: cb()
def delete_buf(self, path, unlink=False): if not utils.is_shared(path): msg.error('Skipping deleting ', path, ' because it is not in shared path ', G.PROJECT_PATH, '.') return if os.path.isdir(path): for dirpath, dirnames, filenames in os.walk(path): # TODO: rexamine this assumption # Don't care about hidden stuff dirnames[:] = [d for d in dirnames if d[0] != '.'] for f in filenames: f_path = os.path.join(dirpath, f) if f[0] == '.': msg.log('Not deleting buf for hidden file ', f_path) else: self.delete_buf(f_path, unlink) return buf_to_delete = self.get_buf_by_path(path) if buf_to_delete is None: msg.error(path, ' is not in this workspace') return msg.log('deleting buffer ', utils.to_rel_path(path)) event = { 'name': 'delete_buf', 'id': buf_to_delete['id'], 'unlink': unlink, } self.send(event)
def delete_buf(path): if not utils.is_shared(path): msg.error('Skipping deleting %s because it is not in shared path %s.' % (path, G.PROJECT_PATH)) return if os.path.isdir(path): for dirpath, dirnames, filenames in os.walk(path): # TODO: rexamine this assumption # Don't care about hidden stuff dirnames[:] = [d for d in dirnames if d[0] != '.'] for f in filenames: f_path = os.path.join(dirpath, f) if f[0] == '.': msg.log('Not deleting buf for hidden file %s' % f_path) else: Listener.delete_buf(f_path) return buf_to_delete = get_buf_by_path(path) if buf_to_delete is None: msg.error('%s is not in this workspace' % path) return msg.log('deleting buffer ', utils.to_rel_path(path)) event = { 'name': 'delete_buf', 'id': buf_to_delete['id'], } G.AGENT.put(event)
def conn_log(action, item): try: item = item.decode('utf-8') except Exception: pass msg.log('%s: %s' % (action, item)) sys.stdout.flush()
def on_highlight(self, data): # floobits.highlight(data['id'], region_key, data['username'], data['ranges'], data.get('ping', False)) #buf_id, region_key, username, ranges, ping=False): ping = data.get('ping', False) if self.follow_mode: ping = True buf = self.FLOO_BUFS[data['id']] view = self.get_view(data['id']) if not view: if not ping: return view = self.create_view(buf) if not view: return if ping: try: offset = data['ranges'][0][0] except IndexError as e: msg.debug('could not get offset from range %s' % e) else: msg.log('You have been summoned by %s' % (data.get('username', 'an unknown user'))) view.focus() view.set_cursor_position(offset) if G.SHOW_HIGHLIGHTS: view.highlight(data['ranges'], data['user_id'])
def delete_buf(self, path): """deletes a path""" if not path: return path = utils.get_full_path(path) if not self.is_shared(path): msg.error('Skipping deleting %s because it is not in shared path %s.' % (path, G.PROJECT_PATH)) return if os.path.isdir(path): for dirpath, dirnames, filenames in os.walk(path): # Don't care about hidden stuff dirnames[:] = [d for d in dirnames if d[0] != '.'] for f in filenames: f_path = os.path.join(dirpath, f) if f[0] == '.': msg.log('Not deleting buf for hidden file %s' % f_path) else: self.delete_buf(f_path) return buf_to_delete = None rel_path = utils.to_rel_path(path) buf_to_delete = self.get_buf_by_path(rel_path) if buf_to_delete is None: msg.error('%s is not in this workspace' % path) return msg.log('deleting buffer ', rel_path) event = { 'name': 'delete_buf', 'id': buf_to_delete['id'], } self.agent.put(event)
def _on_delete_buf(self, req): buf = self.get_buf_by_path(req["path"]) if not buf: msg.debug("No buffer for path %s" % req["path"]) return msg.log("deleting buffer ", buf["path"]) self.send_to_floobits({"name": "delete_buf", "id": buf["id"]})
def _on_highlight(self, data): buf_id = data['id'] user_id = data['user_id'] username = data.get('username', 'an unknown user') ping = G.STALKER_MODE or data.get('ping', False) previous_highlight = self.user_highlights.get(user_id) buf = self.bufs[buf_id] view = self.get_view(buf_id) if not view: if not ping: return view = self.create_view(buf) if not view: return data['path'] = buf['path'] self.user_highlights[user_id] = data if ping: try: offset = data['ranges'][0][0] except IndexError as e: msg.debug('could not get offset from range %s' % e) else: if data.get('ping'): msg.log('You have been summoned by %s' % (username)) view.focus() view.set_cursor_position(offset) if G.SHOW_HIGHLIGHTS: if previous_highlight and previous_highlight['id'] == data['id']: view.clear_highlight(user_id) view.highlight(data['ranges'], user_id)
def conn_log(action, item): try: item = item.decode('utf-8') except Exception: pass if G.SOCK_DEBUG: msg.log(action, ': ', item) sys.stdout.flush()
def toggle_highlights(self): G.SHOW_HIGHLIGHTS = not G.SHOW_HIGHLIGHTS if G.SHOW_HIGHLIGHTS: self.buf_enter() msg.log('Highlights enabled') return self.clear() msg.log('Highlights disabled')
def complete_signup(self): if not self.start_ticker(): return msg.debug('Completing signup.') if not utils.has_browser(): msg.log('You need a modern browser to complete the sign up. Go to https://floobits.com to sign up.') return VUI.pinocchio()
def on_auth(self): self.authed = True G.JOINED_WORKSPACE = True self.retries = self.MAX_RETRIES msg.log('Successfully joined workspace %s/%s' % (self.owner, self.workspace)) if self._on_auth: self._on_auth(self) self._on_auth = None
def check_credentials(): msg.debug('Print checking credentials.') if utils.can_auth(): return if not utils.has_browser(): msg.log('You need a Floobits account to use the Floobits plugin. Go to https://floobits.com to sign up.') return yield VUI.create_or_link_account, None, G.DEFAULT_HOST, False
def update_view(self, buf, view=None): msg.debug('updating view for buf %s' % buf['id']) view = view or self.get_view(buf['id']) if not view: msg.log('view for buf %s not found. not updating' % buf['id']) return self.MODIFIED_EVENTS.put(1) view.set_text(buf['buf'])
def save(self): # TODO: switch to the correct buffer, then save, then switch back (or use writefile) if vim.current.buffer.name != self.vim_buf.name: return try: vim.command('silent w!') except Exception as e: msg.log('Error saving %s: %s' % (self.vim_buf.name, str(e)))
def on_delete_buf(self, data): # TODO: somehow tell the user about this. maybe delete on disk too? del self.FLOO_BUFS[data['id']] path = utils.get_full_path(data['path']) if not G.DELETE_LOCAL_FILES: msg.log('Not deleting %s because delete_local_files is disabled' % path) return utils.rm(path) msg.warn('deleted %s because %s told me to.' % (path, data.get('username', 'the internet')))
def info(self): kwargs = { 'cs': bool(int(vim.eval('has("clientserver")'))), 'servername': vim.eval('v:servername'), 'updatetime': vim.eval('&l:updatetime'), 'version': G.__PLUGIN_VERSION__, } msg.log(FLOOBITS_INFO.format(**kwargs))
def _on_delete_buf(self, req): buf = self.get_buf_by_path(req['path']) if not buf: msg.debug('No buffer for path %s' % req['path']) return msg.log('deleting buffer ', buf['path']) self.send_to_floobits({ 'name': 'delete_buf', 'id': buf['id'], })
def on_window_command(self, window, command, *args, **kwargs): if command == 'rename_path': # User is about to rename something msg.debug('rename') if window == G.WORKSPACE_WINDOW and command == 'close_window': msg.log('Workspace window closed, disconnecting.') try: window.run_command('floobits_leave_workspace') except Exception as e: msg.error(e)
def log_users(self): clients = [] try: clients = ['%s on %s' % (x.get('username'), x.get('client')) for x in self.workspace_info['users'].values()] except Exception as e: print(e) msg.log(len(clients), ' connected clients:') clients.sort() for client in clients: msg.log(client)
def highlight(self, data=None, user=None): if user: data = self.last_highlight_by_user.get(user) elif not data: data = data or self.last_highlight if not data: msg.log('No recent highlight to replay.') return self._on_highlight(data)
def on_emacs_delete_buf(self, req): buf = self.get_buf_by_path(req['path']) if not buf: msg.debug('No buffer for path %s' % req['path']) return msg.log('deleting buffer ', buf['path']) event = { 'name': 'delete_buf', 'id': buf['id'], } self.agent.put(event)
def reconnect(self): if self.reconnect_timeout: return self.cleanup() self.reconnect_delay = min(10000, int(1.5 * self.reconnect_delay)) if self.retries > 0: msg.log('Floobits: Reconnecting in %sms' % self.reconnect_delay) self.reconnect_timeout = utils.set_timeout(self.connect, self.reconnect_delay) elif self.retries == 0: sublime.error_message('Floobits Error! Too many reconnect failures. Giving up.') self.retries -= 1
def upload(path): try: with open(path, 'rb') as buf_fd: buf = buf_fd.read() encoding = 'utf8' rel_path = utils.to_rel_path(path) existing_buf = get_buf_by_path(path) if existing_buf: buf_md5 = hashlib.md5(buf).hexdigest() if existing_buf['md5'] == buf_md5: msg.debug('%s already exists and has the same md5. Skipping.' % path) return msg.log('setting buffer ', rel_path) existing_buf['buf'] = buf existing_buf['md5'] = buf_md5 try: buf = buf.decode('utf-8') except Exception: buf = base64.b64encode(buf).decode('utf-8') encoding = 'base64' existing_buf['encoding'] = encoding G.AGENT.put({ 'name': 'set_buf', 'id': existing_buf['id'], 'buf': buf, 'md5': buf_md5, 'encoding': encoding, }) return try: buf = buf.decode('utf-8') except Exception: buf = base64.b64encode(buf).decode('utf-8') encoding = 'base64' msg.log('creating buffer ', rel_path) event = { 'name': 'create_buf', 'buf': buf, 'path': rel_path, 'encoding': encoding, } G.AGENT.put(event) except (IOError, OSError): msg.error('Failed to open %s.' % path) except Exception as e: msg.error('Failed to create buffer %s: %s' % (path, unicode(e)))
def update_view(buf, view): msg.log('Floobits synced data for consistency: %s' % buf['path']) G.VIEW_TO_HASH[view.buffer_id()] = buf['md5'] view.set_read_only(False) try: view.run_command('floo_view_replace_region', {'r': [0, view.size()], 'data': buf['buf']}) view.set_status('Floobits', 'Floobits synced data for consistency.') utils.set_timeout(lambda: view.set_status('Floobits', ''), 5000) except Exception as e: msg.error('Exception updating view: %s' % e) if 'patch' not in G.PERMS: view.set_status('Floobits', 'You don\'t have write permission. Buffer is read-only.') view.set_read_only(True)
def rename_buf(self, buf_id, new_path): new_path = utils.to_rel_path(new_path) if not utils.is_shared(new_path): msg.log('New path %s is not shared. Discarding rename event.' % new_path) return self.agent.put({ 'name': 'rename_buf', 'id': buf_id, 'path': new_path, }) old_path = self.FLOO_BUFS[buf_id]['path'] del self.FLOO_PATHS_TO_BUFS[old_path] self.FLOO_PATHS_TO_BUFS[new_path] = buf_id self.FLOO_BUFS[buf_id]['path'] = new_path
def stop(self, log=True): if log: msg.log('Disconnecting from workspace %s/%s' % (self.owner, self.workspace)) utils.cancel_timeout(self.reconnect_timeout) self.reconnect_timeout = None try: self.retries = -1 self.sock.shutdown(2) self.sock.close() except Exception: return False if log: msg.log('Disconnected.') return True
def update(self, data): buf = self.buf = data msg.log('Floobits synced data for consistency: %s' % buf['path']) G.VIEW_TO_HASH[self.view.buffer_id()] = buf['md5'] self.view.set_read_only(False) try: self.view.run_command('floo_view_replace_region', {'r': [0, self.view.size()], 'data': buf['buf']}) self.set_status('Floobits synced data for consistency.') utils.set_timeout(self.set_status, 5000, '') except Exception as e: msg.error('Exception updating view: %s' % e) if 'patch' not in G.PERMS: self.set_status('You don\'t have write permission. Buffer is read-only.') self.view.set_read_only(True)
def _on_create_workspace(self, data, workspace_name, dir_to_share, owner=None, perms=None): owner = owner or G.USERNAME workspace_name = data.get('response', workspace_name) try: api_args = { 'name': workspace_name, 'owner': owner, } if perms: api_args['perms'] = perms msg.debug(str(api_args)) r = api.create_workspace(api_args) except Exception as e: msg.error('Unable to create workspace: %s' % unicode(e)) return editor.error_message('Unable to create workspace: %s' % unicode(e)) workspace_url = 'https://%s/%s/%s' % (G.DEFAULT_HOST, owner, workspace_name) if r.code < 400: msg.log('Created workspace %s' % workspace_url) utils.add_workspace_to_persistent_json(owner, workspace_name, workspace_url, dir_to_share) G.PROJECT_PATH = dir_to_share agent = self.remote_connect(owner, workspace_name, False) return agent.once("room_info", lambda: agent.upload(dir_to_share)) msg.error('Unable to create workspace: %s' % r.body) if r.code not in [400, 402, 409]: try: r.body = r.body['detail'] except Exception: pass return editor.error_message('Unable to create workspace: %s' % r.body) if r.code == 400: workspace_name = re.sub('[^A-Za-z0-9_\-\.]', '-', workspace_name) prompt = 'Invalid name. Workspace names must match the regex [A-Za-z0-9_\-\.]. Choose another name:' elif r.code == 402: try: r.body = r.body['detail'] except Exception: pass cb = lambda data: data['response'] and webbrowser.open('https://%s/%s/settings#billing' % (G.DEFAULT_HOST, owner)) self.get_input('%s Open billing settings?' % r.body, '', cb, y_or_n=True) return else: prompt = 'Workspace %s/%s already exists. Choose another name:' % (owner, workspace_name) return self.get_input(prompt, workspace_name, self._on_create_workspace, workspace_name, dir_to_share, owner, perms)
def send_msg(self, text): self.send({'name': 'msg', 'data': text}) timestamp = time.time() msgText = self.format_msg(text, self.username, timestamp) msg.log(msgText) self.chat_deck.appendleft(msgText)
def tick(self): try: self.ticker() except Exception as e: msg.log("Event loop tick error: %s" % e)
def on_part(self, data): msg.log('%s left the workspace' % data['username']) region_key = 'floobits-highlight-%s' % (data['user_id']) for window in sublime.windows(): for view in window.views(): view.erase_regions(region_key)
def stop(self): self.retries = -1 utils.cancel_timeout(self.reconnect_timeout) self.reconnect_timeout = None self.cleanup() msg.log('Disconnected.')
def stomp_prompt(self, changed_bufs, missing_bufs, new_files, ignored, cb): if not G.EXPERT_MODE: editor.message_dialog( 'Your copy of %s/%s is out of sync. ' 'You will be prompted after you close this dialog.' % (self.owner, self.workspace)) def pluralize(arg): return arg != 1 and 's' or '' overwrite_local = '' overwrite_remote = '' missing = [buf['path'] for buf in missing_bufs] changed = [buf['path'] for buf in changed_bufs] to_upload = set(new_files + changed).difference(set(ignored)) to_remove = missing + ignored to_fetch = changed + missing to_upload_len = len(to_upload) to_remove_len = len(to_remove) remote_len = to_remove_len + to_upload_len to_fetch_len = len(to_fetch) msg.log('To fetch: %s' % ', '.join(to_fetch)) msg.log('To upload: %s' % ', '.join(to_upload)) msg.log('To remove: %s' % ', '.join(to_remove)) if not to_fetch: overwrite_local = 'Fetch nothing' elif to_fetch_len < 5: overwrite_local = 'Fetch %s' % ', '.join(to_fetch) else: overwrite_local = 'Fetch %s file%s' % (to_fetch_len, pluralize(to_fetch_len)) if to_upload_len < 5: to_upload_str = 'upload %s' % ', '.join(to_upload) else: to_upload_str = 'upload %s' % to_upload_len if to_remove_len < 5: to_remove_str = 'remove %s' % ', '.join(to_remove) else: to_remove_str = 'remove %s' % to_remove_len if to_upload: overwrite_remote += to_upload_str if to_remove: overwrite_remote += ' and ' if to_remove: overwrite_remote += to_remove_str if remote_len >= 5 and overwrite_remote: overwrite_remote += ' files' overwrite_remote = overwrite_remote.capitalize() action = 'Overwrite' # TODO: change action based on numbers of stuff opts = [ [ '%s %s remote file%s.' % (action, remote_len, pluralize(remote_len)), overwrite_remote ], [ '%s %s local file%s.' % (action, to_fetch_len, pluralize(to_fetch_len)), overwrite_local ], ['Cancel', 'Disconnect and resolve conflict manually.'], ] # TODO: sublime text doesn't let us focus a window. so use the active window. super lame # G.WORKSPACE_WINDOW.show_quick_panel(opts, cb) w = sublime.active_window() or G.WORKSPACE_WINDOW w.show_quick_panel(opts, cb)
def _on_create_workspace(self, data, workspace_name, dir_to_share, owner=None, perms=None): owner = owner or G.USERNAME workspace_name = data.get('response', workspace_name) try: api_args = { 'name': workspace_name, 'owner': owner, } if perms: api_args['perms'] = perms msg.debug(str(api_args)) r = api.create_workspace(api_args) except Exception as e: msg.error('Unable to create workspace: %s' % unicode(e)) return editor.error_message('Unable to create workspace: %s' % unicode(e)) workspace_url = 'https://%s/%s/%s' % (G.DEFAULT_HOST, owner, workspace_name) if r.code < 400: msg.log('Created workspace %s' % workspace_url) utils.add_workspace_to_persistent_json(owner, workspace_name, workspace_url, dir_to_share) G.PROJECT_PATH = dir_to_share agent = self.remote_connect(owner, workspace_name, False) return agent.once("room_info", lambda: agent.upload(dir_to_share)) msg.error('Unable to create workspace: %s' % r.body) if r.code not in [400, 402, 409]: try: r.body = r.body['detail'] except Exception: pass return editor.error_message('Unable to create workspace: %s' % r.body) if r.code == 400: workspace_name = re.sub('[^A-Za-z0-9_\-\.]', '-', workspace_name) prompt = 'Invalid name. Workspace names must match the regex [A-Za-z0-9_\-\.]. Choose another name:' elif r.code == 402: try: r.body = r.body['detail'] except Exception: pass cb = lambda data: data['response'] and webbrowser.open( 'https://%s/%s/settings#billing' % (G.DEFAULT_HOST, owner)) self.get_input('%s Open billing settings?' % r.body, '', cb, y_or_n=True) return else: prompt = 'Workspace %s/%s already exists. Choose another name:' % ( owner, workspace_name) return self.get_input(prompt, workspace_name, self._on_create_workspace, workspace_name, dir_to_share, owner, perms)
def _on_set_follow_mode(self, req): msg.log('follow mode is %s' % ((req.get('follow_mode') and 'enabled') or 'disabled'))
def on_connect(self): msg.log("have an emacs!")
def stomp_prompt(self, changed_bufs, missing_bufs, new_files, ignored, cb): if not (G.EXPERT_MODE or hasattr(sublime, 'KEEP_OPEN_ON_FOCUS_LOST')): editor.message_dialog( 'Your copy of %s/%s is out of sync. ' 'You will be prompted after you close this dialog.' % (self.owner, self.workspace)) def pluralize(arg): return arg != 1 and 's' or '' overwrite_local = '' overwrite_remote = '' missing = [buf['path'] for buf in missing_bufs] changed = [buf['path'] for buf in changed_bufs] to_remove = set(missing + ignored) to_upload = set(new_files + changed).difference(to_remove) to_fetch = changed + missing to_upload_len = len(to_upload) to_remove_len = len(to_remove) remote_len = to_remove_len + to_upload_len to_fetch_len = len(to_fetch) msg.log('To fetch: ', ', '.join(to_fetch)) msg.log('To upload: ', ', '.join(to_upload)) msg.log('To remove: ', ', '.join(to_remove)) if not to_fetch: overwrite_local = 'Fetch nothing' elif to_fetch_len < 5: overwrite_local = 'Fetch %s' % ', '.join(to_fetch) else: overwrite_local = 'Fetch %s file%s' % (to_fetch_len, pluralize(to_fetch_len)) if to_upload_len < 5: to_upload_str = 'Upload %s' % ', '.join(to_upload) else: to_upload_str = 'Upload %s' % to_upload_len if to_remove_len < 5: to_remove_str = 'remove %s' % ', '.join(to_remove) else: to_remove_str = 'remove %s' % to_remove_len if to_upload: overwrite_remote += to_upload_str if to_remove: overwrite_remote += ' and ' if to_remove: overwrite_remote += to_remove_str if remote_len >= 5 and overwrite_remote: overwrite_remote += ' files' # Be fancy and capitalize "remove" if it's the first thing in the string if len(overwrite_remote) > 0: overwrite_remote = overwrite_remote[0].upper( ) + overwrite_remote[1:] connected_users_msg = '' def filter_user(u): if u.get('is_anon'): return False if 'patch' not in u.get('perms'): return False if u.get('username') == self.username: return False return True users = set([ v['username'] for k, v in self.workspace_info['users'].items() if filter_user(v) ]) if users: if len(users) < 4: connected_users_msg = ' Connected: ' + ', '.join(users) else: connected_users_msg = ' %s users connected' % len(users) # TODO: change action based on numbers of stuff action = 'Overwrite' opts = [ [ '%s %s remote file%s.' % (action, remote_len, pluralize(remote_len)), overwrite_remote ], [ '%s %s local file%s.' % (action, to_fetch_len, pluralize(to_fetch_len)), overwrite_local ], ['Cancel', 'Disconnect.' + connected_users_msg], ] w = sublime.active_window() or G.WORKSPACE_WINDOW flags = 0 if hasattr(sublime, 'KEEP_OPEN_ON_FOCUS_LOST'): flags |= sublime.KEEP_OPEN_ON_FOCUS_LOST w.show_quick_panel(opts, cb, flags)
def on_msg(self, data): timestamp = data.get('time') or time.time() msg.log('[%s] <%s> %s' % (time.ctime(timestamp), data.get('username', ''), data.get('data', '')))
def on_msg(self, data): timestamp = data.get('time') or time.time() msgText = self.format_msg(data.get('data', ''), data.get('username', ''), timestamp) msg.log(msgText) self.chat_deck.appendleft(msgText)
def stop(self): stop_msg = 'Disconnecting from workspace %s/%s' % (self.owner, self.workspace) msg.log(stop_msg) sublime.status_message(stop_msg) super(AgentConnection, self).stop()
def on_connect(self): msg.log('Remote connection estabished.') eventStream.emit('remote_conn')
def run(self): msg.log("Starting event loop.") while True: sleep(0.1) self.vim.session.threadsafe_call(self.tick)
def apply_patch(patch_data): if not G.AGENT: msg.debug('Not connected. Discarding view change.') return buf_id = patch_data['id'] buf = BUFS[buf_id] if 'buf' not in buf: msg.debug('buf %s not populated yet. not patching' % buf['path']) return if buf['encoding'] == 'base64': # TODO apply binary patches return Listener.get_buf(buf_id, None) view = get_view(buf_id) if len(patch_data['patch']) == 0: msg.error('wtf? no patches to apply. server is being stupid') return msg.debug('patch is', patch_data['patch']) dmp_patches = DMP.patch_fromText(patch_data['patch']) # TODO: run this in a separate thread old_text = buf['buf'] if view and not view.is_loading(): view_text = get_text(view) if old_text == view_text: buf['forced_patch'] = False elif not buf.get('forced_patch'): patch = utils.FlooPatch(get_text(view), buf) # Update the current copy of the buffer buf['buf'] = patch.current buf['md5'] = hashlib.md5( patch.current.encode('utf-8')).hexdigest() buf['forced_patch'] = True msg.debug('forcing patch for %s' % buf['path']) G.AGENT.put(patch.to_json()) old_text = view_text else: msg.debug( 'forced patch is true. not sending another patch for buf %s' % buf['path']) md5_before = hashlib.md5(old_text.encode('utf-8')).hexdigest() if md5_before != patch_data['md5_before']: msg.warn('starting md5s don\'t match for %s. this is dangerous!' % buf['path']) t = DMP.patch_apply(dmp_patches, old_text) clean_patch = True for applied_patch in t[1]: if not applied_patch: clean_patch = False break if G.DEBUG: if len(t[0]) == 0: try: msg.debug('OMG EMPTY!') msg.debug('Starting data:', buf['buf']) msg.debug('Patch:', patch_data['patch']) except Exception as e: print(e) if '\x01' in t[0]: msg.debug('FOUND CRAZY BYTE IN BUFFER') msg.debug('Starting data:', buf['buf']) msg.debug('Patch:', patch_data['patch']) timeout_id = buf.get('timeout_id') if timeout_id: utils.cancel_timeout(timeout_id) if not clean_patch: msg.log('Couldn\'t patch %s cleanly.' % buf['path']) return Listener.get_buf(buf_id, view) cur_hash = hashlib.md5(t[0].encode('utf-8')).hexdigest() if cur_hash != patch_data['md5_after']: buf['timeout_id'] = utils.set_timeout(Listener.get_buf, 2000, buf_id, view) buf['buf'] = t[0] buf['md5'] = cur_hash if not view: save_buf(buf) return regions = [] commands = [] for patch in t[2]: offset = patch[0] length = patch[1] patch_text = patch[2] region = sublime.Region(offset, offset + length) regions.append(region) commands.append({ 'r': [offset, offset + length], 'data': patch_text }) view.run_command('floo_view_replace_regions', {'commands': commands}) region_key = 'floobits-patch-' + patch_data['username'] view.add_regions(region_key, regions, 'floobits.patch', 'circle', sublime.DRAW_OUTLINED) utils.set_timeout(view.erase_regions, 2000, region_key) view.set_status( 'Floobits', 'Changed by %s at %s' % (patch_data['username'], datetime.now().strftime('%H:%M')))
def highlight(self, data=None): data = data or self.last_highlight if not data: msg.log('No recent highlight to replay.') return self._on_highlight(data)
def follow(self, follow_mode=None): if follow_mode is None: follow_mode = not self.follow_mode self.follow_mode = follow_mode msg.log('follow mode is %s' % {True: 'enabled', False: 'disabled'}[self.follow_mode])
def stop(self): msg.log('Disconnecting from workspace %s/%s' % (self.owner, self.workspace)) super(AgentConnection, self).stop()
def on_post_save(self, view, agent): view_buf_id = view.buffer_id() def cleanup(): i = self.between_save_events[view_buf_id] i[0] -= 1 if view.is_scratch(): return i = self.between_save_events[view_buf_id] if agent.ignored_saves[view_buf_id] > 0: agent.ignored_saves[view_buf_id] -= 1 return cleanup() old_name = i[1] i = self.between_save_events[view_buf_id] if i[0] > 1: return cleanup() old_name = i[1] event = None buf = get_buf(view) try: name = utils.to_rel_path(view.file_name()) except ValueError: name = view.file_name() is_shared = utils.is_shared(view.file_name()) if buf is None: if not is_shared: return cleanup() if G.IGNORE and G.IGNORE.is_ignored(view.file_name(), log=True): msg.log(view.file_name(), ' is ignored. Not creating buffer.') return cleanup() msg.log('Creating new buffer ', name, view.file_name()) event = { 'name': 'create_buf', 'buf': get_text(view), 'path': name } elif name != old_name: if is_shared: msg.log('renamed buffer ', old_name, ' to ', name) event = { 'name': 'rename_buf', 'id': buf['id'], 'path': name } else: msg.log('deleting buffer from shared: ', name) event = { 'name': 'delete_buf', 'id': buf['id'], } if event: agent.send(event) if is_shared and buf: agent.views_changed.append(('saved', view, buf)) cleanup()
def sock_debug(*args, **kwargs): if G.SOCK_DEBUG: msg.log(*args, **kwargs)
def _on_set_follow_mode(self, req): G.FOLLOW_USERS = set() msg.log('follow mode is %s' % ((req.get('follow_mode') and 'enabled') or 'disabled'))
def create_buf(self, path, ig=None, force=False): if G.SPARSE_MODE and not force: msg.debug("Skipping %s because user enabled sparse mode." % path) return if not utils.is_shared(path): msg.error('Skipping adding %s because it is not in shared path %s.' % (path, G.PROJECT_PATH)) return if os.path.islink(path): msg.error('Skipping adding %s because it is a symlink.' % path) return ignored = ig and ig.is_ignored(path) if ignored: msg.log('Not creating buf: %s' % (ignored)) return msg.debug('create_buf: path is %s' % path) if os.path.isdir(path): if ig is None: try: ig = ignore.build_ignores(path) except Exception as e: msg.error('Error adding %s: %s' % (path, unicode(e))) return try: paths = os.listdir(path) except Exception as e: msg.error('Error listing path %s: %s' % (path, unicode(e))) return for p in paths: p_path = os.path.join(path, p) if p[0] == '.': if p not in ignore.HIDDEN_WHITELIST: msg.log('Not creating buf for hidden path %s' % p_path) continue ignored = ig.is_ignored(p_path) if ignored: msg.log('Not creating buf: %s' % (ignored)) continue try: s = os.lstat(p_path) except Exception as e: msg.error('Error lstat()ing path %s: %s' % (path, unicode(e))) continue if stat.S_ISDIR(s.st_mode): child_ig = ignore.Ignore(ig, p_path) utils.set_timeout(self.create_buf, 0, p_path, child_ig) elif stat.S_ISREG(s.st_mode): utils.set_timeout(self.create_buf, 0, p_path, ig) return try: buf_fd = open(path, 'rb') buf = buf_fd.read() encoding = 'utf8' rel_path = utils.to_rel_path(path) existing_buf = self.get_buf_by_path(path) if existing_buf and existing_buf['md5'] == hashlib.md5(buf).hexdigest(): msg.debug('%s already exists and has the same md5. Skipping creating.' % path) return try: buf = buf.decode('utf-8') except Exception: buf = base64.b64encode(buf).decode('utf-8') encoding = 'base64' msg.log('creating buffer ', rel_path) event = { 'name': 'create_buf', 'buf': buf, 'path': rel_path, 'encoding': encoding, } self.agent.put(event) except (IOError, OSError): msg.error('Failed to open %s.' % path) except Exception as e: msg.error('Failed to create buffer %s: %s' % (path, unicode(e)))
def handler(self, name, data): if name == 'patch': Listener.apply_patch(data) elif name == 'get_buf': buf_id = data['id'] buf = listener.BUFS.get(buf_id) if not buf: return msg.warn( 'no buf found: %s. Hopefully you didn\'t need that' % data) timeout_id = buf.get('timeout_id') if timeout_id: utils.cancel_timeout(timeout_id) if data['encoding'] == 'base64': data['buf'] = base64.b64decode(data['buf']) # forced_patch doesn't exist in data, so this is equivalent to buf['forced_patch'] = False listener.BUFS[buf_id] = data view = listener.get_view(buf_id) if view: Listener.update_view(data, view) else: listener.save_buf(data) elif name == 'create_buf': if data['encoding'] == 'base64': data['buf'] = base64.b64decode(data['buf']) listener.BUFS[data['id']] = data listener.PATHS_TO_IDS[data['path']] = data['id'] listener.save_buf(data) cb = listener.CREATE_BUF_CBS.get(data['path']) if cb: del listener.CREATE_BUF_CBS[data['path']] try: cb(data['id']) except Exception as e: print(e) elif name == 'rename_buf': del listener.PATHS_TO_IDS[data['old_path']] listener.PATHS_TO_IDS[data['path']] = data['id'] new = utils.get_full_path(data['path']) old = utils.get_full_path(data['old_path']) new_dir = os.path.split(new)[0] if new_dir: utils.mkdir(new_dir) os.rename(old, new) view = listener.get_view(data['id']) if view: view.retarget(new) listener.BUFS[data['id']]['path'] = data['path'] elif name == 'delete_buf': path = utils.get_full_path(data['path']) listener.delete_buf(data['id']) try: utils.rm(path) except Exception: pass user_id = data.get('user_id') username = self.get_username_by_id(user_id) msg.log('%s deleted %s' % (username, path)) elif name == 'room_info': Listener.reset() G.JOINED_WORKSPACE = True # Success! Reset counter self.reconnect_delay = self.INITIAL_RECONNECT_DELAY self.retries = self.MAX_RETRIES self.workspace_info = data G.PERMS = data['perms'] if 'patch' not in data['perms']: msg.log('No patch permission. Setting buffers to read-only') if sublime.ok_cancel_dialog( 'You don\'t have permission to edit this workspace. All files will be read-only.\n\nDo you want to request edit permission?' ): self.put({'name': 'request_perms', 'perms': ['edit_room']}) project_json = {'folders': [{'path': G.PROJECT_PATH}]} utils.mkdir(G.PROJECT_PATH) with open(os.path.join(G.PROJECT_PATH, '.sublime-project'), 'wb') as project_fd: project_fd.write( json.dumps(project_json, indent=4, sort_keys=True).encode('utf-8')) floo_json = { 'url': utils.to_workspace_url({ 'host': self.host, 'owner': self.owner, 'port': self.port, 'workspace': self.workspace, 'secure': self.secure, }) } with open(os.path.join(G.PROJECT_PATH, '.floo'), 'w') as floo_fd: floo_fd.write(json.dumps(floo_json, indent=4, sort_keys=True)) for buf_id, buf in data['bufs'].items(): buf_id = int(buf_id) # json keys must be strings buf_path = utils.get_full_path(buf['path']) new_dir = os.path.dirname(buf_path) utils.mkdir(new_dir) listener.BUFS[buf_id] = buf listener.PATHS_TO_IDS[buf['path']] = buf_id # TODO: stupidly inefficient view = listener.get_view(buf_id) if view and not view.is_loading( ) and buf['encoding'] == 'utf8': view_text = listener.get_text(view) view_md5 = hashlib.md5( view_text.encode('utf-8')).hexdigest() if view_md5 == buf['md5']: msg.debug( 'md5 sum matches view. not getting buffer %s' % buf['path']) buf['buf'] = view_text G.VIEW_TO_HASH[view.buffer_id()] = view_md5 elif self.get_bufs: Listener.get_buf(buf_id) #TODO: maybe send patch here? else: try: buf_fd = open(buf_path, 'rb') buf_buf = buf_fd.read() md5 = hashlib.md5(buf_buf).hexdigest() if md5 == buf['md5']: msg.debug( 'md5 sum matches. not getting buffer %s' % buf['path']) if buf['encoding'] == 'utf8': buf_buf = buf_buf.decode('utf-8') buf['buf'] = buf_buf elif self.get_bufs: Listener.get_buf(buf_id) except Exception as e: msg.debug('Error calculating md5:', e) Listener.get_buf(buf_id) msg.log('Successfully joined workspace %s/%s' % (self.owner, self.workspace)) temp_data = data.get('temp_data', {}) hangout = temp_data.get('hangout', {}) hangout_url = hangout.get('url') if hangout_url: self.prompt_join_hangout(hangout_url) if self.on_room_info: self.on_room_info() self.on_room_info = None elif name == 'user_info': user_id = str(data['user_id']) user_info = data['user_info'] self.workspace_info['users'][user_id] = user_info if user_id == str(self.workspace_info['user_id']): G.PERMS = user_info['perms'] elif name == 'join': msg.log('%s joined the workspace' % data['username']) user_id = str(data['user_id']) self.workspace_info['users'][user_id] = data elif name == 'part': msg.log('%s left the workspace' % data['username']) user_id = str(data['user_id']) try: del self.workspace_info['users'][user_id] except Exception as e: print('Unable to delete user %s from user list' % (data)) region_key = 'floobits-highlight-%s' % (user_id) for window in sublime.windows(): for view in window.views(): view.erase_regions(region_key) elif name == 'highlight': region_key = 'floobits-highlight-%s' % (data['user_id']) Listener.highlight(data['id'], region_key, data['username'], data['ranges'], data.get('ping', False)) elif name == 'set_temp_data': hangout_data = data.get('data', {}) hangout = hangout_data.get('hangout', {}) hangout_url = hangout.get('url') if hangout_url: self.prompt_join_hangout(hangout_url) elif name == 'saved': try: buf = listener.BUFS[data['id']] username = self.get_username_by_id(data['user_id']) msg.log('%s saved buffer %s' % (username, buf['path'])) except Exception as e: msg.error(str(e)) elif name == 'request_perms': print(data) user_id = str(data.get('user_id')) username = self.get_username_by_id(user_id) if not username: return msg.debug( 'Unknown user for id %s. Not handling request_perms event.' % user_id) perm_mapping = { 'edit_room': 'edit', 'admin_room': 'admin', } perms = data.get('perms') perms_str = ''.join([perm_mapping.get(p) for p in perms]) prompt = 'User %s is requesting %s permission for this room.' % ( username, perms_str) message = data.get('message') if message: prompt += '\n\n%s says: %s' % (username, message) prompt += '\n\nDo you want to grant them permission?' confirm = bool(sublime.ok_cancel_dialog(prompt)) if confirm: action = 'add' else: action = 'reject' self.put({ 'name': 'perms', 'action': action, 'user_id': user_id, 'perms': perms }) elif name == 'perms': action = data['action'] user_id = str(data['user_id']) user = self.workspace_info['users'].get(user_id) if user is None: msg.log('No user for id %s. Not handling perms event' % user_id) return perms = set(user['perms']) if action == 'add': perms |= set(data['perms']) elif action == 'remove': perms -= set(data['perms']) else: return user['perms'] = list(perms) if user_id == self.workspace_info['user_id']: G.PERMS = perms elif name == 'msg': self.on_msg(data) else: msg.debug('unknown name!', name, 'data:', data)
def on_join(self, data): msg.log('%s joined the workspace' % data['username'])
def message_dialog(message): msg.log(message)