def __init__(self): self.persister = Persister()
class DaemonMobWrite(mobwrite_core.MobWrite): def __init__(self): self.persister = Persister() def handleRequest(self, text): try: mobwrite_core.LOG.debug("Incoming: " + text) actions = self.parseRequest(text) reply = self.doActions(actions) mobwrite_core.LOG.debug("Reply: " + reply) return reply except: mobwrite_core.LOG.exception("Error handling request: " + text) return "E:all:Processing error" def doActions(self, actions): output = [] last_username = None last_filename = None for action_index in xrange(len(actions)): action = actions[action_index] mobwrite_core.LOG.debug("action %s = %s", action_index, action) for action_index in xrange(len(actions)): # Use an indexed loop in order to peek ahead one step to detect # username/filename boundaries. action = actions[action_index] # Close mode doesn't need a filename or handle for the 'close all' case # If killing a specific view, then the id is in the 'data' if action["mode"] == "close": to_close = action.get("data") if to_close == "all": kill_views_for_user(action["username"]) elif to_close is not None: kill_view(action["username"], to_close) continue viewobj = fetch_viewobj(action["username"], action["filename"], handle=action["handle"], metadata=action["metadata"], persister=self.persister) if viewobj is None: # Too many views connected at once. # Send back nothing. Pretend the return packet was lost. return "" delta_ok = True mobwrite_core.LOG.debug("view.lock.acquire on %s@%s", viewobj.username, viewobj.filename) viewobj.lock.acquire() textobj = viewobj.textobj try: access = self.persister.check_access(action["filename"], action["handle"]) if access == Access.Denied: name = get_username_from_handle(action["handle"]) message = "%s does not have access to %s" % ( name, action["filename"]) mobwrite_core.LOG.warning(message) output.append("E:" + action["filename"] + ":" + message + "\n") continue if action["mode"] == "null": if access == Access.ReadOnly: output.append("O:" + action["filename"] + "\n") else: # Nullify the text. mobwrite_core.LOG.debug( "Nullifying: '%s@%s'" % (viewobj.username, viewobj.filename)) mobwrite_core.LOG.debug("text.lock.acquire on %s", textobj.name) textobj.lock.acquire() try: textobj.setText(None) finally: mobwrite_core.LOG.debug("text.lock.release on %s", textobj.name) textobj.lock.release() viewobj.nullify() continue if (action["server_version"] != viewobj.shadow_server_version and action["server_version"] == viewobj.backup_shadow_server_version): # Client did not receive the last response. Roll back the shadow. mobwrite_core.LOG.warning( "Rollback from shadow %d to backup shadow %d" % (viewobj.shadow_server_version, viewobj.backup_shadow_server_version)) viewobj.shadow = viewobj.backup_shadow viewobj.shadow_server_version = viewobj.backup_shadow_server_version viewobj.edit_stack = [] # Remove any elements from the edit stack with low version numbers which # have been acked by the client. x = 0 while x < len(viewobj.edit_stack): if viewobj.edit_stack[x][0] <= action["server_version"]: del viewobj.edit_stack[x] else: x += 1 if action["mode"] == "raw": # It's a raw text dump. data = urllib.unquote(action["data"]).decode("utf-8") mobwrite_core.LOG.info( "Got %db raw text: '%s@%s'" % (len(data), viewobj.username, viewobj.filename)) delta_ok = True # First, update the client's shadow. viewobj.shadow = data viewobj.shadow_client_version = action["client_version"] viewobj.shadow_server_version = action["server_version"] viewobj.backup_shadow = viewobj.shadow viewobj.backup_shadow_server_version = viewobj.shadow_server_version viewobj.edit_stack = [] if access == Access.ReadOnly: output.append("O:" + action["filename"] + "\n") elif action["force"] or textobj.text == None: # Clobber the server's text. mobwrite_core.LOG.debug("text.lock.acquire on %s", textobj.name) textobj.lock.acquire() try: if textobj.text != data: textobj.setText(data) mobwrite_core.LOG.debug( "Overwrote content: '%s@%s'" % (viewobj.username, viewobj.filename)) finally: mobwrite_core.LOG.debug("text.lock.release on %s", textobj.name) textobj.lock.release() elif action["mode"] == "delta": # It's a delta. mobwrite_core.LOG.debug("Got delta: %s@%s", viewobj.username, viewobj.filename) # mobwrite_core.LOG.debug("Got '%s' delta: '%s@%s'" % # (action["data"], viewobj.username, viewobj.filename)) if action[ "server_version"] != viewobj.shadow_server_version: # Can't apply a delta on a mismatched shadow version. delta_ok = False mobwrite_core.LOG.warning( "Shadow version mismatch: %d != %d" % (action["server_version"], viewobj.shadow_server_version)) elif action[ "client_version"] > viewobj.shadow_client_version: # Client has a version in the future? delta_ok = False mobwrite_core.LOG.warning( "Future delta: %d > %d" % (action["client_version"], viewobj.shadow_client_version)) elif action[ "client_version"] < viewobj.shadow_client_version: # We've already seen this diff. pass mobwrite_core.LOG.warning( "Repeated delta: %d < %d" % (action["client_version"], viewobj.shadow_client_version)) else: # Expand the delta into a diff using the client shadow. try: diffs = mobwrite_core.DMP.diff_fromDelta( viewobj.shadow, action["data"]) except ValueError: diffs = None delta_ok = False mobwrite_core.LOG.warning( "Delta failure, expected %d length: '%s@%s'" % (len(viewobj.shadow), viewobj.username, viewobj.filename)) viewobj.shadow_client_version += 1 if diffs != None: if access == Access.ReadOnly: output.append("O:" + action["filename"] + "\n") else: # Textobj lock required for read/patch/write cycle. mobwrite_core.LOG.debug( "text.lock.acquire on %s", textobj.name) textobj.lock.acquire() try: self.applyPatches(viewobj, diffs, action) finally: mobwrite_core.LOG.debug( "text.lock.release on %s", textobj.name) textobj.lock.release() # Generate output if this is the last action or the username/filename # will change in the next iteration. if ((action_index + 1 == len(actions)) or actions[action_index + 1]["username"] != viewobj.username or actions[action_index + 1]["filename"] != viewobj.filename): echo_collaborators = "echo_collaborators" in action output.append( self.generateDiffs(viewobj, last_username, last_filename, action["echo_username"], action["force"], delta_ok, echo_collaborators)) last_username = viewobj.username last_filename = viewobj.filename finally: mobwrite_core.LOG.debug("view.lock.release on %s@%s", viewobj.username, viewobj.filename) viewobj.lock.release() answer = "".join(output) return answer def generateDiffs(self, viewobj, last_username, last_filename, echo_username, force, delta_ok, echo_collaborators): output = [] if (echo_username and last_username != viewobj.username): output.append("u:%s\n" % viewobj.username) if (last_filename != viewobj.filename or last_username != viewobj.username): output.append("F:%d:%s\n" % (viewobj.shadow_client_version, viewobj.filename)) textobj = viewobj.textobj mastertext = textobj.text if delta_ok: if mastertext is None: mastertext = "" # Create the diff between the view's text and the master text. diffs = mobwrite_core.DMP.diff_main(viewobj.shadow, mastertext) mobwrite_core.DMP.diff_cleanupEfficiency(diffs) text = mobwrite_core.DMP.diff_toDelta(diffs) if force: # Client sending 'D' means number, no error. # Client sending 'R' means number, client error. # Both cases involve numbers, so send back an overwrite delta. viewobj.edit_stack.append( (viewobj.shadow_server_version, "D:%d:%s\n" % (viewobj.shadow_server_version, text))) else: # Client sending 'd' means text, no error. # Client sending 'r' means text, client error. # Both cases involve text, so send back a merge delta. viewobj.edit_stack.append( (viewobj.shadow_server_version, "d:%d:%s\n" % (viewobj.shadow_server_version, text))) viewobj.shadow_server_version += 1 mobwrite_core.LOG.debug("Sent delta for %s@%s", viewobj.username, viewobj.filename) # mobwrite_core.LOG.debug("Sent '%s' delta: '%s@%s'" % # (text, viewobj.username, viewobj.filename)) else: # Error; server could not parse client's delta. # Send a raw dump of the text. viewobj.shadow_client_version += 1 if mastertext is None: mastertext = "" viewobj.edit_stack.append( (viewobj.shadow_server_version, "r:%d:\n" % viewobj.shadow_server_version)) mobwrite_core.LOG.info("Sent empty raw text: '%s@%s'" % (viewobj.username, viewobj.filename)) else: # Force overwrite of client. text = mastertext text = text.encode("utf-8") text = urllib.quote(text, "!~*'();/?:@&=+$,# ") viewobj.edit_stack.append( (viewobj.shadow_server_version, "R:%d:%s\n" % (viewobj.shadow_server_version, text))) mobwrite_core.LOG.info( "Sent %db raw text: '%s@%s'" % (len(text), viewobj.username, viewobj.filename)) viewobj.shadow = mastertext for edit in viewobj.edit_stack: output.append(edit[1]) # Mozilla: We're passing on the first 4 chars of the username here, but # it's worth checking if there is still value in doing that if echo_collaborators: for view in viewobj.textobj.views: view.metadata["id"] = view.username[0:4] line = "C:" + view.handle + ":" + simplejson.dumps( view.metadata) + "\n" output.append(line) return "".join(output)
class DaemonMobWrite(mobwrite_core.MobWrite): def __init__(self): self.persister = Persister() def handleRequest(self, text): try: mobwrite_core.LOG.debug("Incoming: " + text) actions = self.parseRequest(text) reply = self.doActions(actions) mobwrite_core.LOG.debug("Reply: " + reply) return reply except: mobwrite_core.LOG.exception("Error handling request: " + text) return "E:all:Processing error" def doActions(self, actions): output = [] last_username = None last_filename = None for action_index in xrange(len(actions)): action = actions[action_index] mobwrite_core.LOG.debug("action %s = %s", action_index, action) for action_index in xrange(len(actions)): # Use an indexed loop in order to peek ahead one step to detect # username/filename boundaries. action = actions[action_index] # Close mode doesn't need a filename or handle for the 'close all' case # If killing a specific view, then the id is in the 'data' if action["mode"] == "close": to_close = action.get("data") if to_close == "all": kill_views_for_user(action["username"]) elif to_close is not None: kill_view(action["username"], to_close) continue viewobj = fetch_viewobj(action["username"], action["filename"], handle=action["handle"], metadata=action["metadata"], persister=self.persister) if viewobj is None: # Too many views connected at once. # Send back nothing. Pretend the return packet was lost. return "" delta_ok = True mobwrite_core.LOG.debug("view.lock.acquire on %s@%s", viewobj.username, viewobj.filename) viewobj.lock.acquire() textobj = viewobj.textobj try: access = self.persister.check_access(action["filename"], action["handle"]) if access == Access.Denied: name = get_username_from_handle(action["handle"]) message = "%s does not have access to %s" % (name, action["filename"]) mobwrite_core.LOG.warning(message) output.append("E:" + action["filename"] + ":" + message + "\n") continue if action["mode"] == "null": if access == Access.ReadOnly: output.append("O:" + action["filename"] + "\n") else: # Nullify the text. mobwrite_core.LOG.debug("Nullifying: '%s@%s'" % (viewobj.username, viewobj.filename)) mobwrite_core.LOG.debug("text.lock.acquire on %s", textobj.name) textobj.lock.acquire() try: textobj.setText(None) finally: mobwrite_core.LOG.debug("text.lock.release on %s", textobj.name) textobj.lock.release() viewobj.nullify() continue if (action["server_version"] != viewobj.shadow_server_version and action["server_version"] == viewobj.backup_shadow_server_version): # Client did not receive the last response. Roll back the shadow. mobwrite_core.LOG.warning("Rollback from shadow %d to backup shadow %d" % (viewobj.shadow_server_version, viewobj.backup_shadow_server_version)) viewobj.shadow = viewobj.backup_shadow viewobj.shadow_server_version = viewobj.backup_shadow_server_version viewobj.edit_stack = [] # Remove any elements from the edit stack with low version numbers which # have been acked by the client. x = 0 while x < len(viewobj.edit_stack): if viewobj.edit_stack[x][0] <= action["server_version"]: del viewobj.edit_stack[x] else: x += 1 if action["mode"] == "raw": # It's a raw text dump. data = urllib.unquote(action["data"]).decode("utf-8") mobwrite_core.LOG.info("Got %db raw text: '%s@%s'" % (len(data), viewobj.username, viewobj.filename)) delta_ok = True # First, update the client's shadow. viewobj.shadow = data viewobj.shadow_client_version = action["client_version"] viewobj.shadow_server_version = action["server_version"] viewobj.backup_shadow = viewobj.shadow viewobj.backup_shadow_server_version = viewobj.shadow_server_version viewobj.edit_stack = [] if access == Access.ReadOnly: output.append("O:" + action["filename"] + "\n") elif action["force"] or textobj.text == None: # Clobber the server's text. mobwrite_core.LOG.debug("text.lock.acquire on %s", textobj.name) textobj.lock.acquire() try: if textobj.text != data: textobj.setText(data) mobwrite_core.LOG.debug("Overwrote content: '%s@%s'" % (viewobj.username, viewobj.filename)) finally: mobwrite_core.LOG.debug("text.lock.release on %s", textobj.name) textobj.lock.release() elif action["mode"] == "delta": # It's a delta. mobwrite_core.LOG.debug("Got delta: %s@%s", viewobj.username, viewobj.filename) # mobwrite_core.LOG.debug("Got '%s' delta: '%s@%s'" % # (action["data"], viewobj.username, viewobj.filename)) if action["server_version"] != viewobj.shadow_server_version: # Can't apply a delta on a mismatched shadow version. delta_ok = False mobwrite_core.LOG.warning("Shadow version mismatch: %d != %d" % (action["server_version"], viewobj.shadow_server_version)) elif action["client_version"] > viewobj.shadow_client_version: # Client has a version in the future? delta_ok = False mobwrite_core.LOG.warning("Future delta: %d > %d" % (action["client_version"], viewobj.shadow_client_version)) elif action["client_version"] < viewobj.shadow_client_version: # We've already seen this diff. pass mobwrite_core.LOG.warning("Repeated delta: %d < %d" % (action["client_version"], viewobj.shadow_client_version)) else: # Expand the delta into a diff using the client shadow. try: diffs = mobwrite_core.DMP.diff_fromDelta(viewobj.shadow, action["data"]) except ValueError: diffs = None delta_ok = False mobwrite_core.LOG.warning("Delta failure, expected %d length: '%s@%s'" % (len(viewobj.shadow), viewobj.username, viewobj.filename)) viewobj.shadow_client_version += 1 if diffs != None: if access == Access.ReadOnly: output.append("O:" + action["filename"] + "\n") else: # Textobj lock required for read/patch/write cycle. mobwrite_core.LOG.debug("text.lock.acquire on %s", textobj.name) textobj.lock.acquire() try: self.applyPatches(viewobj, diffs, action) finally: mobwrite_core.LOG.debug("text.lock.release on %s", textobj.name) textobj.lock.release() # Generate output if this is the last action or the username/filename # will change in the next iteration. if ((action_index + 1 == len(actions)) or actions[action_index + 1]["username"] != viewobj.username or actions[action_index + 1]["filename"] != viewobj.filename): echo_collaborators = "echo_collaborators" in action output.append(self.generateDiffs(viewobj, last_username, last_filename, action["echo_username"], action["force"], delta_ok, echo_collaborators)) last_username = viewobj.username last_filename = viewobj.filename finally: mobwrite_core.LOG.debug("view.lock.release on %s@%s", viewobj.username, viewobj.filename) viewobj.lock.release() answer = "".join(output) return answer def generateDiffs(self, viewobj, last_username, last_filename, echo_username, force, delta_ok, echo_collaborators): output = [] if (echo_username and last_username != viewobj.username): output.append("u:%s\n" % viewobj.username) if (last_filename != viewobj.filename or last_username != viewobj.username): output.append("F:%d:%s\n" % (viewobj.shadow_client_version, viewobj.filename)) textobj = viewobj.textobj mastertext = textobj.text if delta_ok: if mastertext is None: mastertext = "" # Create the diff between the view's text and the master text. diffs = mobwrite_core.DMP.diff_main(viewobj.shadow, mastertext) mobwrite_core.DMP.diff_cleanupEfficiency(diffs) text = mobwrite_core.DMP.diff_toDelta(diffs) if force: # Client sending 'D' means number, no error. # Client sending 'R' means number, client error. # Both cases involve numbers, so send back an overwrite delta. viewobj.edit_stack.append((viewobj.shadow_server_version, "D:%d:%s\n" % (viewobj.shadow_server_version, text))) else: # Client sending 'd' means text, no error. # Client sending 'r' means text, client error. # Both cases involve text, so send back a merge delta. viewobj.edit_stack.append((viewobj.shadow_server_version, "d:%d:%s\n" % (viewobj.shadow_server_version, text))) viewobj.shadow_server_version += 1 mobwrite_core.LOG.debug("Sent delta for %s@%s", viewobj.username, viewobj.filename) # mobwrite_core.LOG.debug("Sent '%s' delta: '%s@%s'" % # (text, viewobj.username, viewobj.filename)) else: # Error; server could not parse client's delta. # Send a raw dump of the text. viewobj.shadow_client_version += 1 if mastertext is None: mastertext = "" viewobj.edit_stack.append((viewobj.shadow_server_version, "r:%d:\n" % viewobj.shadow_server_version)) mobwrite_core.LOG.info("Sent empty raw text: '%s@%s'" % (viewobj.username, viewobj.filename)) else: # Force overwrite of client. text = mastertext text = text.encode("utf-8") text = urllib.quote(text, "!~*'();/?:@&=+$,# ") viewobj.edit_stack.append((viewobj.shadow_server_version, "R:%d:%s\n" % (viewobj.shadow_server_version, text))) mobwrite_core.LOG.info("Sent %db raw text: '%s@%s'" % (len(text), viewobj.username, viewobj.filename)) viewobj.shadow = mastertext for edit in viewobj.edit_stack: output.append(edit[1]) # Mozilla: We're passing on the first 4 chars of the username here, but # it's worth checking if there is still value in doing that if echo_collaborators: for view in viewobj.textobj.views: view.metadata["id"] = view.username[0:4] line = "C:" + view.handle + ":" + simplejson.dumps(view.metadata) + "\n" output.append(line) return "".join(output)