LocalFork::MessageAction LocalFork::handleReceivedMessage(MessagePtr msg, const AffectedArea &area) { // No local fork: nothing to do. It is possible that we get a message from ourselves // that is not in the local fork, but this is not an error. It could happen when // playing back recordings, for example. if(_messages.isEmpty()) return CONCURRENT; // Check if this is our own message that has finished its roundtrip if(msg->contextId() == _messages.first()->contextId()) { if(msg.equals(_messages.first())) { _messages.removeFirst(); _areas.removeFirst(); return ALREADYDONE; } else { // Unusual, but not an error. This can happen when the layer is locked while drawing // or when an operator performs some function on behalf the user. qWarning("local fork out of sync: discarding..."); clear(); return ROLLBACK; } } // OK, so this is another user's message. Check if it is concurrent for(const AffectedArea &a : _areas) { if(!area.isConcurrentWith(a)) { return ROLLBACK; } } return CONCURRENT; }
bool Client::handleUndoCommand(protocol::Undo &undo) { // First check if user context override is used if(undo.overrideId()) { undo.setContextId(undo.overrideId()); } // Undo history is limited to last UNDO_HISTORY_LIMIT undopoints // or the latest snapshot point, whichever comes first // Clients usually store more than that, but to ensure a consistent // experience, we enforce the limit here. if(undo.points()>0) { // Undo mode: iterator towards the beginning of the history, // marking not-undone UndoPoints as undone. int limit = protocol::UNDO_HISTORY_LIMIT; int pos = _session->mainstream().end()-1; int points = 0; while(limit>0 && points<undo.points() && _session->mainstream().isValidIndex(pos)) { MessagePtr msg = _session->mainstream().at(pos); if(msg->type() == protocol::MSG_SNAPSHOTPOINT) break; if(msg->type() == protocol::MSG_UNDOPOINT) { --limit; if(msg->contextId() == undo.contextId() && msg->undoState()==protocol::DONE) { msg->setUndoState(protocol::UNDONE); ++points; } } --pos; } // Did we undo anything? if(points==0) return false; // Number of undoable actions may be less than expected, but undo what we got. undo.setPoints(points); return true; } else if(undo.points()<0) { // Redo mode: find the start of the latest undo sequence, then mark // (points) UndoPoints as redone. int redostart = _session->mainstream().end(); int limit = protocol::UNDO_HISTORY_LIMIT; int pos = _session->mainstream().end(); while(_session->mainstream().isValidIndex(--pos) && limit>0) { protocol::MessagePtr msg = _session->mainstream().at(pos); if(msg->type() == protocol::MSG_UNDOPOINT) { --limit; if(msg->contextId() == undo.contextId()) { if(msg->undoState() != protocol::DONE) redostart = pos; else break; } } } // There may be nothing to redo if(redostart == _session->mainstream().end()) return false; pos = redostart; // Mark undone actions as done again int actions = -undo.points() + 1; int points = 0; while(pos < _session->mainstream().end()) { protocol::MessagePtr msg = _session->mainstream().at(pos); if(msg->contextId() == undo.contextId() && msg->type() == protocol::MSG_UNDOPOINT) { if(msg->undoState() == protocol::UNDONE) { if(--actions==0) break; msg->setUndoState(protocol::DONE); ++points; } } ++pos; } // Did we redo anything if(points==0) return false; undo.setPoints(-points); return true; } else { // points==0 is invalid return false; } }
/** * @brief Handle messages in normal session mode * * This one is pretty simple. The message is validated to make sure * the client is authorized to send it, etc. and it is added to the * main message stream, from which it is distributed to all connected clients. * @param msg the message received from the client */ void Client::handleSessionMessage(MessagePtr msg) { // Filter away blatantly unallowed messages switch(msg->type()) { using namespace protocol; case MSG_LOGIN: case MSG_USER_JOIN: case MSG_USER_ATTR: case MSG_USER_LEAVE: case MSG_SESSION_CONFIG: case MSG_STREAMPOS: _server->printDebug(QString("Warning: user #%1 sent server-to-user only command %2").arg(_id).arg(msg->type())); return; default: break; } if(msg->isOpCommand() && !_isOperator) { _server->printDebug(QString("Warning: normal user #%1 tried to use operator command %2").arg(_id).arg(msg->type())); return; } // Layer control locking if(_server->session().layerctrllocked && !_isOperator) { switch(msg->type()) { using namespace protocol; case MSG_LAYER_CREATE: case MSG_LAYER_ATTR: case MSG_LAYER_ORDER: case MSG_LAYER_RETITLE: case MSG_LAYER_DELETE: _server->printDebug(QString("Blocked layer control command from non-operator #%1").arg(_id)); return; default: break; } } // Locking (note. applies only to command stream) if(msg->isCommand()) { if(isDropLocked()) { // ignore command return; } else if(isHoldLocked()) { _holdqueue.append(msg); return; } // Layer specific locking. Drop commands that affect layer contents switch(msg->type()) { using namespace protocol; case MSG_PEN_MOVE: if(isLayerLocked(_server->session().drawingctx[_id].currentLayer)) return; break; case MSG_LAYER_ATTR: if(!_isOperator && isLayerLocked(msg.cast<LayerAttributes>().id())) return; break; case MSG_LAYER_RETITLE: if(!_isOperator && isLayerLocked(msg.cast<LayerRetitle>().id())) return; break; case MSG_LAYER_DELETE: if(!_isOperator && isLayerLocked(msg.cast<LayerDelete>().id())) return; break; case MSG_PUTIMAGE: if(isLayerLocked(msg.cast<PutImage>().layer())) return; break; default: /* other types are always allowed */ break; } } // Make sure the origin user ID is set msg->setContextId(_id); // Track state and special commands switch(msg->type()) { using namespace protocol; case MSG_TOOLCHANGE: _server->session().drawingContextToolChange(msg.cast<ToolChange>()); break; case MSG_PEN_MOVE: _server->session().drawingContextPenDown(msg.cast<PenMove>()); break; case MSG_PEN_UP: _server->session().drawingContextPenUp(msg.cast<PenUp>()); if(_barrierlock == BARRIER_WAIT) { _barrierlock = BARRIER_LOCKED; emit barrierLocked(); } break; case MSG_LAYER_CREATE: _server->session().createLayer(msg.cast<LayerCreate>(), true); break; case MSG_LAYER_ORDER: _server->session().reorderLayers(msg.cast<LayerOrder>()); break; case MSG_LAYER_DELETE: // drop message if layer didn't exist if(!_server->session().deleteLayer(msg.cast<LayerDelete>().id())) return; break; case MSG_LAYER_ACL: // drop message if layer didn't exist if(!_server->session().updateLayerAcl(msg.cast<LayerACL>())) return; break; case MSG_ANNOTATION_CREATE: _server->session().createAnnotation(msg.cast<AnnotationCreate>(), true); break; case MSG_ANNOTATION_DELETE: // drop message if annotation didn't exist if(!_server->session().deleteAnnotation(msg.cast<AnnotationDelete>().id())) return; break; case MSG_UNDOPOINT: // keep track of undo points handleUndoPoint(); break; case MSG_UNDO: // validate undo command if(!handleUndoCommand(msg.cast<Undo>())) return; break; case MSG_CHAT: // Chat is used also for operator commands if(_isOperator && handleOperatorCommand(msg->contextId(), msg.cast<Chat>().message())) return; break; case MSG_SNAPSHOT: handleSnapshotStart(msg.cast<SnapshotMode>()); return; default: break; } // Add to main command stream to be distributed to everyone _server->addToCommandStream(msg); }