diff options
Diffstat (limited to 'src/lib/server')
31 files changed, 10045 insertions, 0 deletions
diff --git a/src/lib/server/BaseClientProxy.cpp b/src/lib/server/BaseClientProxy.cpp new file mode 100644 index 0000000..b9c5339 --- /dev/null +++ b/src/lib/server/BaseClientProxy.cpp @@ -0,0 +1,56 @@ +/* + * barrier -- mouse and keyboard sharing utility + * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2006 Chris Schoeneman + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "server/BaseClientProxy.h" + +// +// BaseClientProxy +// + +BaseClientProxy::BaseClientProxy(const String& name) : + m_name(name), + m_x(0), + m_y(0) +{ + // do nothing +} + +BaseClientProxy::~BaseClientProxy() +{ + // do nothing +} + +void +BaseClientProxy::setJumpCursorPos(SInt32 x, SInt32 y) +{ + m_x = x; + m_y = y; +} + +void +BaseClientProxy::getJumpCursorPos(SInt32& x, SInt32& y) const +{ + x = m_x; + y = m_y; +} + +String +BaseClientProxy::getName() const +{ + return m_name; +} diff --git a/src/lib/server/BaseClientProxy.h b/src/lib/server/BaseClientProxy.h new file mode 100644 index 0000000..c7c23ff --- /dev/null +++ b/src/lib/server/BaseClientProxy.h @@ -0,0 +1,99 @@ +/* + * barrier -- mouse and keyboard sharing utility + * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2002 Chris Schoeneman + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "barrier/IClient.h" +#include "base/String.h" + +namespace barrier { class IStream; } + +//! Generic proxy for client or primary +class BaseClientProxy : public IClient { +public: + /*! + \c name is the name of the client. + */ + BaseClientProxy(const String& name); + ~BaseClientProxy(); + + //! @name manipulators + //@{ + + //! Save cursor position + /*! + Save the position of the cursor when jumping from client. + */ + void setJumpCursorPos(SInt32 x, SInt32 y); + + //@} + //! @name accessors + //@{ + + //! Get cursor position + /*! + Get the position of the cursor when last jumping from client. + */ + void getJumpCursorPos(SInt32& x, SInt32& y) const; + + //! Get cursor position + /*! + Return if this proxy is for client or primary. + */ + virtual bool isPrimary() const { return false; } + + //@} + + // IScreen + virtual void* getEventTarget() const = 0; + virtual bool getClipboard(ClipboardID id, IClipboard*) const = 0; + virtual void getShape(SInt32& x, SInt32& y, + SInt32& width, SInt32& height) const = 0; + virtual void getCursorPos(SInt32& x, SInt32& y) const = 0; + + // IClient overrides + virtual void enter(SInt32 xAbs, SInt32 yAbs, + UInt32 seqNum, KeyModifierMask mask, + bool forScreensaver) = 0; + virtual bool leave() = 0; + virtual void setClipboard(ClipboardID, const IClipboard*) = 0; + virtual void grabClipboard(ClipboardID) = 0; + virtual void setClipboardDirty(ClipboardID, bool) = 0; + virtual void keyDown(KeyID, KeyModifierMask, KeyButton) = 0; + virtual void keyRepeat(KeyID, KeyModifierMask, + SInt32 count, KeyButton) = 0; + virtual void keyUp(KeyID, KeyModifierMask, KeyButton) = 0; + virtual void mouseDown(ButtonID) = 0; + virtual void mouseUp(ButtonID) = 0; + virtual void mouseMove(SInt32 xAbs, SInt32 yAbs) = 0; + virtual void mouseRelativeMove(SInt32 xRel, SInt32 yRel) = 0; + virtual void mouseWheel(SInt32 xDelta, SInt32 yDelta) = 0; + virtual void screensaver(bool activate) = 0; + virtual void resetOptions() = 0; + virtual void setOptions(const OptionsList& options) = 0; + virtual void sendDragInfo(UInt32 fileCount, const char* info, + size_t size) = 0; + virtual void fileChunkSending(UInt8 mark, char* data, size_t dataSize) = 0; + virtual String getName() const; + virtual barrier::IStream* + getStream() const = 0; + +private: + String m_name; + SInt32 m_x, m_y; +}; diff --git a/src/lib/server/CMakeLists.txt b/src/lib/server/CMakeLists.txt new file mode 100644 index 0000000..5242d6d --- /dev/null +++ b/src/lib/server/CMakeLists.txt @@ -0,0 +1,30 @@ +# barrier -- mouse and keyboard sharing utility +# Copyright (C) 2012-2016 Symless Ltd. +# Copyright (C) 2009 Nick Bolton +# +# This package is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# found in the file LICENSE that should have accompanied this file. +# +# This package is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +file(GLOB headers "*.h") +file(GLOB sources "*.cpp") + +if (BARRIER_ADD_HEADERS) + list(APPEND sources ${headers}) +endif() + +add_library(server STATIC ${sources}) + +target_link_libraries(server) + +if (UNIX) + target_link_libraries(server synlib) +endif() diff --git a/src/lib/server/ClientListener.cpp b/src/lib/server/ClientListener.cpp new file mode 100644 index 0000000..00067ba --- /dev/null +++ b/src/lib/server/ClientListener.cpp @@ -0,0 +1,256 @@ +/* + * barrier -- mouse and keyboard sharing utility + * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2004 Chris Schoeneman + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "server/ClientListener.h" + +#include "server/ClientProxy.h" +#include "server/ClientProxyUnknown.h" +#include "barrier/PacketStreamFilter.h" +#include "net/IDataSocket.h" +#include "net/IListenSocket.h" +#include "net/ISocketFactory.h" +#include "net/XSocket.h" +#include "base/Log.h" +#include "base/IEventQueue.h" +#include "base/TMethodEventJob.h" + +// +// ClientListener +// + +ClientListener::ClientListener(const NetworkAddress& address, + ISocketFactory* socketFactory, + IEventQueue* events, + bool enableCrypto) : + m_socketFactory(socketFactory), + m_server(NULL), + m_events(events), + m_useSecureNetwork(enableCrypto) +{ + assert(m_socketFactory != NULL); + + try { + m_listen = m_socketFactory->createListen( + ARCH->getAddrFamily(address.getAddress()), + m_useSecureNetwork); + + // setup event handler + m_events->adoptHandler(m_events->forIListenSocket().connecting(), + m_listen, + new TMethodEventJob<ClientListener>(this, + &ClientListener::handleClientConnecting)); + + // bind listen address + LOG((CLOG_DEBUG1 "binding listen socket")); + m_listen->bind(address); + } + catch (XSocketAddressInUse&) { + cleanupListenSocket(); + delete m_socketFactory; + throw; + } + catch (XBase&) { + cleanupListenSocket(); + delete m_socketFactory; + throw; + } + LOG((CLOG_DEBUG1 "listening for clients")); +} + +ClientListener::~ClientListener() +{ + LOG((CLOG_DEBUG1 "stop listening for clients")); + + // discard already connected clients + for (NewClients::iterator index = m_newClients.begin(); + index != m_newClients.end(); ++index) { + ClientProxyUnknown* client = *index; + m_events->removeHandler( + m_events->forClientProxyUnknown().success(), client); + m_events->removeHandler( + m_events->forClientProxyUnknown().failure(), client); + m_events->removeHandler( + m_events->forClientProxy().disconnected(), client); + delete client; + } + + // discard waiting clients + ClientProxy* client = getNextClient(); + while (client != NULL) { + delete client; + client = getNextClient(); + } + + m_events->removeHandler(m_events->forIListenSocket().connecting(), m_listen); + cleanupListenSocket(); + cleanupClientSockets(); + delete m_socketFactory; +} + +void +ClientListener::setServer(Server* server) +{ + assert(server != NULL); + m_server = server; +} + +ClientProxy* +ClientListener::getNextClient() +{ + ClientProxy* client = NULL; + if (!m_waitingClients.empty()) { + client = m_waitingClients.front(); + m_waitingClients.pop_front(); + m_events->removeHandler(m_events->forClientProxy().disconnected(), client); + } + return client; +} + +void +ClientListener::handleClientConnecting(const Event&, void*) +{ + // accept client connection + IDataSocket* socket = m_listen->accept(); + + if (socket == NULL) { + return; + } + + m_clientSockets.insert(socket); + + m_events->adoptHandler(m_events->forClientListener().accepted(), + socket->getEventTarget(), + new TMethodEventJob<ClientListener>(this, + &ClientListener::handleClientAccepted, socket)); + + // When using non SSL, server accepts clients immediately, while SSL + // has to call secure accept which may require retry + if (!m_useSecureNetwork) { + m_events->addEvent(Event(m_events->forClientListener().accepted(), + socket->getEventTarget())); + } +} + +void +ClientListener::handleClientAccepted(const Event&, void* vsocket) +{ + LOG((CLOG_NOTE "accepted client connection")); + + IDataSocket* socket = static_cast<IDataSocket*>(vsocket); + + // filter socket messages, including a packetizing filter + barrier::IStream* stream = new PacketStreamFilter(m_events, socket, false); + assert(m_server != NULL); + + // create proxy for unknown client + ClientProxyUnknown* client = new ClientProxyUnknown(stream, 30.0, m_server, m_events); + + m_newClients.insert(client); + + // watch for events from unknown client + m_events->adoptHandler(m_events->forClientProxyUnknown().success(), + client, + new TMethodEventJob<ClientListener>(this, + &ClientListener::handleUnknownClient, client)); + m_events->adoptHandler(m_events->forClientProxyUnknown().failure(), + client, + new TMethodEventJob<ClientListener>(this, + &ClientListener::handleUnknownClient, client)); +} + +void +ClientListener::handleUnknownClient(const Event&, void* vclient) +{ + ClientProxyUnknown* unknownClient = + static_cast<ClientProxyUnknown*>(vclient); + + // we should have the client in our new client list + assert(m_newClients.count(unknownClient) == 1); + + // get the real client proxy and install it + ClientProxy* client = unknownClient->orphanClientProxy(); + bool handshakeOk = true; + if (client != NULL) { + // handshake was successful + m_waitingClients.push_back(client); + m_events->addEvent(Event(m_events->forClientListener().connected(), + this)); + + // watch for client to disconnect while it's in our queue + m_events->adoptHandler(m_events->forClientProxy().disconnected(), client, + new TMethodEventJob<ClientListener>(this, + &ClientListener::handleClientDisconnected, + client)); + } + else { + handshakeOk = false; + } + + // now finished with unknown client + m_events->removeHandler(m_events->forClientProxyUnknown().success(), client); + m_events->removeHandler(m_events->forClientProxyUnknown().failure(), client); + m_newClients.erase(unknownClient); + PacketStreamFilter* streamFileter = dynamic_cast<PacketStreamFilter*>(unknownClient->getStream()); + IDataSocket* socket = NULL; + if (streamFileter != NULL) { + socket = dynamic_cast<IDataSocket*>(streamFileter->getStream()); + } + + delete unknownClient; +} + +void +ClientListener::handleClientDisconnected(const Event&, void* vclient) +{ + ClientProxy* client = static_cast<ClientProxy*>(vclient); + + // find client in waiting clients queue + for (WaitingClients::iterator i = m_waitingClients.begin(), + n = m_waitingClients.end(); i != n; ++i) { + if (*i == client) { + m_waitingClients.erase(i); + m_events->removeHandler(m_events->forClientProxy().disconnected(), + client); + + // pull out the socket before deleting the client so + // we know which socket we no longer need + IDataSocket* socket = static_cast<IDataSocket*>(client->getStream()); + delete client; + m_clientSockets.erase(socket); + delete socket; + + break; + } + } +} + +void +ClientListener::cleanupListenSocket() +{ + delete m_listen; +} + +void +ClientListener::cleanupClientSockets() +{ + ClientSockets::iterator it; + for (it = m_clientSockets.begin(); it != m_clientSockets.end(); it++) { + delete *it; + } + m_clientSockets.clear(); +} diff --git a/src/lib/server/ClientListener.h b/src/lib/server/ClientListener.h new file mode 100644 index 0000000..b02cbb1 --- /dev/null +++ b/src/lib/server/ClientListener.h @@ -0,0 +1,91 @@ +/* + * barrier -- mouse and keyboard sharing utility + * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2004 Chris Schoeneman + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "server/Config.h" +#include "base/EventTypes.h" +#include "base/Event.h" +#include "common/stddeque.h" +#include "common/stdset.h" + +class ClientProxy; +class ClientProxyUnknown; +class NetworkAddress; +class IListenSocket; +class ISocketFactory; +class Server; +class IEventQueue; +class IDataSocket; + +class ClientListener { +public: + // The factories are adopted. + ClientListener(const NetworkAddress&, + ISocketFactory*, + IEventQueue* events, + bool enableCrypto); + ~ClientListener(); + + //! @name manipulators + //@{ + + void setServer(Server* server); + + //@} + + //! @name accessors + //@{ + + //! Get next connected client + /*! + Returns the next connected client and removes it from the internal + list. The client is responsible for deleting the returned client. + Returns NULL if no clients are available. + */ + ClientProxy* getNextClient(); + + //! Get server which owns this listener + Server* getServer() { return m_server; } + + //@} + +private: + // client connection event handlers + void handleClientConnecting(const Event&, void*); + void handleClientAccepted(const Event&, void*); + void handleUnknownClient(const Event&, void*); + void handleClientDisconnected(const Event&, void*); + + void cleanupListenSocket(); + void cleanupClientSockets(); + +private: + typedef std::set<ClientProxyUnknown*> NewClients; + typedef std::deque<ClientProxy*> WaitingClients; + typedef std::set<IDataSocket*> ClientSockets; + + IListenSocket* m_listen; + ISocketFactory* m_socketFactory; + NewClients m_newClients; + WaitingClients m_waitingClients; + Server* m_server; + IEventQueue* m_events; + bool m_useSecureNetwork; + ClientSockets m_clientSockets; +}; diff --git a/src/lib/server/ClientProxy.cpp b/src/lib/server/ClientProxy.cpp new file mode 100644 index 0000000..5a28248 --- /dev/null +++ b/src/lib/server/ClientProxy.cpp @@ -0,0 +1,61 @@ +/* + * barrier -- mouse and keyboard sharing utility + * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2002 Chris Schoeneman + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "server/ClientProxy.h" + +#include "barrier/ProtocolUtil.h" +#include "io/IStream.h" +#include "base/Log.h" +#include "base/EventQueue.h" + +// +// ClientProxy +// + +ClientProxy::ClientProxy(const String& name, barrier::IStream* stream) : + BaseClientProxy(name), + m_stream(stream) +{ +} + +ClientProxy::~ClientProxy() +{ + delete m_stream; +} + +void +ClientProxy::close(const char* msg) +{ + LOG((CLOG_DEBUG1 "send close \"%s\" to \"%s\"", msg, getName().c_str())); + ProtocolUtil::writef(getStream(), msg); + + // force the close to be sent before we return + getStream()->flush(); +} + +barrier::IStream* +ClientProxy::getStream() const +{ + return m_stream; +} + +void* +ClientProxy::getEventTarget() const +{ + return static_cast<IScreen*>(const_cast<ClientProxy*>(this)); +} diff --git a/src/lib/server/ClientProxy.h b/src/lib/server/ClientProxy.h new file mode 100644 index 0000000..726ded4 --- /dev/null +++ b/src/lib/server/ClientProxy.h @@ -0,0 +1,91 @@ +/* + * barrier -- mouse and keyboard sharing utility + * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2002 Chris Schoeneman + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "server/BaseClientProxy.h" +#include "base/Event.h" +#include "base/String.h" +#include "base/EventTypes.h" + +namespace barrier { class IStream; } + +//! Generic proxy for client +class ClientProxy : public BaseClientProxy { +public: + /*! + \c name is the name of the client. + */ + ClientProxy(const String& name, barrier::IStream* adoptedStream); + ~ClientProxy(); + + //! @name manipulators + //@{ + + //! Disconnect + /*! + Ask the client to disconnect, using \p msg as the reason. + */ + void close(const char* msg); + + //@} + //! @name accessors + //@{ + + //! Get stream + /*! + Returns the original stream passed to the c'tor. + */ + barrier::IStream* getStream() const; + + //@} + + // IScreen + virtual void* getEventTarget() const; + virtual bool getClipboard(ClipboardID id, IClipboard*) const = 0; + virtual void getShape(SInt32& x, SInt32& y, + SInt32& width, SInt32& height) const = 0; + virtual void getCursorPos(SInt32& x, SInt32& y) const = 0; + + // IClient overrides + virtual void enter(SInt32 xAbs, SInt32 yAbs, + UInt32 seqNum, KeyModifierMask mask, + bool forScreensaver) = 0; + virtual bool leave() = 0; + virtual void setClipboard(ClipboardID, const IClipboard*) = 0; + virtual void grabClipboard(ClipboardID) = 0; + virtual void setClipboardDirty(ClipboardID, bool) = 0; + virtual void keyDown(KeyID, KeyModifierMask, KeyButton) = 0; + virtual void keyRepeat(KeyID, KeyModifierMask, + SInt32 count, KeyButton) = 0; + virtual void keyUp(KeyID, KeyModifierMask, KeyButton) = 0; + virtual void mouseDown(ButtonID) = 0; + virtual void mouseUp(ButtonID) = 0; + virtual void mouseMove(SInt32 xAbs, SInt32 yAbs) = 0; + virtual void mouseRelativeMove(SInt32 xRel, SInt32 yRel) = 0; + virtual void mouseWheel(SInt32 xDelta, SInt32 yDelta) = 0; + virtual void screensaver(bool activate) = 0; + virtual void resetOptions() = 0; + virtual void setOptions(const OptionsList& options) = 0; + virtual void sendDragInfo(UInt32 fileCount, const char* info, + size_t size) = 0; + virtual void fileChunkSending(UInt8 mark, char* data, size_t dataSize) = 0; + +private: + barrier::IStream* m_stream; +}; diff --git a/src/lib/server/ClientProxy1_0.cpp b/src/lib/server/ClientProxy1_0.cpp new file mode 100644 index 0000000..ee805c6 --- /dev/null +++ b/src/lib/server/ClientProxy1_0.cpp @@ -0,0 +1,484 @@ +/* + * barrier -- mouse and keyboard sharing utility + * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2002 Chris Schoeneman + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "server/ClientProxy1_0.h" + +#include "barrier/ProtocolUtil.h" +#include "barrier/XBarrier.h" +#include "io/IStream.h" +#include "base/Log.h" +#include "base/IEventQueue.h" +#include "base/TMethodEventJob.h" + +#include <cstring> + +// +// ClientProxy1_0 +// + +ClientProxy1_0::ClientProxy1_0(const String& name, barrier::IStream* stream, IEventQueue* events) : + ClientProxy(name, stream), + m_heartbeatTimer(NULL), + m_parser(&ClientProxy1_0::parseHandshakeMessage), + m_events(events) +{ + // install event handlers + m_events->adoptHandler(m_events->forIStream().inputReady(), + stream->getEventTarget(), + new TMethodEventJob<ClientProxy1_0>(this, + &ClientProxy1_0::handleData, NULL)); + m_events->adoptHandler(m_events->forIStream().outputError(), + stream->getEventTarget(), + new TMethodEventJob<ClientProxy1_0>(this, + &ClientProxy1_0::handleWriteError, NULL)); + m_events->adoptHandler(m_events->forIStream().inputShutdown(), + stream->getEventTarget(), + new TMethodEventJob<ClientProxy1_0>(this, + &ClientProxy1_0::handleDisconnect, NULL)); + m_events->adoptHandler(m_events->forIStream().outputShutdown(), + stream->getEventTarget(), + new TMethodEventJob<ClientProxy1_0>(this, + &ClientProxy1_0::handleWriteError, NULL)); + m_events->adoptHandler(Event::kTimer, this, + new TMethodEventJob<ClientProxy1_0>(this, + &ClientProxy1_0::handleFlatline, NULL)); + + setHeartbeatRate(kHeartRate, kHeartRate * kHeartBeatsUntilDeath); + + LOG((CLOG_DEBUG1 "querying client \"%s\" info", getName().c_str())); + ProtocolUtil::writef(getStream(), kMsgQInfo); +} + +ClientProxy1_0::~ClientProxy1_0() +{ + removeHandlers(); +} + +void +ClientProxy1_0::disconnect() +{ + removeHandlers(); + getStream()->close(); + m_events->addEvent(Event(m_events->forClientProxy().disconnected(), getEventTarget())); +} + +void +ClientProxy1_0::removeHandlers() +{ + // uninstall event handlers + m_events->removeHandler(m_events->forIStream().inputReady(), + getStream()->getEventTarget()); + m_events->removeHandler(m_events->forIStream().outputError(), + getStream()->getEventTarget()); + m_events->removeHandler(m_events->forIStream().inputShutdown(), + getStream()->getEventTarget()); + m_events->removeHandler(m_events->forIStream().outputShutdown(), + getStream()->getEventTarget()); + m_events->removeHandler(Event::kTimer, this); + + // remove timer + removeHeartbeatTimer(); +} + +void +ClientProxy1_0::addHeartbeatTimer() +{ + if (m_heartbeatAlarm > 0.0) { + m_heartbeatTimer = m_events->newOneShotTimer(m_heartbeatAlarm, this); + } +} + +void +ClientProxy1_0::removeHeartbeatTimer() +{ + if (m_heartbeatTimer != NULL) { + m_events->deleteTimer(m_heartbeatTimer); + m_heartbeatTimer = NULL; + } +} + +void +ClientProxy1_0::resetHeartbeatTimer() +{ + // reset the alarm + removeHeartbeatTimer(); + addHeartbeatTimer(); +} + +void +ClientProxy1_0::resetHeartbeatRate() +{ + setHeartbeatRate(kHeartRate, kHeartRate * kHeartBeatsUntilDeath); +} + +void +ClientProxy1_0::setHeartbeatRate(double, double alarm) +{ + m_heartbeatAlarm = alarm; +} + +void +ClientProxy1_0::handleData(const Event&, void*) +{ + // handle messages until there are no more. first read message code. + UInt8 code[4]; + UInt32 n = getStream()->read(code, 4); + while (n != 0) { + // verify we got an entire code + if (n != 4) { + LOG((CLOG_ERR "incomplete message from \"%s\": %d bytes", getName().c_str(), n)); + disconnect(); + return; + } + + // parse message + LOG((CLOG_DEBUG2 "msg from \"%s\": %c%c%c%c", getName().c_str(), code[0], code[1], code[2], code[3])); + if (!(this->*m_parser)(code)) { + LOG((CLOG_ERR "invalid message from client \"%s\": %c%c%c%c", getName().c_str(), code[0], code[1], code[2], code[3])); + disconnect(); + return; + } + + // next message + n = getStream()->read(code, 4); + } + + // restart heartbeat timer + resetHeartbeatTimer(); +} + +bool +ClientProxy1_0::parseHandshakeMessage(const UInt8* code) +{ + if (memcmp(code, kMsgCNoop, 4) == 0) { + // discard no-ops + LOG((CLOG_DEBUG2 "no-op from", getName().c_str())); + return true; + } + else if (memcmp(code, kMsgDInfo, 4) == 0) { + // future messages get parsed by parseMessage + m_parser = &ClientProxy1_0::parseMessage; + if (recvInfo()) { + m_events->addEvent(Event(m_events->forClientProxy().ready(), getEventTarget())); + addHeartbeatTimer(); + return true; + } + } + return false; +} + +bool +ClientProxy1_0::parseMessage(const UInt8* code) +{ + if (memcmp(code, kMsgDInfo, 4) == 0) { + if (recvInfo()) { + m_events->addEvent( + Event(m_events->forIScreen().shapeChanged(), getEventTarget())); + return true; + } + return false; + } + else if (memcmp(code, kMsgCNoop, 4) == 0) { + // discard no-ops + LOG((CLOG_DEBUG2 "no-op from", getName().c_str())); + return true; + } + else if (memcmp(code, kMsgCClipboard, 4) == 0) { + return recvGrabClipboard(); + } + else if (memcmp(code, kMsgDClipboard, 4) == 0) { + return recvClipboard(); + } + return false; +} + +void +ClientProxy1_0::handleDisconnect(const Event&, void*) +{ + LOG((CLOG_NOTE "client \"%s\" has disconnected", getName().c_str())); + disconnect(); +} + +void +ClientProxy1_0::handleWriteError(const Event&, void*) +{ + LOG((CLOG_WARN "error writing to client \"%s\"", getName().c_str())); + disconnect(); +} + +void +ClientProxy1_0::handleFlatline(const Event&, void*) +{ + // didn't get a heartbeat fast enough. assume client is dead. + LOG((CLOG_NOTE "client \"%s\" is dead", getName().c_str())); + disconnect(); +} + +bool +ClientProxy1_0::getClipboard(ClipboardID id, IClipboard* clipboard) const +{ + Clipboard::copy(clipboard, &m_clipboard[id].m_clipboard); + return true; +} + +void +ClientProxy1_0::getShape(SInt32& x, SInt32& y, SInt32& w, SInt32& h) const +{ + x = m_info.m_x; + y = m_info.m_y; + w = m_info.m_w; + h = m_info.m_h; +} + +void +ClientProxy1_0::getCursorPos(SInt32& x, SInt32& y) const +{ + // note -- this returns the cursor pos from when we last got client info + x = m_info.m_mx; + y = m_info.m_my; +} + +void +ClientProxy1_0::enter(SInt32 xAbs, SInt32 yAbs, + UInt32 seqNum, KeyModifierMask mask, bool) +{ + LOG((CLOG_DEBUG1 "send enter to \"%s\", %d,%d %d %04x", getName().c_str(), xAbs, yAbs, seqNum, mask)); + ProtocolUtil::writef(getStream(), kMsgCEnter, + xAbs, yAbs, seqNum, mask); +} + +bool +ClientProxy1_0::leave() +{ + LOG((CLOG_DEBUG1 "send leave to \"%s\"", getName().c_str())); + ProtocolUtil::writef(getStream(), kMsgCLeave); + + // we can never prevent the user from leaving + return true; +} + +void +ClientProxy1_0::setClipboard(ClipboardID id, const IClipboard* clipboard) +{ + // ignore -- deprecated in protocol 1.0 +} + +void +ClientProxy1_0::grabClipboard(ClipboardID id) +{ + LOG((CLOG_DEBUG "send grab clipboard %d to \"%s\"", id, getName().c_str())); + ProtocolUtil::writef(getStream(), kMsgCClipboard, id, 0); + + // this clipboard is now dirty + m_clipboard[id].m_dirty = true; +} + +void +ClientProxy1_0::setClipboardDirty(ClipboardID id, bool dirty) +{ + m_clipboard[id].m_dirty = dirty; +} + +void +ClientProxy1_0::keyDown(KeyID key, KeyModifierMask mask, KeyButton) +{ + LOG((CLOG_DEBUG1 "send key down to \"%s\" id=%d, mask=0x%04x", getName().c_str(), key, mask)); + ProtocolUtil::writef(getStream(), kMsgDKeyDown1_0, key, mask); +} + +void +ClientProxy1_0::keyRepeat(KeyID key, KeyModifierMask mask, + SInt32 count, KeyButton) +{ + LOG((CLOG_DEBUG1 "send key repeat to \"%s\" id=%d, mask=0x%04x, count=%d", getName().c_str(), key, mask, count)); + ProtocolUtil::writef(getStream(), kMsgDKeyRepeat1_0, key, mask, count); +} + +void +ClientProxy1_0::keyUp(KeyID key, KeyModifierMask mask, KeyButton) +{ + LOG((CLOG_DEBUG1 "send key up to \"%s\" id=%d, mask=0x%04x", getName().c_str(), key, mask)); + ProtocolUtil::writef(getStream(), kMsgDKeyUp1_0, key, mask); +} + +void +ClientProxy1_0::mouseDown(ButtonID button) +{ + LOG((CLOG_DEBUG1 "send mouse down to \"%s\" id=%d", getName().c_str(), button)); + ProtocolUtil::writef(getStream(), kMsgDMouseDown, button); +} + +void +ClientProxy1_0::mouseUp(ButtonID button) +{ + LOG((CLOG_DEBUG1 "send mouse up to \"%s\" id=%d", getName().c_str(), button)); + ProtocolUtil::writef(getStream(), kMsgDMouseUp, button); +} + +void +ClientProxy1_0::mouseMove(SInt32 xAbs, SInt32 yAbs) +{ + LOG((CLOG_DEBUG2 "send mouse move to \"%s\" %d,%d", getName().c_str(), xAbs, yAbs)); + ProtocolUtil::writef(getStream(), kMsgDMouseMove, xAbs, yAbs); +} + +void +ClientProxy1_0::mouseRelativeMove(SInt32, SInt32) +{ + // ignore -- not supported in protocol 1.0 +} + +void +ClientProxy1_0::mouseWheel(SInt32, SInt32 yDelta) +{ + // clients prior to 1.3 only support the y axis + LOG((CLOG_DEBUG2 "send mouse wheel to \"%s\" %+d", getName().c_str(), yDelta)); + ProtocolUtil::writef(getStream(), kMsgDMouseWheel1_0, yDelta); +} + +void +ClientProxy1_0::sendDragInfo(UInt32 fileCount, const char* info, size_t size) +{ + // ignore -- not supported in protocol 1.0 + LOG((CLOG_DEBUG "draggingInfoSending not supported")); +} + +void +ClientProxy1_0::fileChunkSending(UInt8 mark, char* data, size_t dataSize) +{ + // ignore -- not supported in protocol 1.0 + LOG((CLOG_DEBUG "fileChunkSending not supported")); +} + +void +ClientProxy1_0::screensaver(bool on) +{ + LOG((CLOG_DEBUG1 "send screen saver to \"%s\" on=%d", getName().c_str(), on ? 1 : 0)); + ProtocolUtil::writef(getStream(), kMsgCScreenSaver, on ? 1 : 0); +} + +void +ClientProxy1_0::resetOptions() +{ + LOG((CLOG_DEBUG1 "send reset options to \"%s\"", getName().c_str())); + ProtocolUtil::writef(getStream(), kMsgCResetOptions); + + // reset heart rate and death + resetHeartbeatRate(); + removeHeartbeatTimer(); + addHeartbeatTimer(); +} + +void +ClientProxy1_0::setOptions(const OptionsList& options) +{ + LOG((CLOG_DEBUG1 "send set options to \"%s\" size=%d", getName().c_str(), options.size())); + ProtocolUtil::writef(getStream(), kMsgDSetOptions, &options); + + // check options + for (UInt32 i = 0, n = (UInt32)options.size(); i < n; i += 2) { + if (options[i] == kOptionHeartbeat) { + double rate = 1.0e-3 * static_cast<double>(options[i + 1]); + if (rate <= 0.0) { + rate = -1.0; + } + setHeartbeatRate(rate, rate * kHeartBeatsUntilDeath); + removeHeartbeatTimer(); + addHeartbeatTimer(); + } + } +} + +bool +ClientProxy1_0::recvInfo() +{ + // parse the message + SInt16 x, y, w, h, dummy1, mx, my; + if (!ProtocolUtil::readf(getStream(), kMsgDInfo + 4, + &x, &y, &w, &h, &dummy1, &mx, &my)) { + return false; + } + LOG((CLOG_DEBUG "received client \"%s\" info shape=%d,%d %dx%d at %d,%d", getName().c_str(), x, y, w, h, mx, my)); + + // validate + if (w <= 0 || h <= 0) { + return false; + } + if (mx < x || mx >= x + w || my < y || my >= y + h) { + mx = x + w / 2; + my = y + h / 2; + } + + // save + m_info.m_x = x; + m_info.m_y = y; + m_info.m_w = w; + m_info.m_h = h; + m_info.m_mx = mx; + m_info.m_my = my; + + // acknowledge receipt + LOG((CLOG_DEBUG1 "send info ack to \"%s\"", getName().c_str())); + ProtocolUtil::writef(getStream(), kMsgCInfoAck); + return true; +} + +bool +ClientProxy1_0::recvClipboard() +{ + // deprecated in protocol 1.0 + return false; +} + +bool +ClientProxy1_0::recvGrabClipboard() +{ + // parse message + ClipboardID id; + UInt32 seqNum; + if (!ProtocolUtil::readf(getStream(), kMsgCClipboard + 4, &id, &seqNum)) { + return false; + } + LOG((CLOG_DEBUG "received client \"%s\" grabbed clipboard %d seqnum=%d", getName().c_str(), id, seqNum)); + + // validate + if (id >= kClipboardEnd) { + return false; + } + + // notify + ClipboardInfo* info = new ClipboardInfo; + info->m_id = id; + info->m_sequenceNumber = seqNum; + m_events->addEvent(Event(m_events->forClipboard().clipboardGrabbed(), + getEventTarget(), info)); + + return true; +} + +// +// ClientProxy1_0::ClientClipboard +// + +ClientProxy1_0::ClientClipboard::ClientClipboard() : + m_clipboard(), + m_sequenceNumber(0), + m_dirty(true) +{ + // do nothing +} diff --git a/src/lib/server/ClientProxy1_0.h b/src/lib/server/ClientProxy1_0.h new file mode 100644 index 0000000..0720232 --- /dev/null +++ b/src/lib/server/ClientProxy1_0.h @@ -0,0 +1,107 @@ +/* + * barrier -- mouse and keyboard sharing utility + * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2002 Chris Schoeneman + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "server/ClientProxy.h" +#include "barrier/Clipboard.h" +#include "barrier/protocol_types.h" + +class Event; +class EventQueueTimer; +class IEventQueue; + +//! Proxy for client implementing protocol version 1.0 +class ClientProxy1_0 : public ClientProxy { +public: + ClientProxy1_0(const String& name, barrier::IStream* adoptedStream, IEventQueue* events); + ~ClientProxy1_0(); + + // IScreen + virtual bool getClipboard(ClipboardID id, IClipboard*) const; + virtual void getShape(SInt32& x, SInt32& y, + SInt32& width, SInt32& height) const; + virtual void getCursorPos(SInt32& x, SInt32& y) const; + + // IClient overrides + virtual void enter(SInt32 xAbs, SInt32 yAbs, + UInt32 seqNum, KeyModifierMask mask, + bool forScreensaver); + virtual bool leave(); + virtual void setClipboard(ClipboardID, const IClipboard*); + virtual void grabClipboard(ClipboardID); + virtual void setClipboardDirty(ClipboardID, bool); + virtual void keyDown(KeyID, KeyModifierMask, KeyButton); + virtual void keyRepeat(KeyID, KeyModifierMask, + SInt32 count, KeyButton); + virtual void keyUp(KeyID, KeyModifierMask, KeyButton); + virtual void mouseDown(ButtonID); + virtual void mouseUp(ButtonID); + virtual void mouseMove(SInt32 xAbs, SInt32 yAbs); + virtual void mouseRelativeMove(SInt32 xRel, SInt32 yRel); + virtual void mouseWheel(SInt32 xDelta, SInt32 yDelta); + virtual void screensaver(bool activate); + virtual void resetOptions(); + virtual void setOptions(const OptionsList& options); + virtual void sendDragInfo(UInt32 fileCount, const char* info, size_t size); + virtual void fileChunkSending(UInt8 mark, char* data, size_t dataSize); + +protected: + virtual bool parseHandshakeMessage(const UInt8* code); + virtual bool parseMessage(const UInt8* code); + + virtual void resetHeartbeatRate(); + virtual void setHeartbeatRate(double rate, double alarm); + virtual void resetHeartbeatTimer(); + virtual void addHeartbeatTimer(); + virtual void removeHeartbeatTimer(); + virtual bool recvClipboard(); +private: + void disconnect(); + void removeHandlers(); + + void handleData(const Event&, void*); + void handleDisconnect(const Event&, void*); + void handleWriteError(const Event&, void*); + void handleFlatline(const Event&, void*); + + bool recvInfo(); + bool recvGrabClipboard(); + +protected: + struct ClientClipboard { + public: + ClientClipboard(); + + public: + Clipboard m_clipboard; + UInt32 m_sequenceNumber; + bool m_dirty; + }; + + ClientClipboard m_clipboard[kClipboardEnd]; + +private: + typedef bool (ClientProxy1_0::*MessageParser)(const UInt8*); + + ClientInfo m_info; + double m_heartbeatAlarm; + EventQueueTimer* m_heartbeatTimer; + MessageParser m_parser; + IEventQueue* m_events; +}; diff --git a/src/lib/server/ClientProxy1_1.cpp b/src/lib/server/ClientProxy1_1.cpp new file mode 100644 index 0000000..b7eb4c4 --- /dev/null +++ b/src/lib/server/ClientProxy1_1.cpp @@ -0,0 +1,61 @@ +/* + * barrier -- mouse and keyboard sharing utility + * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2002 Chris Schoeneman + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "server/ClientProxy1_1.h" + +#include "barrier/ProtocolUtil.h" +#include "base/Log.h" + +#include <cstring> + +// +// ClientProxy1_1 +// + +ClientProxy1_1::ClientProxy1_1(const String& name, barrier::IStream* stream, IEventQueue* events) : + ClientProxy1_0(name, stream, events) +{ + // do nothing +} + +ClientProxy1_1::~ClientProxy1_1() +{ + // do nothing +} + +void +ClientProxy1_1::keyDown(KeyID key, KeyModifierMask mask, KeyButton button) +{ + LOG((CLOG_DEBUG1 "send key down to \"%s\" id=%d, mask=0x%04x, button=0x%04x", getName().c_str(), key, mask, button)); + ProtocolUtil::writef(getStream(), kMsgDKeyDown, key, mask, button); +} + +void +ClientProxy1_1::keyRepeat(KeyID key, KeyModifierMask mask, + SInt32 count, KeyButton button) +{ + LOG((CLOG_DEBUG1 "send key repeat to \"%s\" id=%d, mask=0x%04x, count=%d, button=0x%04x", getName().c_str(), key, mask, count, button)); + ProtocolUtil::writef(getStream(), kMsgDKeyRepeat, key, mask, count, button); +} + +void +ClientProxy1_1::keyUp(KeyID key, KeyModifierMask mask, KeyButton button) +{ + LOG((CLOG_DEBUG1 "send key up to \"%s\" id=%d, mask=0x%04x, button=0x%04x", getName().c_str(), key, mask, button)); + ProtocolUtil::writef(getStream(), kMsgDKeyUp, key, mask, button); +} diff --git a/src/lib/server/ClientProxy1_1.h b/src/lib/server/ClientProxy1_1.h new file mode 100644 index 0000000..cdb674d --- /dev/null +++ b/src/lib/server/ClientProxy1_1.h @@ -0,0 +1,34 @@ +/* + * barrier -- mouse and keyboard sharing utility + * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2002 Chris Schoeneman + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "server/ClientProxy1_0.h" + +//! Proxy for client implementing protocol version 1.1 +class ClientProxy1_1 : public ClientProxy1_0 { +public: + ClientProxy1_1(const String& name, barrier::IStream* adoptedStream, IEventQueue* events); + ~ClientProxy1_1(); + + // IClient overrides + virtual void keyDown(KeyID, KeyModifierMask, KeyButton); + virtual void keyRepeat(KeyID, KeyModifierMask, + SInt32 count, KeyButton); + virtual void keyUp(KeyID, KeyModifierMask, KeyButton); +}; diff --git a/src/lib/server/ClientProxy1_2.cpp b/src/lib/server/ClientProxy1_2.cpp new file mode 100644 index 0000000..2dd13cf --- /dev/null +++ b/src/lib/server/ClientProxy1_2.cpp @@ -0,0 +1,44 @@ +/* + * barrier -- mouse and keyboard sharing utility + * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2002 Chris Schoeneman + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "server/ClientProxy1_2.h" + +#include "barrier/ProtocolUtil.h" +#include "base/Log.h" + +// +// ClientProxy1_1 +// + +ClientProxy1_2::ClientProxy1_2(const String& name, barrier::IStream* stream, IEventQueue* events) : + ClientProxy1_1(name, stream, events) +{ + // do nothing +} + +ClientProxy1_2::~ClientProxy1_2() +{ + // do nothing +} + +void +ClientProxy1_2::mouseRelativeMove(SInt32 xRel, SInt32 yRel) +{ + LOG((CLOG_DEBUG2 "send mouse relative move to \"%s\" %d,%d", getName().c_str(), xRel, yRel)); + ProtocolUtil::writef(getStream(), kMsgDMouseRelMove, xRel, yRel); +} diff --git a/src/lib/server/ClientProxy1_2.h b/src/lib/server/ClientProxy1_2.h new file mode 100644 index 0000000..f6ffe94 --- /dev/null +++ b/src/lib/server/ClientProxy1_2.h @@ -0,0 +1,33 @@ +/* + * barrier -- mouse and keyboard sharing utility + * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2004 Chris Schoeneman + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "server/ClientProxy1_1.h" + +class IEventQueue; + +//! Proxy for client implementing protocol version 1.2 +class ClientProxy1_2 : public ClientProxy1_1 { +public: + ClientProxy1_2(const String& name, barrier::IStream* adoptedStream, IEventQueue* events); + ~ClientProxy1_2(); + + // IClient overrides + virtual void mouseRelativeMove(SInt32 xRel, SInt32 yRel); +}; diff --git a/src/lib/server/ClientProxy1_3.cpp b/src/lib/server/ClientProxy1_3.cpp new file mode 100644 index 0000000..34ea0c8 --- /dev/null +++ b/src/lib/server/ClientProxy1_3.cpp @@ -0,0 +1,129 @@ +/* + * barrier -- mouse and keyboard sharing utility + * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2006 Chris Schoeneman + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "server/ClientProxy1_3.h" + +#include "barrier/ProtocolUtil.h" +#include "base/Log.h" +#include "base/IEventQueue.h" +#include "base/TMethodEventJob.h" + +#include <cstring> +#include <memory> + +// +// ClientProxy1_3 +// + +ClientProxy1_3::ClientProxy1_3(const String& name, barrier::IStream* stream, IEventQueue* events) : + ClientProxy1_2(name, stream, events), + m_keepAliveRate(kKeepAliveRate), + m_keepAliveTimer(NULL), + m_events(events) +{ + setHeartbeatRate(kKeepAliveRate, kKeepAliveRate * kKeepAlivesUntilDeath); +} + +ClientProxy1_3::~ClientProxy1_3() +{ + // cannot do this in superclass or our override wouldn't get called + removeHeartbeatTimer(); +} + +void +ClientProxy1_3::mouseWheel(SInt32 xDelta, SInt32 yDelta) +{ + LOG((CLOG_DEBUG2 "send mouse wheel to \"%s\" %+d,%+d", getName().c_str(), xDelta, yDelta)); + ProtocolUtil::writef(getStream(), kMsgDMouseWheel, xDelta, yDelta); +} + +bool +ClientProxy1_3::parseMessage(const UInt8* code) +{ + // process message + if (memcmp(code, kMsgCKeepAlive, 4) == 0) { + // reset alarm + resetHeartbeatTimer(); + return true; + } + else { + return ClientProxy1_2::parseMessage(code); + } +} + +void +ClientProxy1_3::resetHeartbeatRate() +{ + setHeartbeatRate(kKeepAliveRate, kKeepAliveRate * kKeepAlivesUntilDeath); +} + +void +ClientProxy1_3::setHeartbeatRate(double rate, double) +{ + m_keepAliveRate = rate; + ClientProxy1_2::setHeartbeatRate(rate, rate * kKeepAlivesUntilDeath); +} + +void +ClientProxy1_3::resetHeartbeatTimer() +{ + // reset the alarm but not the keep alive timer + ClientProxy1_2::removeHeartbeatTimer(); + ClientProxy1_2::addHeartbeatTimer(); +} + +void +ClientProxy1_3::addHeartbeatTimer() +{ + // create and install a timer to periodically send keep alives + if (m_keepAliveRate > 0.0) { + m_keepAliveTimer = m_events->newTimer(m_keepAliveRate, NULL); + m_events->adoptHandler(Event::kTimer, m_keepAliveTimer, + new TMethodEventJob<ClientProxy1_3>(this, + &ClientProxy1_3::handleKeepAlive, NULL)); + } + + // superclass does the alarm + ClientProxy1_2::addHeartbeatTimer(); +} + +void +ClientProxy1_3::removeHeartbeatTimer() +{ + // remove the timer that sends keep alives periodically + if (m_keepAliveTimer != NULL) { + m_events->removeHandler(Event::kTimer, m_keepAliveTimer); + m_events->deleteTimer(m_keepAliveTimer); + m_keepAliveTimer = NULL; + } + + // superclass does the alarm + ClientProxy1_2::removeHeartbeatTimer(); +} + +void +ClientProxy1_3::handleKeepAlive(const Event&, void*) +{ + keepAlive(); +} + +void +ClientProxy1_3::keepAlive() +{ + ProtocolUtil::writef(getStream(), kMsgCKeepAlive); +} diff --git a/src/lib/server/ClientProxy1_3.h b/src/lib/server/ClientProxy1_3.h new file mode 100644 index 0000000..ff2ed0a --- /dev/null +++ b/src/lib/server/ClientProxy1_3.h @@ -0,0 +1,48 @@ +/* + * barrier -- mouse and keyboard sharing utility + * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2006 Chris Schoeneman + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "server/ClientProxy1_2.h" + +//! Proxy for client implementing protocol version 1.3 +class ClientProxy1_3 : public ClientProxy1_2 { +public: + ClientProxy1_3(const String& name, barrier::IStream* adoptedStream, IEventQueue* events); + ~ClientProxy1_3(); + + // IClient overrides + virtual void mouseWheel(SInt32 xDelta, SInt32 yDelta); + + void handleKeepAlive(const Event&, void*); + +protected: + // ClientProxy overrides + virtual bool parseMessage(const UInt8* code); + virtual void resetHeartbeatRate(); + virtual void setHeartbeatRate(double rate, double alarm); + virtual void resetHeartbeatTimer(); + virtual void addHeartbeatTimer(); + virtual void removeHeartbeatTimer(); + virtual void keepAlive(); + +private: + double m_keepAliveRate; + EventQueueTimer* m_keepAliveTimer; + IEventQueue* m_events; +}; diff --git a/src/lib/server/ClientProxy1_4.cpp b/src/lib/server/ClientProxy1_4.cpp new file mode 100644 index 0000000..43c708d --- /dev/null +++ b/src/lib/server/ClientProxy1_4.cpp @@ -0,0 +1,66 @@ +/* + * barrier -- mouse and keyboard sharing utility + * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2011 Chris Schoeneman + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "server/ClientProxy1_4.h" + +#include "server/Server.h" +#include "barrier/ProtocolUtil.h" +#include "base/Log.h" +#include "base/IEventQueue.h" +#include "base/TMethodEventJob.h" + +#include <cstring> +#include <memory> + +// +// ClientProxy1_4 +// + +ClientProxy1_4::ClientProxy1_4(const String& name, barrier::IStream* stream, Server* server, IEventQueue* events) : + ClientProxy1_3(name, stream, events), m_server(server) +{ + assert(m_server != NULL); +} + +ClientProxy1_4::~ClientProxy1_4() +{ +} + +void +ClientProxy1_4::keyDown(KeyID key, KeyModifierMask mask, KeyButton button) +{ + ClientProxy1_3::keyDown(key, mask, button); +} + +void +ClientProxy1_4::keyRepeat(KeyID key, KeyModifierMask mask, SInt32 count, KeyButton button) +{ + ClientProxy1_3::keyRepeat(key, mask, count, button); +} + +void +ClientProxy1_4::keyUp(KeyID key, KeyModifierMask mask, KeyButton button) +{ + ClientProxy1_3::keyUp(key, mask, button); +} + +void +ClientProxy1_4::keepAlive() +{ + ClientProxy1_3::keepAlive(); +} diff --git a/src/lib/server/ClientProxy1_4.h b/src/lib/server/ClientProxy1_4.h new file mode 100644 index 0000000..6c55965 --- /dev/null +++ b/src/lib/server/ClientProxy1_4.h @@ -0,0 +1,46 @@ +/* + * barrier -- mouse and keyboard sharing utility + * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2011 Chris Schoeneman + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "server/ClientProxy1_3.h" + +class Server; + +//! Proxy for client implementing protocol version 1.4 +class ClientProxy1_4 : public ClientProxy1_3 { +public: + ClientProxy1_4(const String& name, barrier::IStream* adoptedStream, Server* server, IEventQueue* events); + ~ClientProxy1_4(); + + //! @name accessors + //@{ + + //! get server pointer + Server* getServer() { return m_server; } + + //@} + + // IClient overrides + virtual void keyDown(KeyID key, KeyModifierMask mask, KeyButton button); + virtual void keyRepeat(KeyID key, KeyModifierMask mask, SInt32 count, KeyButton button); + virtual void keyUp(KeyID key, KeyModifierMask mask, KeyButton button); + virtual void keepAlive(); + + Server* m_server; +}; diff --git a/src/lib/server/ClientProxy1_5.cpp b/src/lib/server/ClientProxy1_5.cpp new file mode 100644 index 0000000..43fd0b7 --- /dev/null +++ b/src/lib/server/ClientProxy1_5.cpp @@ -0,0 +1,110 @@ +/* + * barrier -- mouse and keyboard sharing utility + * Copyright (C) 2013-2016 Symless Ltd. + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "server/ClientProxy1_5.h" + +#include "server/Server.h" +#include "barrier/FileChunk.h" +#include "barrier/StreamChunker.h" +#include "barrier/ProtocolUtil.h" +#include "io/IStream.h" +#include "base/TMethodEventJob.h" +#include "base/Log.h" + +#include <sstream> + +// +// ClientProxy1_5 +// + +ClientProxy1_5::ClientProxy1_5(const String& name, barrier::IStream* stream, Server* server, IEventQueue* events) : + ClientProxy1_4(name, stream, server, events), + m_events(events) +{ + + m_events->adoptHandler(m_events->forFile().keepAlive(), + this, + new TMethodEventJob<ClientProxy1_3>(this, + &ClientProxy1_3::handleKeepAlive, NULL)); +} + +ClientProxy1_5::~ClientProxy1_5() +{ + m_events->removeHandler(m_events->forFile().keepAlive(), this); +} + +void +ClientProxy1_5::sendDragInfo(UInt32 fileCount, const char* info, size_t size) +{ + String data(info, size); + + ProtocolUtil::writef(getStream(), kMsgDDragInfo, fileCount, &data); +} + +void +ClientProxy1_5::fileChunkSending(UInt8 mark, char* data, size_t dataSize) +{ + FileChunk::send(getStream(), mark, data, dataSize); +} + +bool +ClientProxy1_5::parseMessage(const UInt8* code) +{ + if (memcmp(code, kMsgDFileTransfer, 4) == 0) { + fileChunkReceived(); + } + else if (memcmp(code, kMsgDDragInfo, 4) == 0) { + dragInfoReceived(); + } + else { + return ClientProxy1_4::parseMessage(code); + } + + return true; +} + +void +ClientProxy1_5::fileChunkReceived() +{ + Server* server = getServer(); + int result = FileChunk::assemble( + getStream(), + server->getReceivedFileData(), + server->getExpectedFileSize()); + + + if (result == kFinish) { + m_events->addEvent(Event(m_events->forFile().fileRecieveCompleted(), server)); + } + else if (result == kStart) { + if (server->getFakeDragFileList().size() > 0) { + String filename = server->getFakeDragFileList().at(0).getFilename(); + LOG((CLOG_DEBUG "start receiving %s", filename.c_str())); + } + } +} + +void +ClientProxy1_5::dragInfoReceived() +{ + // parse + UInt32 fileNum = 0; + String content; + ProtocolUtil::readf(getStream(), kMsgDDragInfo + 4, &fileNum, &content); + + m_server->dragInfoReceived(fileNum, content); +} diff --git a/src/lib/server/ClientProxy1_5.h b/src/lib/server/ClientProxy1_5.h new file mode 100644 index 0000000..776de03 --- /dev/null +++ b/src/lib/server/ClientProxy1_5.h @@ -0,0 +1,41 @@ +/* + * barrier -- mouse and keyboard sharing utility + * Copyright (C) 2013-2016 Symless Ltd. + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "server/ClientProxy1_4.h" +#include "base/Stopwatch.h" +#include "common/stdvector.h" + +class Server; +class IEventQueue; + +//! Proxy for client implementing protocol version 1.5 +class ClientProxy1_5 : public ClientProxy1_4 { +public: + ClientProxy1_5(const String& name, barrier::IStream* adoptedStream, Server* server, IEventQueue* events); + ~ClientProxy1_5(); + + virtual void sendDragInfo(UInt32 fileCount, const char* info, size_t size); + virtual void fileChunkSending(UInt8 mark, char* data, size_t dataSize); + virtual bool parseMessage(const UInt8* code); + void fileChunkReceived(); + void dragInfoReceived(); + +private: + IEventQueue* m_events; +}; diff --git a/src/lib/server/ClientProxy1_6.cpp b/src/lib/server/ClientProxy1_6.cpp new file mode 100644 index 0000000..a0d2621 --- /dev/null +++ b/src/lib/server/ClientProxy1_6.cpp @@ -0,0 +1,100 @@ +/* + * barrier -- mouse and keyboard sharing utility + * Copyright (C) 2015-2016 Symless Ltd. + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "server/ClientProxy1_6.h" + +#include "server/Server.h" +#include "barrier/ProtocolUtil.h" +#include "barrier/StreamChunker.h" +#include "barrier/ClipboardChunk.h" +#include "io/IStream.h" +#include "base/TMethodEventJob.h" +#include "base/Log.h" + +// +// ClientProxy1_6 +// + +ClientProxy1_6::ClientProxy1_6(const String& name, barrier::IStream* stream, Server* server, IEventQueue* events) : + ClientProxy1_5(name, stream, server, events), + m_events(events) +{ + m_events->adoptHandler(m_events->forClipboard().clipboardSending(), + this, + new TMethodEventJob<ClientProxy1_6>(this, + &ClientProxy1_6::handleClipboardSendingEvent)); +} + +ClientProxy1_6::~ClientProxy1_6() +{ +} + +void +ClientProxy1_6::setClipboard(ClipboardID id, const IClipboard* clipboard) +{ + // ignore if this clipboard is already clean + if (m_clipboard[id].m_dirty) { + // this clipboard is now clean + m_clipboard[id].m_dirty = false; + Clipboard::copy(&m_clipboard[id].m_clipboard, clipboard); + + String data = m_clipboard[id].m_clipboard.marshall(); + + size_t size = data.size(); + LOG((CLOG_DEBUG "sending clipboard %d to \"%s\"", id, getName().c_str())); + + StreamChunker::sendClipboard(data, size, id, 0, m_events, this); + } +} + +void +ClientProxy1_6::handleClipboardSendingEvent(const Event& event, void*) +{ + ClipboardChunk::send(getStream(), event.getData()); +} + +bool +ClientProxy1_6::recvClipboard() +{ + // parse message + static String dataCached; + ClipboardID id; + UInt32 seq; + + int r = ClipboardChunk::assemble(getStream(), dataCached, id, seq); + + if (r == kStart) { + size_t size = ClipboardChunk::getExpectedSize(); + LOG((CLOG_DEBUG "receiving clipboard %d size=%d", id, size)); + } + else if (r == kFinish) { + LOG((CLOG_DEBUG "received client \"%s\" clipboard %d seqnum=%d, size=%d", + getName().c_str(), id, seq, dataCached.size())); + // save clipboard + m_clipboard[id].m_clipboard.unmarshall(dataCached, 0); + m_clipboard[id].m_sequenceNumber = seq; + + // notify + ClipboardInfo* info = new ClipboardInfo; + info->m_id = id; + info->m_sequenceNumber = seq; + m_events->addEvent(Event(m_events->forClipboard().clipboardChanged(), + getEventTarget(), info)); + } + + return true; +} diff --git a/src/lib/server/ClientProxy1_6.h b/src/lib/server/ClientProxy1_6.h new file mode 100644 index 0000000..838cb02 --- /dev/null +++ b/src/lib/server/ClientProxy1_6.h @@ -0,0 +1,39 @@ +/* + * barrier -- mouse and keyboard sharing utility + * Copyright (C) 2015-2016 Symless Ltd. + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "server/ClientProxy1_5.h" + +class Server; +class IEventQueue; + +//! Proxy for client implementing protocol version 1.6 +class ClientProxy1_6 : public ClientProxy1_5 { +public: + ClientProxy1_6(const String& name, barrier::IStream* adoptedStream, Server* server, IEventQueue* events); + ~ClientProxy1_6(); + + virtual void setClipboard(ClipboardID id, const IClipboard* clipboard); + virtual bool recvClipboard(); + +private: + void handleClipboardSendingEvent(const Event&, void*); + +private: + IEventQueue* m_events; +}; diff --git a/src/lib/server/ClientProxyUnknown.cpp b/src/lib/server/ClientProxyUnknown.cpp new file mode 100644 index 0000000..f929108 --- /dev/null +++ b/src/lib/server/ClientProxyUnknown.cpp @@ -0,0 +1,295 @@ +/* + * barrier -- mouse and keyboard sharing utility + * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2004 Chris Schoeneman + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "server/ClientProxyUnknown.h" + +#include "server/Server.h" +#include "server/ClientProxy1_0.h" +#include "server/ClientProxy1_1.h" +#include "server/ClientProxy1_2.h" +#include "server/ClientProxy1_3.h" +#include "server/ClientProxy1_4.h" +#include "server/ClientProxy1_5.h" +#include "server/ClientProxy1_6.h" +#include "barrier/protocol_types.h" +#include "barrier/ProtocolUtil.h" +#include "barrier/XBarrier.h" +#include "io/IStream.h" +#include "io/XIO.h" +#include "base/Log.h" +#include "base/String.h" +#include "base/IEventQueue.h" +#include "base/TMethodEventJob.h" + +// +// ClientProxyUnknown +// + +ClientProxyUnknown::ClientProxyUnknown(barrier::IStream* stream, double timeout, Server* server, IEventQueue* events) : + m_stream(stream), + m_proxy(NULL), + m_ready(false), + m_server(server), + m_events(events) +{ + assert(m_server != NULL); + + m_events->adoptHandler(Event::kTimer, this, + new TMethodEventJob<ClientProxyUnknown>(this, + &ClientProxyUnknown::handleTimeout, NULL)); + m_timer = m_events->newOneShotTimer(timeout, this); + addStreamHandlers(); + + LOG((CLOG_DEBUG1 "saying hello")); + ProtocolUtil::writef(m_stream, kMsgHello, + kProtocolMajorVersion, + kProtocolMinorVersion); +} + +ClientProxyUnknown::~ClientProxyUnknown() +{ + removeHandlers(); + removeTimer(); + delete m_stream; + delete m_proxy; +} + +ClientProxy* +ClientProxyUnknown::orphanClientProxy() +{ + if (m_ready) { + removeHandlers(); + ClientProxy* proxy = m_proxy; + m_proxy = NULL; + return proxy; + } + else { + return NULL; + } +} + +void +ClientProxyUnknown::sendSuccess() +{ + m_ready = true; + removeTimer(); + m_events->addEvent(Event(m_events->forClientProxyUnknown().success(), this)); +} + +void +ClientProxyUnknown::sendFailure() +{ + delete m_proxy; + m_proxy = NULL; + m_ready = false; + removeHandlers(); + removeTimer(); + m_events->addEvent(Event(m_events->forClientProxyUnknown().failure(), this)); +} + +void +ClientProxyUnknown::addStreamHandlers() +{ + assert(m_stream != NULL); + + m_events->adoptHandler(m_events->forIStream().inputReady(), + m_stream->getEventTarget(), + new TMethodEventJob<ClientProxyUnknown>(this, + &ClientProxyUnknown::handleData)); + m_events->adoptHandler(m_events->forIStream().outputError(), + m_stream->getEventTarget(), + new TMethodEventJob<ClientProxyUnknown>(this, + &ClientProxyUnknown::handleWriteError)); + m_events->adoptHandler(m_events->forIStream().inputShutdown(), + m_stream->getEventTarget(), + new TMethodEventJob<ClientProxyUnknown>(this, + &ClientProxyUnknown::handleDisconnect)); + m_events->adoptHandler(m_events->forIStream().outputShutdown(), + m_stream->getEventTarget(), + new TMethodEventJob<ClientProxyUnknown>(this, + &ClientProxyUnknown::handleWriteError)); +} + +void +ClientProxyUnknown::addProxyHandlers() +{ + assert(m_proxy != NULL); + + m_events->adoptHandler(m_events->forClientProxy().ready(), + m_proxy, + new TMethodEventJob<ClientProxyUnknown>(this, + &ClientProxyUnknown::handleReady)); + m_events->adoptHandler(m_events->forClientProxy().disconnected(), + m_proxy, + new TMethodEventJob<ClientProxyUnknown>(this, + &ClientProxyUnknown::handleDisconnect)); +} + +void +ClientProxyUnknown::removeHandlers() +{ + if (m_stream != NULL) { + m_events->removeHandler(m_events->forIStream().inputReady(), + m_stream->getEventTarget()); + m_events->removeHandler(m_events->forIStream().outputError(), + m_stream->getEventTarget()); + m_events->removeHandler(m_events->forIStream().inputShutdown(), + m_stream->getEventTarget()); + m_events->removeHandler(m_events->forIStream().outputShutdown(), + m_stream->getEventTarget()); + } + if (m_proxy != NULL) { + m_events->removeHandler(m_events->forClientProxy().ready(), + m_proxy); + m_events->removeHandler(m_events->forClientProxy().disconnected(), + m_proxy); + } +} + +void +ClientProxyUnknown::removeTimer() +{ + if (m_timer != NULL) { + m_events->deleteTimer(m_timer); + m_events->removeHandler(Event::kTimer, this); + m_timer = NULL; + } +} + +void +ClientProxyUnknown::handleData(const Event&, void*) +{ + LOG((CLOG_DEBUG1 "parsing hello reply")); + + String name("<unknown>"); + try { + // limit the maximum length of the hello + UInt32 n = m_stream->getSize(); + if (n > kMaxHelloLength) { + LOG((CLOG_DEBUG1 "hello reply too long")); + throw XBadClient(); + } + + // parse the reply to hello + SInt16 major, minor; + if (!ProtocolUtil::readf(m_stream, kMsgHelloBack, + &major, &minor, &name)) { + throw XBadClient(); + } + + // disallow invalid version numbers + if (major <= 0 || minor < 0) { + throw XIncompatibleClient(major, minor); + } + + // remove stream event handlers. the proxy we're about to create + // may install its own handlers and we don't want to accidentally + // remove those later. + removeHandlers(); + + // create client proxy for highest version supported by the client + if (major == 1) { + switch (minor) { + case 0: + m_proxy = new ClientProxy1_0(name, m_stream, m_events); + break; + + case 1: + m_proxy = new ClientProxy1_1(name, m_stream, m_events); + break; + + case 2: + m_proxy = new ClientProxy1_2(name, m_stream, m_events); + break; + + case 3: + m_proxy = new ClientProxy1_3(name, m_stream, m_events); + break; + + case 4: + m_proxy = new ClientProxy1_4(name, m_stream, m_server, m_events); + break; + + case 5: + m_proxy = new ClientProxy1_5(name, m_stream, m_server, m_events); + break; + + case 6: + m_proxy = new ClientProxy1_6(name, m_stream, m_server, m_events); + break; + } + } + + // hangup (with error) if version isn't supported + if (m_proxy == NULL) { + throw XIncompatibleClient(major, minor); + } + + // the proxy is created and now proxy now owns the stream + LOG((CLOG_DEBUG1 "created proxy for client \"%s\" version %d.%d", name.c_str(), major, minor)); + m_stream = NULL; + + // wait until the proxy signals that it's ready or has disconnected + addProxyHandlers(); + return; + } + catch (XIncompatibleClient& e) { + // client is incompatible + LOG((CLOG_WARN "client \"%s\" has incompatible version %d.%d)", name.c_str(), e.getMajor(), e.getMinor())); + ProtocolUtil::writef(m_stream, + kMsgEIncompatible, + kProtocolMajorVersion, kProtocolMinorVersion); + } + catch (XBadClient&) { + // client not behaving + LOG((CLOG_WARN "protocol error from client \"%s\"", name.c_str())); + ProtocolUtil::writef(m_stream, kMsgEBad); + } + catch (XBase& e) { + // misc error + LOG((CLOG_WARN "error communicating with client \"%s\": %s", name.c_str(), e.what())); + } + sendFailure(); +} + +void +ClientProxyUnknown::handleWriteError(const Event&, void*) +{ + LOG((CLOG_NOTE "error communicating with new client")); + sendFailure(); +} + +void +ClientProxyUnknown::handleTimeout(const Event&, void*) +{ + LOG((CLOG_NOTE "new client is unresponsive")); + sendFailure(); +} + +void +ClientProxyUnknown::handleDisconnect(const Event&, void*) +{ + LOG((CLOG_NOTE "new client disconnected")); + sendFailure(); +} + +void +ClientProxyUnknown::handleReady(const Event&, void*) +{ + sendSuccess(); +} diff --git a/src/lib/server/ClientProxyUnknown.h b/src/lib/server/ClientProxyUnknown.h new file mode 100644 index 0000000..5d59402 --- /dev/null +++ b/src/lib/server/ClientProxyUnknown.h @@ -0,0 +1,71 @@ +/* + * barrier -- mouse and keyboard sharing utility + * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2004 Chris Schoeneman + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "base/Event.h" +#include "base/EventTypes.h" + +class ClientProxy; +class EventQueueTimer; +namespace barrier { class IStream; } +class Server; +class IEventQueue; + +class ClientProxyUnknown { +public: + ClientProxyUnknown(barrier::IStream* stream, double timeout, Server* server, IEventQueue* events); + ~ClientProxyUnknown(); + + //! @name manipulators + //@{ + + //! Get the client proxy + /*! + Returns the client proxy created after a successful handshake + (i.e. when this object sends a success event). Returns NULL + if the handshake is unsuccessful or incomplete. + */ + ClientProxy* orphanClientProxy(); + + //! Get the stream + barrier::IStream* getStream() { return m_stream; } + + //@} + +private: + void sendSuccess(); + void sendFailure(); + void addStreamHandlers(); + void addProxyHandlers(); + void removeHandlers(); + void removeTimer(); + void handleData(const Event&, void*); + void handleWriteError(const Event&, void*); + void handleTimeout(const Event&, void*); + void handleDisconnect(const Event&, void*); + void handleReady(const Event&, void*); + +private: + barrier::IStream* m_stream; + EventQueueTimer* m_timer; + ClientProxy* m_proxy; + bool m_ready; + Server* m_server; + IEventQueue* m_events; +}; diff --git a/src/lib/server/Config.cpp b/src/lib/server/Config.cpp new file mode 100644 index 0000000..3cf60a5 --- /dev/null +++ b/src/lib/server/Config.cpp @@ -0,0 +1,2335 @@ +/* + * barrier -- mouse and keyboard sharing utility + * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2002 Chris Schoeneman + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "server/Config.h" + +#include "server/Server.h" +#include "barrier/KeyMap.h" +#include "barrier/key_types.h" +#include "net/XSocket.h" +#include "base/IEventQueue.h" +#include "common/stdistream.h" +#include "common/stdostream.h" + +#include <cstdlib> + +using namespace barrier::string; + +// +// Config +// + +Config::Config(IEventQueue* events) : + m_inputFilter(events), + m_hasLockToScreenAction(false), + m_events(events) +{ + // do nothing +} + +Config::~Config() +{ + // do nothing +} + +bool +Config::addScreen(const String& name) +{ + // alias name must not exist + if (m_nameToCanonicalName.find(name) != m_nameToCanonicalName.end()) { + return false; + } + + // add cell + m_map.insert(std::make_pair(name, Cell())); + + // add name + m_nameToCanonicalName.insert(std::make_pair(name, name)); + + return true; +} + +bool +Config::renameScreen(const String& oldName, + const String& newName) +{ + // get canonical name and find cell + String oldCanonical = getCanonicalName(oldName); + CellMap::iterator index = m_map.find(oldCanonical); + if (index == m_map.end()) { + return false; + } + + // accept if names are equal but replace with new name to maintain + // case. otherwise, the new name must not exist. + if (!CaselessCmp::equal(oldName, newName) && + m_nameToCanonicalName.find(newName) != m_nameToCanonicalName.end()) { + return false; + } + + // update cell + Cell tmpCell = index->second; + m_map.erase(index); + m_map.insert(std::make_pair(newName, tmpCell)); + + // update name + m_nameToCanonicalName.erase(oldCanonical); + m_nameToCanonicalName.insert(std::make_pair(newName, newName)); + + // update connections + Name oldNameObj(this, oldName); + for (index = m_map.begin(); index != m_map.end(); ++index) { + index->second.rename(oldNameObj, newName); + } + + // update alias targets + if (CaselessCmp::equal(oldName, oldCanonical)) { + for (NameMap::iterator iter = m_nameToCanonicalName.begin(); + iter != m_nameToCanonicalName.end(); ++iter) { + if (CaselessCmp::equal( + iter->second, oldCanonical)) { + iter->second = newName; + } + } + } + + return true; +} + +void +Config::removeScreen(const String& name) +{ + // get canonical name and find cell + String canonical = getCanonicalName(name); + CellMap::iterator index = m_map.find(canonical); + if (index == m_map.end()) { + return; + } + + // remove from map + m_map.erase(index); + + // disconnect + Name nameObj(this, name); + for (index = m_map.begin(); index != m_map.end(); ++index) { + index->second.remove(nameObj); + } + + // remove aliases (and canonical name) + for (NameMap::iterator iter = m_nameToCanonicalName.begin(); + iter != m_nameToCanonicalName.end(); ) { + if (iter->second == canonical) { + m_nameToCanonicalName.erase(iter++); + } + else { + ++index; + } + } +} + +void +Config::removeAllScreens() +{ + m_map.clear(); + m_nameToCanonicalName.clear(); +} + +bool +Config::addAlias(const String& canonical, const String& alias) +{ + // alias name must not exist + if (m_nameToCanonicalName.find(alias) != m_nameToCanonicalName.end()) { + return false; + } + + // canonical name must be known + if (m_map.find(canonical) == m_map.end()) { + return false; + } + + // insert alias + m_nameToCanonicalName.insert(std::make_pair(alias, canonical)); + + return true; +} + +bool +Config::removeAlias(const String& alias) +{ + // must not be a canonical name + if (m_map.find(alias) != m_map.end()) { + return false; + } + + // find alias + NameMap::iterator index = m_nameToCanonicalName.find(alias); + if (index == m_nameToCanonicalName.end()) { + return false; + } + + // remove alias + m_nameToCanonicalName.erase(index); + + return true; +} + +bool +Config::removeAliases(const String& canonical) +{ + // must be a canonical name + if (m_map.find(canonical) == m_map.end()) { + return false; + } + + // find and removing matching aliases + for (NameMap::iterator index = m_nameToCanonicalName.begin(); + index != m_nameToCanonicalName.end(); ) { + if (index->second == canonical && index->first != canonical) { + m_nameToCanonicalName.erase(index++); + } + else { + ++index; + } + } + + return true; +} + +void +Config::removeAllAliases() +{ + // remove all names + m_nameToCanonicalName.clear(); + + // put the canonical names back in + for (CellMap::iterator index = m_map.begin(); + index != m_map.end(); ++index) { + m_nameToCanonicalName.insert( + std::make_pair(index->first, index->first)); + } +} + +bool +Config::connect(const String& srcName, + EDirection srcSide, + float srcStart, float srcEnd, + const String& dstName, + float dstStart, float dstEnd) +{ + assert(srcSide >= kFirstDirection && srcSide <= kLastDirection); + + // find source cell + CellMap::iterator index = m_map.find(getCanonicalName(srcName)); + if (index == m_map.end()) { + return false; + } + + // add link + CellEdge srcEdge(srcSide, Interval(srcStart, srcEnd)); + CellEdge dstEdge(dstName, srcSide, Interval(dstStart, dstEnd)); + return index->second.add(srcEdge, dstEdge); +} + +bool +Config::disconnect(const String& srcName, EDirection srcSide) +{ + assert(srcSide >= kFirstDirection && srcSide <= kLastDirection); + + // find source cell + CellMap::iterator index = m_map.find(srcName); + if (index == m_map.end()) { + return false; + } + + // disconnect side + index->second.remove(srcSide); + + return true; +} + +bool +Config::disconnect(const String& srcName, EDirection srcSide, float position) +{ + assert(srcSide >= kFirstDirection && srcSide <= kLastDirection); + + // find source cell + CellMap::iterator index = m_map.find(srcName); + if (index == m_map.end()) { + return false; + } + + // disconnect side + index->second.remove(srcSide, position); + + return true; +} + +void +Config::setBarrierAddress(const NetworkAddress& addr) +{ + m_barrierAddress = addr; +} + +bool +Config::addOption(const String& name, OptionID option, OptionValue value) +{ + // find options + ScreenOptions* options = NULL; + if (name.empty()) { + options = &m_globalOptions; + } + else { + CellMap::iterator index = m_map.find(name); + if (index != m_map.end()) { + options = &index->second.m_options; + } + } + if (options == NULL) { + return false; + } + + // add option + options->insert(std::make_pair(option, value)); + return true; +} + +bool +Config::removeOption(const String& name, OptionID option) +{ + // find options + ScreenOptions* options = NULL; + if (name.empty()) { + options = &m_globalOptions; + } + else { + CellMap::iterator index = m_map.find(name); + if (index != m_map.end()) { + options = &index->second.m_options; + } + } + if (options == NULL) { + return false; + } + + // remove option + options->erase(option); + return true; +} + +bool +Config::removeOptions(const String& name) +{ + // find options + ScreenOptions* options = NULL; + if (name.empty()) { + options = &m_globalOptions; + } + else { + CellMap::iterator index = m_map.find(name); + if (index != m_map.end()) { + options = &index->second.m_options; + } + } + if (options == NULL) { + return false; + } + + // remove options + options->clear(); + return true; +} + +bool +Config::isValidScreenName(const String& name) const +{ + // name is valid if matches validname + // name ::= [_A-Za-z0-9] | [_A-Za-z0-9][-_A-Za-z0-9]*[_A-Za-z0-9] + // domain ::= . name + // validname ::= name domain* + // we also accept names ending in . because many OS X users have + // so misconfigured their systems. + + // empty name is invalid + if (name.empty()) { + return false; + } + + // check each dot separated part + String::size_type b = 0; + for (;;) { + // accept trailing . + if (b == name.size()) { + break; + } + + // find end of part + String::size_type e = name.find('.', b); + if (e == String::npos) { + e = name.size(); + } + + // part may not be empty + if (e - b < 1) { + return false; + } + + // check first and last characters + if (!(isalnum(name[b]) || name[b] == '_') || + !(isalnum(name[e - 1]) || name[e - 1] == '_')) { + return false; + } + + // check interior characters + for (String::size_type i = b; i < e; ++i) { + if (!isalnum(name[i]) && name[i] != '_' && name[i] != '-') { + return false; + } + } + + // next part + if (e == name.size()) { + // no more parts + break; + } + b = e + 1; + } + + return true; +} + +Config::const_iterator +Config::begin() const +{ + return const_iterator(m_map.begin()); +} + +Config::const_iterator +Config::end() const +{ + return const_iterator(m_map.end()); +} + +Config::all_const_iterator +Config::beginAll() const +{ + return m_nameToCanonicalName.begin(); +} + +Config::all_const_iterator +Config::endAll() const +{ + return m_nameToCanonicalName.end(); +} + +bool +Config::isScreen(const String& name) const +{ + return (m_nameToCanonicalName.count(name) > 0); +} + +bool +Config::isCanonicalName(const String& name) const +{ + return (!name.empty() && + CaselessCmp::equal(getCanonicalName(name), name)); +} + +String +Config::getCanonicalName(const String& name) const +{ + NameMap::const_iterator index = m_nameToCanonicalName.find(name); + if (index == m_nameToCanonicalName.end()) { + return String(); + } + else { + return index->second; + } +} + +String +Config::getNeighbor(const String& srcName, EDirection srcSide, + float position, float* positionOut) const +{ + assert(srcSide >= kFirstDirection && srcSide <= kLastDirection); + + // find source cell + CellMap::const_iterator index = m_map.find(getCanonicalName(srcName)); + if (index == m_map.end()) { + return String(); + } + + // find edge + const CellEdge* srcEdge, *dstEdge; + if (!index->second.getLink(srcSide, position, srcEdge, dstEdge)) { + // no neighbor + return ""; + } + else { + // compute position on neighbor + if (positionOut != NULL) { + *positionOut = + dstEdge->inverseTransform(srcEdge->transform(position)); + } + + // return neighbor's name + return getCanonicalName(dstEdge->getName()); + } +} + +bool +Config::hasNeighbor(const String& srcName, EDirection srcSide) const +{ + return hasNeighbor(srcName, srcSide, 0.0f, 1.0f); +} + +bool +Config::hasNeighbor(const String& srcName, EDirection srcSide, + float start, float end) const +{ + assert(srcSide >= kFirstDirection && srcSide <= kLastDirection); + + // find source cell + CellMap::const_iterator index = m_map.find(getCanonicalName(srcName)); + if (index == m_map.end()) { + return false; + } + + return index->second.overlaps(CellEdge(srcSide, Interval(start, end))); +} + +Config::link_const_iterator +Config::beginNeighbor(const String& srcName) const +{ + CellMap::const_iterator index = m_map.find(getCanonicalName(srcName)); + assert(index != m_map.end()); + return index->second.begin(); +} + +Config::link_const_iterator +Config::endNeighbor(const String& srcName) const +{ + CellMap::const_iterator index = m_map.find(getCanonicalName(srcName)); + assert(index != m_map.end()); + return index->second.end(); +} + +const NetworkAddress& +Config::getBarrierAddress() const +{ + return m_barrierAddress; +} + +const Config::ScreenOptions* +Config::getOptions(const String& name) const +{ + // find options + const ScreenOptions* options = NULL; + if (name.empty()) { + options = &m_globalOptions; + } + else { + CellMap::const_iterator index = m_map.find(name); + if (index != m_map.end()) { + options = &index->second.m_options; + } + } + + // return options + return options; +} + +bool +Config::hasLockToScreenAction() const +{ + return m_hasLockToScreenAction; +} + +bool +Config::operator==(const Config& x) const +{ + if (m_barrierAddress != x.m_barrierAddress) { + return false; + } + if (m_map.size() != x.m_map.size()) { + return false; + } + if (m_nameToCanonicalName.size() != x.m_nameToCanonicalName.size()) { + return false; + } + + // compare global options + if (m_globalOptions != x.m_globalOptions) { + return false; + } + + for (CellMap::const_iterator index1 = m_map.begin(), + index2 = x.m_map.begin(); + index1 != m_map.end(); ++index1, ++index2) { + // compare names + if (!CaselessCmp::equal(index1->first, index2->first)) { + return false; + } + + // compare cells + if (index1->second != index2->second) { + return false; + } + } + + for (NameMap::const_iterator index1 = m_nameToCanonicalName.begin(), + index2 = x.m_nameToCanonicalName.begin(); + index1 != m_nameToCanonicalName.end(); + ++index1, ++index2) { + if (!CaselessCmp::equal(index1->first, index2->first) || + !CaselessCmp::equal(index1->second, index2->second)) { + return false; + } + } + + // compare input filters + if (m_inputFilter != x.m_inputFilter) { + return false; + } + + return true; +} + +bool +Config::operator!=(const Config& x) const +{ + return !operator==(x); +} + +void +Config::read(ConfigReadContext& context) +{ + Config tmp(m_events); + while (context.getStream()) { + tmp.readSection(context); + } + *this = tmp; +} + +const char* +Config::dirName(EDirection dir) +{ + static const char* s_name[] = { "left", "right", "up", "down" }; + + assert(dir >= kFirstDirection && dir <= kLastDirection); + + return s_name[dir - kFirstDirection]; +} + +InputFilter* +Config::getInputFilter() +{ + return &m_inputFilter; +} + +String +Config::formatInterval(const Interval& x) +{ + if (x.first == 0.0f && x.second == 1.0f) { + return ""; + } + return barrier::string::sprintf("(%d,%d)", (int)(x.first * 100.0f + 0.5f), + (int)(x.second * 100.0f + 0.5f)); +} + +void +Config::readSection(ConfigReadContext& s) +{ + static const char s_section[] = "section:"; + static const char s_options[] = "options"; + static const char s_screens[] = "screens"; + static const char s_links[] = "links"; + static const char s_aliases[] = "aliases"; + + String line; + if (!s.readLine(line)) { + // no more sections + return; + } + + // should be a section header + if (line.find(s_section) != 0) { + throw XConfigRead(s, "found data outside section"); + } + + // get section name + String::size_type i = line.find_first_not_of(" \t", sizeof(s_section) - 1); + if (i == String::npos) { + throw XConfigRead(s, "section name is missing"); + } + String name = line.substr(i); + i = name.find_first_of(" \t"); + if (i != String::npos) { + throw XConfigRead(s, "unexpected data after section name"); + } + + // read section + if (name == s_options) { + readSectionOptions(s); + } + else if (name == s_screens) { + readSectionScreens(s); + } + else if (name == s_links) { + readSectionLinks(s); + } + else if (name == s_aliases) { + readSectionAliases(s); + } + else { + throw XConfigRead(s, "unknown section name \"%{1}\"", name); + } +} + +void +Config::readSectionOptions(ConfigReadContext& s) +{ + String line; + while (s.readLine(line)) { + // check for end of section + if (line == "end") { + return; + } + + // parse argument: `nameAndArgs = [values][;[values]]' + // nameAndArgs := <name>[(arg[,...])] + // values := valueAndArgs[,valueAndArgs]... + // valueAndArgs := <value>[(arg[,...])] + String::size_type i = 0; + String name, value; + ConfigReadContext::ArgList nameArgs, valueArgs; + s.parseNameWithArgs("name", line, "=", i, name, nameArgs); + ++i; + s.parseNameWithArgs("value", line, ",;\n", i, value, valueArgs); + + bool handled = true; + if (name == "address") { + try { + m_barrierAddress = NetworkAddress(value, kDefaultPort); + m_barrierAddress.resolve(); + } + catch (XSocketAddress& e) { + throw XConfigRead(s, + String("invalid address argument ") + e.what()); + } + } + else if (name == "heartbeat") { + addOption("", kOptionHeartbeat, s.parseInt(value)); + } + else if (name == "switchCorners") { + addOption("", kOptionScreenSwitchCorners, s.parseCorners(value)); + } + else if (name == "switchCornerSize") { + addOption("", kOptionScreenSwitchCornerSize, s.parseInt(value)); + } + else if (name == "switchDelay") { + addOption("", kOptionScreenSwitchDelay, s.parseInt(value)); + } + else if (name == "switchDoubleTap") { + addOption("", kOptionScreenSwitchTwoTap, s.parseInt(value)); + } + else if (name == "switchNeedsShift") { + addOption("", kOptionScreenSwitchNeedsShift, s.parseBoolean(value)); + } + else if (name == "switchNeedsControl") { + addOption("", kOptionScreenSwitchNeedsControl, s.parseBoolean(value)); + } + else if (name == "switchNeedsAlt") { + addOption("", kOptionScreenSwitchNeedsAlt, s.parseBoolean(value)); + } + else if (name == "screenSaverSync") { + addOption("", kOptionScreenSaverSync, s.parseBoolean(value)); + } + else if (name == "relativeMouseMoves") { + addOption("", kOptionRelativeMouseMoves, s.parseBoolean(value)); + } + else if (name == "win32KeepForeground") { + addOption("", kOptionWin32KeepForeground, s.parseBoolean(value)); + } + else if (name == "clipboardSharing") { + addOption("", kOptionClipboardSharing, s.parseBoolean(value)); + } + + else { + handled = false; + } + + if (handled) { + // make sure handled options aren't followed by more values + if (i < line.size() && (line[i] == ',' || line[i] == ';')) { + throw XConfigRead(s, "to many arguments to %s", name.c_str()); + } + } + else { + // make filter rule + InputFilter::Rule rule(parseCondition(s, name, nameArgs)); + + // save first action (if any) + if (!value.empty() || line[i] != ';') { + parseAction(s, value, valueArgs, rule, true); + } + + // get remaining activate actions + while (i < line.length() && line[i] != ';') { + ++i; + s.parseNameWithArgs("value", line, ",;\n", i, value, valueArgs); + parseAction(s, value, valueArgs, rule, true); + } + + // get deactivate actions + if (i < line.length() && line[i] == ';') { + // allow trailing ';' + i = line.find_first_not_of(" \t", i + 1); + if (i == String::npos) { + i = line.length(); + } + else { + --i; + } + + // get actions + while (i < line.length()) { + ++i; + s.parseNameWithArgs("value", line, ",\n", + i, value, valueArgs); + parseAction(s, value, valueArgs, rule, false); + } + } + + // add rule + m_inputFilter.addFilterRule(rule); + } + } + throw XConfigRead(s, "unexpected end of options section"); +} + +void +Config::readSectionScreens(ConfigReadContext& s) +{ + String line; + String screen; + while (s.readLine(line)) { + // check for end of section + if (line == "end") { + return; + } + + // see if it's the next screen + if (line[line.size() - 1] == ':') { + // strip : + screen = line.substr(0, line.size() - 1); + + // verify validity of screen name + if (!isValidScreenName(screen)) { + throw XConfigRead(s, "invalid screen name \"%{1}\"", screen); + } + + // add the screen to the configuration + if (!addScreen(screen)) { + throw XConfigRead(s, "duplicate screen name \"%{1}\"", screen); + } + } + else if (screen.empty()) { + throw XConfigRead(s, "argument before first screen"); + } + else { + // parse argument: `<name>=<value>' + String::size_type i = line.find_first_of(" \t="); + if (i == 0) { + throw XConfigRead(s, "missing argument name"); + } + if (i == String::npos) { + throw XConfigRead(s, "missing ="); + } + String name = line.substr(0, i); + i = line.find_first_not_of(" \t", i); + if (i == String::npos || line[i] != '=') { + throw XConfigRead(s, "missing ="); + } + i = line.find_first_not_of(" \t", i + 1); + String value; + if (i != String::npos) { + value = line.substr(i); + } + + // handle argument + if (name == "halfDuplexCapsLock") { + addOption(screen, kOptionHalfDuplexCapsLock, + s.parseBoolean(value)); + } + else if (name == "halfDuplexNumLock") { + addOption(screen, kOptionHalfDuplexNumLock, + s.parseBoolean(value)); + } + else if (name == "halfDuplexScrollLock") { + addOption(screen, kOptionHalfDuplexScrollLock, + s.parseBoolean(value)); + } + else if (name == "shift") { + addOption(screen, kOptionModifierMapForShift, + s.parseModifierKey(value)); + } + else if (name == "ctrl") { + addOption(screen, kOptionModifierMapForControl, + s.parseModifierKey(value)); + } + else if (name == "alt") { + addOption(screen, kOptionModifierMapForAlt, + s.parseModifierKey(value)); + } + else if (name == "altgr") { + addOption(screen, kOptionModifierMapForAltGr, + s.parseModifierKey(value)); + } + else if (name == "meta") { + addOption(screen, kOptionModifierMapForMeta, + s.parseModifierKey(value)); + } + else if (name == "super") { + addOption(screen, kOptionModifierMapForSuper, + s.parseModifierKey(value)); + } + else if (name == "xtestIsXineramaUnaware") { + addOption(screen, kOptionXTestXineramaUnaware, + s.parseBoolean(value)); + } + else if (name == "switchCorners") { + addOption(screen, kOptionScreenSwitchCorners, + s.parseCorners(value)); + } + else if (name == "switchCornerSize") { + addOption(screen, kOptionScreenSwitchCornerSize, + s.parseInt(value)); + } + else if (name == "preserveFocus") { + addOption(screen, kOptionScreenPreserveFocus, + s.parseBoolean(value)); + } + else { + // unknown argument + throw XConfigRead(s, "unknown argument \"%{1}\"", name); + } + } + } + throw XConfigRead(s, "unexpected end of screens section"); +} + +void +Config::readSectionLinks(ConfigReadContext& s) +{ + String line; + String screen; + while (s.readLine(line)) { + // check for end of section + if (line == "end") { + return; + } + + // see if it's the next screen + if (line[line.size() - 1] == ':') { + // strip : + screen = line.substr(0, line.size() - 1); + + // verify we know about the screen + if (!isScreen(screen)) { + throw XConfigRead(s, "unknown screen name \"%{1}\"", screen); + } + if (!isCanonicalName(screen)) { + throw XConfigRead(s, "cannot use screen name alias here"); + } + } + else if (screen.empty()) { + throw XConfigRead(s, "argument before first screen"); + } + else { + // parse argument: `<name>[(<s0>,<e0>)]=<value>[(<s1>,<e1>)]' + // the stuff in brackets is optional. interval values must be + // in the range [0,100] and start < end. if not given the + // interval is taken to be (0,100). + String::size_type i = 0; + String side, dstScreen, srcArgString, dstArgString; + ConfigReadContext::ArgList srcArgs, dstArgs; + s.parseNameWithArgs("link", line, "=", i, side, srcArgs); + ++i; + s.parseNameWithArgs("screen", line, "", i, dstScreen, dstArgs); + Interval srcInterval(s.parseInterval(srcArgs)); + Interval dstInterval(s.parseInterval(dstArgs)); + + // handle argument + EDirection dir; + if (side == "left") { + dir = kLeft; + } + else if (side == "right") { + dir = kRight; + } + else if (side == "up") { + dir = kTop; + } + else if (side == "down") { + dir = kBottom; + } + else { + // unknown argument + throw XConfigRead(s, "unknown side \"%{1}\" in link", side); + } + if (!isScreen(dstScreen)) { + throw XConfigRead(s, "unknown screen name \"%{1}\"", dstScreen); + } + if (!connect(screen, dir, + srcInterval.first, srcInterval.second, + dstScreen, + dstInterval.first, dstInterval.second)) { + throw XConfigRead(s, "overlapping range"); + } + } + } + throw XConfigRead(s, "unexpected end of links section"); +} + +void +Config::readSectionAliases(ConfigReadContext& s) +{ + String line; + String screen; + while (s.readLine(line)) { + // check for end of section + if (line == "end") { + return; + } + + // see if it's the next screen + if (line[line.size() - 1] == ':') { + // strip : + screen = line.substr(0, line.size() - 1); + + // verify we know about the screen + if (!isScreen(screen)) { + throw XConfigRead(s, "unknown screen name \"%{1}\"", screen); + } + if (!isCanonicalName(screen)) { + throw XConfigRead(s, "cannot use screen name alias here"); + } + } + else if (screen.empty()) { + throw XConfigRead(s, "argument before first screen"); + } + else { + // verify validity of screen name + if (!isValidScreenName(line)) { + throw XConfigRead(s, "invalid screen alias \"%{1}\"", line); + } + + // add alias + if (!addAlias(screen, line)) { + throw XConfigRead(s, "alias \"%{1}\" is already used", line); + } + } + } + throw XConfigRead(s, "unexpected end of aliases section"); +} + + +InputFilter::Condition* +Config::parseCondition(ConfigReadContext& s, + const String& name, const std::vector<String>& args) +{ + if (name == "keystroke") { + if (args.size() != 1) { + throw XConfigRead(s, "syntax for condition: keystroke(modifiers+key)"); + } + + IPlatformScreen::KeyInfo* keyInfo = s.parseKeystroke(args[0]); + + return new InputFilter::KeystrokeCondition(m_events, keyInfo); + } + + if (name == "mousebutton") { + if (args.size() != 1) { + throw XConfigRead(s, "syntax for condition: mousebutton(modifiers+button)"); + } + + IPlatformScreen::ButtonInfo* mouseInfo = s.parseMouse(args[0]); + + return new InputFilter::MouseButtonCondition(m_events, mouseInfo); + } + + if (name == "connect") { + if (args.size() != 1) { + throw XConfigRead(s, "syntax for condition: connect([screen])"); + } + + String screen = args[0]; + if (isScreen(screen)) { + screen = getCanonicalName(screen); + } + else if (!screen.empty()) { + throw XConfigRead(s, "unknown screen name \"%{1}\" in connect", screen); + } + + return new InputFilter::ScreenConnectedCondition(m_events, screen); + } + + throw XConfigRead(s, "unknown argument \"%{1}\"", name); +} + +void +Config::parseAction(ConfigReadContext& s, + const String& name, const std::vector<String>& args, + InputFilter::Rule& rule, bool activate) +{ + InputFilter::Action* action; + + if (name == "keystroke" || name == "keyDown" || name == "keyUp") { + if (args.size() < 1 || args.size() > 2) { + throw XConfigRead(s, "syntax for action: keystroke(modifiers+key[,screens])"); + } + + IPlatformScreen::KeyInfo* keyInfo; + if (args.size() == 1) { + keyInfo = s.parseKeystroke(args[0]); + } + else { + std::set<String> screens; + parseScreens(s, args[1], screens); + keyInfo = s.parseKeystroke(args[0], screens); + } + + if (name == "keystroke") { + IPlatformScreen::KeyInfo* keyInfo2 = + IKeyState::KeyInfo::alloc(*keyInfo); + action = new InputFilter::KeystrokeAction(m_events, keyInfo2, true); + rule.adoptAction(action, true); + action = new InputFilter::KeystrokeAction(m_events, keyInfo, false); + activate = false; + } + else if (name == "keyDown") { + action = new InputFilter::KeystrokeAction(m_events, keyInfo, true); + } + else { + action = new InputFilter::KeystrokeAction(m_events, keyInfo, false); + } + } + + else if (name == "mousebutton" || + name == "mouseDown" || name == "mouseUp") { + if (args.size() != 1) { + throw XConfigRead(s, "syntax for action: mousebutton(modifiers+button)"); + } + + IPlatformScreen::ButtonInfo* mouseInfo = s.parseMouse(args[0]); + + if (name == "mousebutton") { + IPlatformScreen::ButtonInfo* mouseInfo2 = + IPlatformScreen::ButtonInfo::alloc(*mouseInfo); + action = new InputFilter::MouseButtonAction(m_events, mouseInfo2, true); + rule.adoptAction(action, true); + action = new InputFilter::MouseButtonAction(m_events, mouseInfo, false); + activate = false; + } + else if (name == "mouseDown") { + action = new InputFilter::MouseButtonAction(m_events, mouseInfo, true); + } + else { + action = new InputFilter::MouseButtonAction(m_events, mouseInfo, false); + } + } + +/* XXX -- not supported + else if (name == "modifier") { + if (args.size() != 1) { + throw XConfigRead(s, "syntax for action: modifier(modifiers)"); + } + + KeyModifierMask mask = s.parseModifier(args[0]); + + action = new InputFilter::ModifierAction(mask, ~mask); + } +*/ + + else if (name == "switchToScreen") { + if (args.size() != 1) { + throw XConfigRead(s, "syntax for action: switchToScreen(name)"); + } + + String screen = args[0]; + if (isScreen(screen)) { + screen = getCanonicalName(screen); + } + else if (!screen.empty()) { + throw XConfigRead(s, "unknown screen name in switchToScreen"); + } + + action = new InputFilter::SwitchToScreenAction(m_events, screen); + } + + else if (name == "switchInDirection") { + if (args.size() != 1) { + throw XConfigRead(s, "syntax for action: switchInDirection(<left|right|up|down>)"); + } + + EDirection direction; + if (args[0] == "left") { + direction = kLeft; + } + else if (args[0] == "right") { + direction = kRight; + } + else if (args[0] == "up") { + direction = kTop; + } + else if (args[0] == "down") { + direction = kBottom; + } + else { + throw XConfigRead(s, "unknown direction \"%{1}\" in switchToScreen", args[0]); + } + + action = new InputFilter::SwitchInDirectionAction(m_events, direction); + } + + else if (name == "lockCursorToScreen") { + if (args.size() > 1) { + throw XConfigRead(s, "syntax for action: lockCursorToScreen([{off|on|toggle}])"); + } + + InputFilter::LockCursorToScreenAction::Mode mode = + InputFilter::LockCursorToScreenAction::kToggle; + if (args.size() == 1) { + if (args[0] == "off") { + mode = InputFilter::LockCursorToScreenAction::kOff; + } + else if (args[0] == "on") { + mode = InputFilter::LockCursorToScreenAction::kOn; + } + else if (args[0] == "toggle") { + mode = InputFilter::LockCursorToScreenAction::kToggle; + } + else { + throw XConfigRead(s, "syntax for action: lockCursorToScreen([{off|on|toggle}])"); + } + } + + if (mode != InputFilter::LockCursorToScreenAction::kOff) { + m_hasLockToScreenAction = true; + } + + action = new InputFilter::LockCursorToScreenAction(m_events, mode); + } + + else if (name == "keyboardBroadcast") { + if (args.size() > 2) { + throw XConfigRead(s, "syntax for action: keyboardBroadcast([{off|on|toggle}[,screens]])"); + } + + InputFilter::KeyboardBroadcastAction::Mode mode = + InputFilter::KeyboardBroadcastAction::kToggle; + if (args.size() >= 1) { + if (args[0] == "off") { + mode = InputFilter::KeyboardBroadcastAction::kOff; + } + else if (args[0] == "on") { + mode = InputFilter::KeyboardBroadcastAction::kOn; + } + else if (args[0] == "toggle") { + mode = InputFilter::KeyboardBroadcastAction::kToggle; + } + else { + throw XConfigRead(s, "syntax for action: keyboardBroadcast([{off|on|toggle}[,screens]])"); + } + } + + std::set<String> screens; + if (args.size() >= 2) { + parseScreens(s, args[1], screens); + } + + action = new InputFilter::KeyboardBroadcastAction(m_events, mode, screens); + } + + else { + throw XConfigRead(s, "unknown action argument \"%{1}\"", name); + } + + rule.adoptAction(action, activate); +} + +void +Config::parseScreens(ConfigReadContext& c, + const String& s, std::set<String>& screens) const +{ + screens.clear(); + + String::size_type i = 0; + while (i < s.size()) { + // find end of next screen name + String::size_type j = s.find(':', i); + if (j == String::npos) { + j = s.size(); + } + + // extract name + String rawName; + i = s.find_first_not_of(" \t", i); + if (i < j) { + rawName = s.substr(i, s.find_last_not_of(" \t", j - 1) - i + 1); + } + + // add name + if (rawName == "*") { + screens.insert("*"); + } + else if (!rawName.empty()) { + String name = getCanonicalName(rawName); + if (name.empty()) { + throw XConfigRead(c, "unknown screen name \"%{1}\"", rawName); + } + screens.insert(name); + } + + // next + i = j + 1; + } +} + +const char* +Config::getOptionName(OptionID id) +{ + if (id == kOptionHalfDuplexCapsLock) { + return "halfDuplexCapsLock"; + } + if (id == kOptionHalfDuplexNumLock) { + return "halfDuplexNumLock"; + } + if (id == kOptionHalfDuplexScrollLock) { + return "halfDuplexScrollLock"; + } + if (id == kOptionModifierMapForShift) { + return "shift"; + } + if (id == kOptionModifierMapForControl) { + return "ctrl"; + } + if (id == kOptionModifierMapForAlt) { + return "alt"; + } + if (id == kOptionModifierMapForAltGr) { + return "altgr"; + } + if (id == kOptionModifierMapForMeta) { + return "meta"; + } + if (id == kOptionModifierMapForSuper) { + return "super"; + } + if (id == kOptionHeartbeat) { + return "heartbeat"; + } + if (id == kOptionScreenSwitchCorners) { + return "switchCorners"; + } + if (id == kOptionScreenSwitchCornerSize) { + return "switchCornerSize"; + } + if (id == kOptionScreenSwitchDelay) { + return "switchDelay"; + } + if (id == kOptionScreenSwitchTwoTap) { + return "switchDoubleTap"; + } + if (id == kOptionScreenSwitchNeedsShift) { + return "switchNeedsShift"; + } + if (id == kOptionScreenSwitchNeedsControl) { + return "switchNeedsControl"; + } + if (id == kOptionScreenSwitchNeedsAlt) { + return "switchNeedsAlt"; + } + if (id == kOptionScreenSaverSync) { + return "screenSaverSync"; + } + if (id == kOptionXTestXineramaUnaware) { + return "xtestIsXineramaUnaware"; + } + if (id == kOptionRelativeMouseMoves) { + return "relativeMouseMoves"; + } + if (id == kOptionWin32KeepForeground) { + return "win32KeepForeground"; + } + if (id == kOptionScreenPreserveFocus) { + return "preserveFocus"; + } + if (id == kOptionClipboardSharing) { + return "clipboardSharing"; + } + return NULL; +} + +String +Config::getOptionValue(OptionID id, OptionValue value) +{ + if (id == kOptionHalfDuplexCapsLock || + id == kOptionHalfDuplexNumLock || + id == kOptionHalfDuplexScrollLock || + id == kOptionScreenSwitchNeedsShift || + id == kOptionScreenSwitchNeedsControl || + id == kOptionScreenSwitchNeedsAlt || + id == kOptionScreenSaverSync || + id == kOptionXTestXineramaUnaware || + id == kOptionRelativeMouseMoves || + id == kOptionWin32KeepForeground || + id == kOptionScreenPreserveFocus || + id == kOptionClipboardSharing) { + return (value != 0) ? "true" : "false"; + } + if (id == kOptionModifierMapForShift || + id == kOptionModifierMapForControl || + id == kOptionModifierMapForAlt || + id == kOptionModifierMapForAltGr || + id == kOptionModifierMapForMeta || + id == kOptionModifierMapForSuper) { + switch (value) { + case kKeyModifierIDShift: + return "shift"; + + case kKeyModifierIDControl: + return "ctrl"; + + case kKeyModifierIDAlt: + return "alt"; + + case kKeyModifierIDAltGr: + return "altgr"; + + case kKeyModifierIDMeta: + return "meta"; + + case kKeyModifierIDSuper: + return "super"; + + default: + return "none"; + } + } + if (id == kOptionHeartbeat || + id == kOptionScreenSwitchCornerSize || + id == kOptionScreenSwitchDelay || + id == kOptionScreenSwitchTwoTap) { + return barrier::string::sprintf("%d", value); + } + if (id == kOptionScreenSwitchCorners) { + std::string result("none"); + if ((value & kTopLeftMask) != 0) { + result += " +top-left"; + } + if ((value & kTopRightMask) != 0) { + result += " +top-right"; + } + if ((value & kBottomLeftMask) != 0) { + result += " +bottom-left"; + } + if ((value & kBottomRightMask) != 0) { + result += " +bottom-right"; + } + return result; + } + + return ""; +} + + +// +// Config::Name +// + +Config::Name::Name(Config* config, const String& name) : + m_config(config), + m_name(config->getCanonicalName(name)) +{ + // do nothing +} + +bool +Config::Name::operator==(const String& name) const +{ + String canonical = m_config->getCanonicalName(name); + return CaselessCmp::equal(canonical, m_name); +} + + +// +// Config::CellEdge +// + +Config::CellEdge::CellEdge(EDirection side, float position) +{ + init("", side, Interval(position, position)); +} + +Config::CellEdge::CellEdge(EDirection side, const Interval& interval) +{ + assert(interval.first >= 0.0f); + assert(interval.second <= 1.0f); + assert(interval.first < interval.second); + + init("", side, interval); +} + +Config::CellEdge::CellEdge(const String& name, + EDirection side, const Interval& interval) +{ + assert(interval.first >= 0.0f); + assert(interval.second <= 1.0f); + assert(interval.first < interval.second); + + init(name, side, interval); +} + +Config::CellEdge::~CellEdge() +{ + // do nothing +} + +void +Config::CellEdge::init(const String& name, EDirection side, + const Interval& interval) +{ + assert(side != kNoDirection); + + m_name = name; + m_side = side; + m_interval = interval; +} + +Config::Interval +Config::CellEdge::getInterval() const +{ + return m_interval; +} + +void +Config::CellEdge::setName(const String& newName) +{ + m_name = newName; +} + +String +Config::CellEdge::getName() const +{ + return m_name; +} + +EDirection +Config::CellEdge::getSide() const +{ + return m_side; +} + +bool +Config::CellEdge::overlaps(const CellEdge& edge) const +{ + const Interval& x = m_interval; + const Interval& y = edge.m_interval; + if (m_side != edge.m_side) { + return false; + } + return (x.first >= y.first && x.first < y.second) || + (x.second > y.first && x.second <= y.second) || + (y.first >= x.first && y.first < x.second) || + (y.second > x.first && y.second <= x.second); +} + +bool +Config::CellEdge::isInside(float x) const +{ + return (x >= m_interval.first && x < m_interval.second); +} + +float +Config::CellEdge::transform(float x) const +{ + return (x - m_interval.first) / (m_interval.second - m_interval.first); +} + + +float +Config::CellEdge::inverseTransform(float x) const +{ + return x * (m_interval.second - m_interval.first) + m_interval.first; +} + +bool +Config::CellEdge::operator<(const CellEdge& o) const +{ + if (static_cast<int>(m_side) < static_cast<int>(o.m_side)) { + return true; + } + else if (static_cast<int>(m_side) > static_cast<int>(o.m_side)) { + return false; + } + + return (m_interval.first < o.m_interval.first); +} + +bool +Config::CellEdge::operator==(const CellEdge& x) const +{ + return (m_side == x.m_side && m_interval == x.m_interval); +} + +bool +Config::CellEdge::operator!=(const CellEdge& x) const +{ + return !operator==(x); +} + + +// +// Config::Cell +// + +bool +Config::Cell::add(const CellEdge& src, const CellEdge& dst) +{ + // cannot add an edge that overlaps other existing edges but we + // can exactly replace an edge. + if (!hasEdge(src) && overlaps(src)) { + return false; + } + + m_neighbors.erase(src); + m_neighbors.insert(std::make_pair(src, dst)); + return true; +} + +void +Config::Cell::remove(EDirection side) +{ + for (EdgeLinks::iterator j = m_neighbors.begin(); + j != m_neighbors.end(); ) { + if (j->first.getSide() == side) { + m_neighbors.erase(j++); + } + else { + ++j; + } + } +} + +void +Config::Cell::remove(EDirection side, float position) +{ + for (EdgeLinks::iterator j = m_neighbors.begin(); + j != m_neighbors.end(); ++j) { + if (j->first.getSide() == side && j->first.isInside(position)) { + m_neighbors.erase(j); + break; + } + } +} +void +Config::Cell::remove(const Name& name) +{ + for (EdgeLinks::iterator j = m_neighbors.begin(); + j != m_neighbors.end(); ) { + if (name == j->second.getName()) { + m_neighbors.erase(j++); + } + else { + ++j; + } + } +} + +void +Config::Cell::rename(const Name& oldName, const String& newName) +{ + for (EdgeLinks::iterator j = m_neighbors.begin(); + j != m_neighbors.end(); ++j) { + if (oldName == j->second.getName()) { + j->second.setName(newName); + } + } +} + +bool +Config::Cell::hasEdge(const CellEdge& edge) const +{ + EdgeLinks::const_iterator i = m_neighbors.find(edge); + return (i != m_neighbors.end() && i->first == edge); +} + +bool +Config::Cell::overlaps(const CellEdge& edge) const +{ + EdgeLinks::const_iterator i = m_neighbors.upper_bound(edge); + if (i != m_neighbors.end() && i->first.overlaps(edge)) { + return true; + } + if (i != m_neighbors.begin() && (--i)->first.overlaps(edge)) { + return true; + } + return false; +} + +bool +Config::Cell::getLink(EDirection side, float position, + const CellEdge*& src, const CellEdge*& dst) const +{ + CellEdge edge(side, position); + EdgeLinks::const_iterator i = m_neighbors.upper_bound(edge); + if (i == m_neighbors.begin()) { + return false; + } + --i; + if (i->first.getSide() == side && i->first.isInside(position)) { + src = &i->first; + dst = &i->second; + return true; + } + return false; +} + +bool +Config::Cell::operator==(const Cell& x) const +{ + // compare options + if (m_options != x.m_options) { + return false; + } + + // compare links + if (m_neighbors.size() != x.m_neighbors.size()) { + return false; + } + for (EdgeLinks::const_iterator index1 = m_neighbors.begin(), + index2 = x.m_neighbors.begin(); + index1 != m_neighbors.end(); + ++index1, ++index2) { + if (index1->first != index2->first) { + return false; + } + if (index1->second != index2->second) { + return false; + } + + // operator== doesn't compare names. only compare destination + // names. + if (!CaselessCmp::equal(index1->second.getName(), + index2->second.getName())) { + return false; + } + } + return true; +} + +bool +Config::Cell::operator!=(const Cell& x) const +{ + return !operator==(x); +} + +Config::Cell::const_iterator +Config::Cell::begin() const +{ + return m_neighbors.begin(); +} + +Config::Cell::const_iterator +Config::Cell::end() const +{ + return m_neighbors.end(); +} + + +// +// Config I/O +// + +std::istream& +operator>>(std::istream& s, Config& config) +{ + ConfigReadContext context(s); + config.read(context); + return s; +} + +std::ostream& +operator<<(std::ostream& s, const Config& config) +{ + // screens section + s << "section: screens" << std::endl; + for (Config::const_iterator screen = config.begin(); + screen != config.end(); ++screen) { + s << "\t" << screen->c_str() << ":" << std::endl; + const Config::ScreenOptions* options = config.getOptions(*screen); + if (options != NULL && options->size() > 0) { + for (Config::ScreenOptions::const_iterator + option = options->begin(); + option != options->end(); ++option) { + const char* name = Config::getOptionName(option->first); + String value = Config::getOptionValue(option->first, + option->second); + if (name != NULL && !value.empty()) { + s << "\t\t" << name << " = " << value << std::endl; + } + } + } + } + s << "end" << std::endl; + + // links section + String neighbor; + s << "section: links" << std::endl; + for (Config::const_iterator screen = config.begin(); + screen != config.end(); ++screen) { + s << "\t" << screen->c_str() << ":" << std::endl; + + for (Config::link_const_iterator + link = config.beginNeighbor(*screen), + nend = config.endNeighbor(*screen); link != nend; ++link) { + s << "\t\t" << Config::dirName(link->first.getSide()) << + Config::formatInterval(link->first.getInterval()) << + " = " << link->second.getName().c_str() << + Config::formatInterval(link->second.getInterval()) << + std::endl; + } + } + s << "end" << std::endl; + + // aliases section (if there are any) + if (config.m_map.size() != config.m_nameToCanonicalName.size()) { + // map canonical to alias + typedef std::multimap<String, String, + CaselessCmp> CMNameMap; + CMNameMap aliases; + for (Config::NameMap::const_iterator + index = config.m_nameToCanonicalName.begin(); + index != config.m_nameToCanonicalName.end(); + ++index) { + if (index->first != index->second) { + aliases.insert(std::make_pair(index->second, index->first)); + } + } + + // dump it + String screen; + s << "section: aliases" << std::endl; + for (CMNameMap::const_iterator index = aliases.begin(); + index != aliases.end(); ++index) { + if (index->first != screen) { + screen = index->first; + s << "\t" << screen.c_str() << ":" << std::endl; + } + s << "\t\t" << index->second.c_str() << std::endl; + } + s << "end" << std::endl; + } + + // options section + s << "section: options" << std::endl; + const Config::ScreenOptions* options = config.getOptions(""); + if (options != NULL && options->size() > 0) { + for (Config::ScreenOptions::const_iterator + option = options->begin(); + option != options->end(); ++option) { + const char* name = Config::getOptionName(option->first); + String value = Config::getOptionValue(option->first, + option->second); + if (name != NULL && !value.empty()) { + s << "\t" << name << " = " << value << std::endl; + } + } + } + if (config.m_barrierAddress.isValid()) { + s << "\taddress = " << + config.m_barrierAddress.getHostname().c_str() << std::endl; + } + s << config.m_inputFilter.format("\t"); + s << "end" << std::endl; + + return s; +} + + +// +// ConfigReadContext +// + +ConfigReadContext::ConfigReadContext(std::istream& s, SInt32 firstLine) : + m_stream(s), + m_line(firstLine - 1) +{ + // do nothing +} + +ConfigReadContext::~ConfigReadContext() +{ + // do nothing +} + +bool +ConfigReadContext::readLine(String& line) +{ + ++m_line; + while (std::getline(m_stream, line)) { + // strip leading whitespace + String::size_type i = line.find_first_not_of(" \t"); + if (i != String::npos) { + line.erase(0, i); + } + + // strip comments and then trailing whitespace + i = line.find('#'); + if (i != String::npos) { + line.erase(i); + } + i = line.find_last_not_of(" \r\t"); + if (i != String::npos) { + line.erase(i + 1); + } + + // return non empty line + if (!line.empty()) { + // make sure there are no invalid characters + for (i = 0; i < line.length(); ++i) { + if (!isgraph(line[i]) && line[i] != ' ' && line[i] != '\t') { + throw XConfigRead(*this, + "invalid character %{1}", + barrier::string::sprintf("%#2x", line[i])); + } + } + + return true; + } + + // next line + ++m_line; + } + return false; +} + +UInt32 +ConfigReadContext::getLineNumber() const +{ + return m_line; +} + +bool +ConfigReadContext::operator!() const +{ + return !m_stream; +} + +OptionValue +ConfigReadContext::parseBoolean(const String& arg) const +{ + if (CaselessCmp::equal(arg, "true")) { + return static_cast<OptionValue>(true); + } + if (CaselessCmp::equal(arg, "false")) { + return static_cast<OptionValue>(false); + } + throw XConfigRead(*this, "invalid boolean argument \"%{1}\"", arg); +} + +OptionValue +ConfigReadContext::parseInt(const String& arg) const +{ + const char* s = arg.c_str(); + char* end; + long tmp = strtol(s, &end, 10); + if (*end != '\0') { + // invalid characters + throw XConfigRead(*this, "invalid integer argument \"%{1}\"", arg); + } + OptionValue value = static_cast<OptionValue>(tmp); + if (value != tmp) { + // out of range + throw XConfigRead(*this, "integer argument \"%{1}\" out of range", arg); + } + return value; +} + +OptionValue +ConfigReadContext::parseModifierKey(const String& arg) const +{ + if (CaselessCmp::equal(arg, "shift")) { + return static_cast<OptionValue>(kKeyModifierIDShift); + } + if (CaselessCmp::equal(arg, "ctrl")) { + return static_cast<OptionValue>(kKeyModifierIDControl); + } + if (CaselessCmp::equal(arg, "alt")) { + return static_cast<OptionValue>(kKeyModifierIDAlt); + } + if (CaselessCmp::equal(arg, "altgr")) { + return static_cast<OptionValue>(kKeyModifierIDAltGr); + } + if (CaselessCmp::equal(arg, "meta")) { + return static_cast<OptionValue>(kKeyModifierIDMeta); + } + if (CaselessCmp::equal(arg, "super")) { + return static_cast<OptionValue>(kKeyModifierIDSuper); + } + if (CaselessCmp::equal(arg, "none")) { + return static_cast<OptionValue>(kKeyModifierIDNull); + } + throw XConfigRead(*this, "invalid argument \"%{1}\"", arg); +} + +OptionValue +ConfigReadContext::parseCorner(const String& arg) const +{ + if (CaselessCmp::equal(arg, "left")) { + return kTopLeftMask | kBottomLeftMask; + } + else if (CaselessCmp::equal(arg, "right")) { + return kTopRightMask | kBottomRightMask; + } + else if (CaselessCmp::equal(arg, "top")) { + return kTopLeftMask | kTopRightMask; + } + else if (CaselessCmp::equal(arg, "bottom")) { + return kBottomLeftMask | kBottomRightMask; + } + else if (CaselessCmp::equal(arg, "top-left")) { + return kTopLeftMask; + } + else if (CaselessCmp::equal(arg, "top-right")) { + return kTopRightMask; + } + else if (CaselessCmp::equal(arg, "bottom-left")) { + return kBottomLeftMask; + } + else if (CaselessCmp::equal(arg, "bottom-right")) { + return kBottomRightMask; + } + else if (CaselessCmp::equal(arg, "none")) { + return kNoCornerMask; + } + else if (CaselessCmp::equal(arg, "all")) { + return kAllCornersMask; + } + throw XConfigRead(*this, "invalid argument \"%{1}\"", arg); +} + +OptionValue +ConfigReadContext::parseCorners(const String& args) const +{ + // find first token + String::size_type i = args.find_first_not_of(" \t", 0); + if (i == String::npos) { + throw XConfigRead(*this, "missing corner argument"); + } + String::size_type j = args.find_first_of(" \t", i); + + // parse first corner token + OptionValue corners = parseCorner(args.substr(i, j - i)); + + // get +/- + i = args.find_first_not_of(" \t", j); + while (i != String::npos) { + // parse +/- + bool add; + if (args[i] == '-') { + add = false; + } + else if (args[i] == '+') { + add = true; + } + else { + throw XConfigRead(*this, + "invalid corner operator \"%{1}\"", + String(args.c_str() + i, 1)); + } + + // get next corner token + i = args.find_first_not_of(" \t", i + 1); + j = args.find_first_of(" \t", i); + if (i == String::npos) { + throw XConfigRead(*this, "missing corner argument"); + } + + // parse next corner token + if (add) { + corners |= parseCorner(args.substr(i, j - i)); + } + else { + corners &= ~parseCorner(args.substr(i, j - i)); + } + i = args.find_first_not_of(" \t", j); + } + + return corners; +} + +Config::Interval +ConfigReadContext::parseInterval(const ArgList& args) const +{ + if (args.size() == 0) { + return Config::Interval(0.0f, 1.0f); + } + if (args.size() != 2 || args[0].empty() || args[1].empty()) { + throw XConfigRead(*this, "invalid interval \"%{1}\"", concatArgs(args)); + } + + char* end; + double startValue = strtod(args[0].c_str(), &end); + if (end[0] != '\0') { + throw XConfigRead(*this, "invalid interval \"%{1}\"", concatArgs(args)); + } + double endValue = strtod(args[1].c_str(), &end); + if (end[0] != '\0') { + throw XConfigRead(*this, "invalid interval \"%{1}\"", concatArgs(args)); + } + + if (startValue < 0 || startValue > 100 || + endValue < 0 || endValue > 100 || + startValue >= endValue) { + throw XConfigRead(*this, "invalid interval \"%{1}\"", concatArgs(args)); + } + + return Config::Interval(startValue / 100.0f, endValue / 100.0f); +} + +void +ConfigReadContext::parseNameWithArgs( + const String& type, const String& line, + const String& delim, String::size_type& index, + String& name, ArgList& args) const +{ + // skip leading whitespace + String::size_type i = line.find_first_not_of(" \t", index); + if (i == String::npos) { + throw XConfigRead(*this, String("missing ") + type); + } + + // find end of name + String::size_type j = line.find_first_of(" \t(" + delim, i); + if (j == String::npos) { + j = line.length(); + } + + // save name + name = line.substr(i, j - i); + args.clear(); + + // is it okay to not find a delimiter? + bool needDelim = (!delim.empty() && delim.find('\n') == String::npos); + + // skip whitespace + i = line.find_first_not_of(" \t", j); + if (i == String::npos && needDelim) { + // expected delimiter but didn't find it + throw XConfigRead(*this, String("missing ") + delim[0]); + } + if (i == String::npos) { + // no arguments + index = line.length(); + return; + } + if (line[i] != '(') { + // no arguments + index = i; + return; + } + + // eat '(' + ++i; + + // parse arguments + j = line.find_first_of(",)", i); + while (j != String::npos) { + // extract arg + String arg(line.substr(i, j - i)); + i = j; + + // trim whitespace + j = arg.find_first_not_of(" \t"); + if (j != String::npos) { + arg.erase(0, j); + } + j = arg.find_last_not_of(" \t"); + if (j != String::npos) { + arg.erase(j + 1); + } + + // save arg + args.push_back(arg); + + // exit loop at end of arguments + if (line[i] == ')') { + break; + } + + // eat ',' + ++i; + + // next + j = line.find_first_of(",)", i); + } + + // verify ')' + if (j == String::npos) { + // expected ) + throw XConfigRead(*this, "missing )"); + } + + // eat ')' + ++i; + + // skip whitespace + j = line.find_first_not_of(" \t", i); + if (j == String::npos && needDelim) { + // expected delimiter but didn't find it + throw XConfigRead(*this, String("missing ") + delim[0]); + } + + // verify delimiter + if (needDelim && delim.find(line[j]) == String::npos) { + throw XConfigRead(*this, String("expected ") + delim[0]); + } + + if (j == String::npos) { + j = line.length(); + } + + index = j; + return; +} + +IPlatformScreen::KeyInfo* +ConfigReadContext::parseKeystroke(const String& keystroke) const +{ + return parseKeystroke(keystroke, std::set<String>()); +} + +IPlatformScreen::KeyInfo* +ConfigReadContext::parseKeystroke(const String& keystroke, + const std::set<String>& screens) const +{ + String s = keystroke; + + KeyModifierMask mask; + if (!barrier::KeyMap::parseModifiers(s, mask)) { + throw XConfigRead(*this, "unable to parse key modifiers"); + } + + KeyID key; + if (!barrier::KeyMap::parseKey(s, key)) { + throw XConfigRead(*this, "unable to parse key"); + } + + if (key == kKeyNone && mask == 0) { + throw XConfigRead(*this, "missing key and/or modifiers in keystroke"); + } + + return IPlatformScreen::KeyInfo::alloc(key, mask, 0, 0, screens); +} + +IPlatformScreen::ButtonInfo* +ConfigReadContext::parseMouse(const String& mouse) const +{ + String s = mouse; + + KeyModifierMask mask; + if (!barrier::KeyMap::parseModifiers(s, mask)) { + throw XConfigRead(*this, "unable to parse button modifiers"); + } + + char* end; + ButtonID button = (ButtonID)strtol(s.c_str(), &end, 10); + if (*end != '\0') { + throw XConfigRead(*this, "unable to parse button"); + } + if (s.empty() || button <= 0) { + throw XConfigRead(*this, "invalid button"); + } + + return IPlatformScreen::ButtonInfo::alloc(button, mask); +} + +KeyModifierMask +ConfigReadContext::parseModifier(const String& modifiers) const +{ + String s = modifiers; + + KeyModifierMask mask; + if (!barrier::KeyMap::parseModifiers(s, mask)) { + throw XConfigRead(*this, "unable to parse modifiers"); + } + + if (mask == 0) { + throw XConfigRead(*this, "no modifiers specified"); + } + + return mask; +} + +String +ConfigReadContext::concatArgs(const ArgList& args) +{ + String s("("); + for (size_t i = 0; i < args.size(); ++i) { + if (i != 0) { + s += ","; + } + s += args[i]; + } + s += ")"; + return s; +} + + +// +// Config I/O exceptions +// + +XConfigRead::XConfigRead(const ConfigReadContext& context, + const String& error) : + m_error(barrier::string::sprintf("line %d: %s", + context.getLineNumber(), error.c_str())) +{ + // do nothing +} + +XConfigRead::XConfigRead(const ConfigReadContext& context, + const char* errorFmt, const String& arg) : + m_error(barrier::string::sprintf("line %d: ", context.getLineNumber()) + + barrier::string::format(errorFmt, arg.c_str())) +{ + // do nothing +} + +XConfigRead::~XConfigRead() _NOEXCEPT +{ + // do nothing +} + +String +XConfigRead::getWhat() const throw() +{ + return format("XConfigRead", "read error: %{1}", m_error.c_str()); +} diff --git a/src/lib/server/Config.h b/src/lib/server/Config.h new file mode 100644 index 0000000..69b01c4 --- /dev/null +++ b/src/lib/server/Config.h @@ -0,0 +1,549 @@ +/* + * barrier -- mouse and keyboard sharing utility + * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2002 Chris Schoeneman + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "server/InputFilter.h" +#include "barrier/option_types.h" +#include "barrier/protocol_types.h" +#include "barrier/IPlatformScreen.h" +#include "net/NetworkAddress.h" +#include "base/String.h" +#include "base/XBase.h" +#include "common/stdmap.h" +#include "common/stdset.h" + +#include <iosfwd> + +class Config; +class ConfigReadContext; +class IEventQueue; + +namespace std { +template <> +struct iterator_traits<Config> { + typedef String value_type; + typedef ptrdiff_t difference_type; + typedef bidirectional_iterator_tag iterator_category; + typedef String* pointer; + typedef String& reference; +}; +}; + +//! Server configuration +/*! +This class holds server configuration information. That includes +the names of screens and their aliases, the links between them, +and network addresses. + +Note that case is preserved in screen names but is ignored when +comparing names. Screen names and their aliases share a +namespace and must be unique. +*/ +class Config { +public: + typedef std::map<OptionID, OptionValue> ScreenOptions; + typedef std::pair<float, float> Interval; + + class CellEdge { + public: + CellEdge(EDirection side, float position); + CellEdge(EDirection side, const Interval&); + CellEdge(const String& name, EDirection side, const Interval&); + ~CellEdge(); + + Interval getInterval() const; + void setName(const String& newName); + String getName() const; + EDirection getSide() const; + bool overlaps(const CellEdge&) const; + bool isInside(float x) const; + + // transform position to [0,1] + float transform(float x) const; + + // transform [0,1] to position + float inverseTransform(float x) const; + + // compares side and start of interval + bool operator<(const CellEdge&) const; + + // compares side and interval + bool operator==(const CellEdge&) const; + bool operator!=(const CellEdge&) const; + + private: + void init(const String& name, EDirection side, + const Interval&); + + private: + String m_name; + EDirection m_side; + Interval m_interval; + }; + +private: + class Name { + public: + Name(Config*, const String& name); + + bool operator==(const String& name) const; + + private: + Config* m_config; + String m_name; + }; + + class Cell { + private: + typedef std::map<CellEdge, CellEdge> EdgeLinks; + + public: + typedef EdgeLinks::const_iterator const_iterator; + + bool add(const CellEdge& src, const CellEdge& dst); + void remove(EDirection side); + void remove(EDirection side, float position); + void remove(const Name& destinationName); + void rename(const Name& oldName, const String& newName); + + bool hasEdge(const CellEdge&) const; + bool overlaps(const CellEdge&) const; + + bool getLink(EDirection side, float position, + const CellEdge*& src, const CellEdge*& dst) const; + + bool operator==(const Cell&) const; + bool operator!=(const Cell&) const; + + const_iterator begin() const; + const_iterator end() const; + + private: + EdgeLinks m_neighbors; + + public: + ScreenOptions m_options; + }; + typedef std::map<String, Cell, barrier::string::CaselessCmp> CellMap; + typedef std::map<String, String, barrier::string::CaselessCmp> NameMap; + +public: + typedef Cell::const_iterator link_const_iterator; + typedef CellMap::const_iterator internal_const_iterator; + typedef NameMap::const_iterator all_const_iterator; + class const_iterator : std::iterator_traits<Config> { + public: + explicit const_iterator() : m_i() { } + explicit const_iterator(const internal_const_iterator& i) : m_i(i) { } + + const_iterator& operator=(const const_iterator& i) { + m_i = i.m_i; + return *this; + } + String operator*() { return m_i->first; } + const String* operator->() { return &(m_i->first); } + const_iterator& operator++() { ++m_i; return *this; } + const_iterator operator++(int) { return const_iterator(m_i++); } + const_iterator& operator--() { --m_i; return *this; } + const_iterator operator--(int) { return const_iterator(m_i--); } + bool operator==(const const_iterator& i) const { + return (m_i == i.m_i); + } + bool operator!=(const const_iterator& i) const { + return (m_i != i.m_i); + } + + private: + internal_const_iterator m_i; + }; + + Config(IEventQueue* events); + virtual ~Config(); + +#ifdef TEST_ENV + Config() : m_inputFilter(NULL) { } +#endif + + //! @name manipulators + //@{ + + //! Add screen + /*! + Adds a screen, returning true iff successful. If a screen or + alias with the given name exists then it fails. + */ + bool addScreen(const String& name); + + //! Rename screen + /*! + Renames a screen. All references to the name are updated. + Returns true iff successful. + */ + bool renameScreen(const String& oldName, + const String& newName); + + //! Remove screen + /*! + Removes a screen. This also removes aliases for the screen and + disconnects any connections to the screen. \c name may be an + alias. + */ + void removeScreen(const String& name); + + //! Remove all screens + /*! + Removes all screens, aliases, and connections. + */ + void removeAllScreens(); + + //! Add alias + /*! + Adds an alias for a screen name. An alias can be used + any place the canonical screen name can (except addScreen()). + Returns false if the alias name already exists or the canonical + name is unknown, otherwise returns true. + */ + bool addAlias(const String& canonical, + const String& alias); + + //! Remove alias + /*! + Removes an alias for a screen name. It returns false if the + alias is unknown or a canonical name, otherwise returns true. + */ + bool removeAlias(const String& alias); + + //! Remove aliases + /*! + Removes all aliases for a canonical screen name. It returns false + if the canonical name is unknown, otherwise returns true. + */ + bool removeAliases(const String& canonical); + + //! Remove all aliases + /*! + This removes all aliases but not the screens. + */ + void removeAllAliases(); + + //! Connect screens + /*! + Establishes a one-way connection between portions of opposite edges + of two screens. Each portion is described by an interval defined + by two numbers, the start and end of the interval half-open on the + end. The numbers range from 0 to 1, inclusive, for the left/top + to the right/bottom. The user will be able to jump from the + \c srcStart to \c srcSend interval of \c srcSide of screen + \c srcName to the opposite side of screen \c dstName in the interval + \c dstStart and \c dstEnd when both screens are connected to the + server and the user isn't locked to a screen. Returns false if + \c srcName is unknown. \c srcStart must be less than or equal to + \c srcEnd and \c dstStart must be less then or equal to \c dstEnd + and all of \c srcStart, \c srcEnd, \c dstStart, or \c dstEnd must + be inside the range [0,1]. + */ + bool connect(const String& srcName, + EDirection srcSide, + float srcStart, float srcEnd, + const String& dstName, + float dstStart, float dstEnd); + + //! Disconnect screens + /*! + Removes all connections created by connect() on side \c srcSide. + Returns false if \c srcName is unknown. + */ + bool disconnect(const String& srcName, + EDirection srcSide); + + //! Disconnect screens + /*! + Removes the connections created by connect() on side \c srcSide + covering position \c position. Returns false if \c srcName is + unknown. + */ + bool disconnect(const String& srcName, + EDirection srcSide, float position); + + //! Set server address + /*! + Set the barrier listen addresses. There is no default address so + this must be called to run a server using this configuration. + */ + void setBarrierAddress(const NetworkAddress&); + + //! Add a screen option + /*! + Adds an option and its value to the named screen. Replaces the + existing option's value if there is one. Returns true iff \c name + is a known screen. + */ + bool addOption(const String& name, + OptionID option, OptionValue value); + + //! Remove a screen option + /*! + Removes an option and its value from the named screen. Does + nothing if the option doesn't exist on the screen. Returns true + iff \c name is a known screen. + */ + bool removeOption(const String& name, OptionID option); + + //! Remove a screen options + /*! + Removes all options and values from the named screen. Returns true + iff \c name is a known screen. + */ + bool removeOptions(const String& name); + + //! Get the hot key input filter + /*! + Returns the hot key input filter. Clients can modify hotkeys using + that object. + */ + virtual InputFilter* + getInputFilter(); + + //@} + //! @name accessors + //@{ + + //! Test screen name validity + /*! + Returns true iff \c name is a valid screen name. + */ + bool isValidScreenName(const String& name) const; + + //! Get beginning (canonical) screen name iterator + const_iterator begin() const; + //! Get ending (canonical) screen name iterator + const_iterator end() const; + + //! Get beginning screen name iterator + all_const_iterator beginAll() const; + //! Get ending screen name iterator + all_const_iterator endAll() const; + + //! Test for screen name + /*! + Returns true iff \c name names a screen. + */ + virtual bool isScreen(const String& name) const; + + //! Test for canonical screen name + /*! + Returns true iff \c name is the canonical name of a screen. + */ + bool isCanonicalName(const String& name) const; + + //! Get canonical name + /*! + Returns the canonical name of a screen or the empty string if + the name is unknown. Returns the canonical name if one is given. + */ + String getCanonicalName(const String& name) const; + + //! Get neighbor + /*! + Returns the canonical screen name of the neighbor in the given + direction (set through connect()) at position \c position. Returns + the empty string if there is no neighbor in that direction, otherwise + saves the position on the neighbor in \c positionOut if it's not + \c NULL. + */ + String getNeighbor(const String&, EDirection, + float position, float* positionOut) const; + + //! Check for neighbor + /*! + Returns \c true if the screen has a neighbor anywhere along the edge + given by the direction. + */ + bool hasNeighbor(const String&, EDirection) const; + + //! Check for neighbor + /*! + Returns \c true if the screen has a neighbor in the given range along + the edge given by the direction. + */ + bool hasNeighbor(const String&, EDirection, + float start, float end) const; + + //! Get beginning neighbor iterator + link_const_iterator beginNeighbor(const String&) const; + //! Get ending neighbor iterator + link_const_iterator endNeighbor(const String&) const; + + //! Get the server address + const NetworkAddress& + getBarrierAddress() const; + + //! Get the screen options + /*! + Returns all the added options for the named screen. Returns NULL + if the screen is unknown and an empty collection if there are no + options. + */ + const ScreenOptions* + getOptions(const String& name) const; + + //! Check for lock to screen action + /*! + Returns \c true if this configuration has a lock to screen action. + This is for backwards compatible support of ScrollLock locking. + */ + bool hasLockToScreenAction() const; + + //! Compare configurations + bool operator==(const Config&) const; + //! Compare configurations + bool operator!=(const Config&) const; + + //! Read configuration + /*! + Reads a configuration from a context. Throws XConfigRead on error + and context is unchanged. + */ + void read(ConfigReadContext& context); + + //! Read configuration + /*! + Reads a configuration from a stream. Throws XConfigRead on error. + */ + friend std::istream& + operator>>(std::istream&, Config&); + + //! Write configuration + /*! + Writes a configuration to a stream. + */ + friend std::ostream& + operator<<(std::ostream&, const Config&); + + //! Get direction name + /*! + Returns the name of a direction (for debugging). + */ + static const char* dirName(EDirection); + + //! Get interval as string + /*! + Returns an interval as a parseable string. + */ + static String formatInterval(const Interval&); + + //@} + +private: + void readSection(ConfigReadContext&); + void readSectionOptions(ConfigReadContext&); + void readSectionScreens(ConfigReadContext&); + void readSectionLinks(ConfigReadContext&); + void readSectionAliases(ConfigReadContext&); + + InputFilter::Condition* + parseCondition(ConfigReadContext&, + const String& condition, + const std::vector<String>& args); + void parseAction(ConfigReadContext&, + const String& action, + const std::vector<String>& args, + InputFilter::Rule&, bool activate); + + void parseScreens(ConfigReadContext&, const String&, + std::set<String>& screens) const; + static const char* getOptionName(OptionID); + static String getOptionValue(OptionID, OptionValue); + +private: + CellMap m_map; + NameMap m_nameToCanonicalName; + NetworkAddress m_barrierAddress; + ScreenOptions m_globalOptions; + InputFilter m_inputFilter; + bool m_hasLockToScreenAction; + IEventQueue* m_events; +}; + +//! Configuration read context +/*! +Maintains a context when reading a configuration from a stream. +*/ +class ConfigReadContext { +public: + typedef std::vector<String> ArgList; + + ConfigReadContext(std::istream&, SInt32 firstLine = 1); + ~ConfigReadContext(); + + bool readLine(String&); + UInt32 getLineNumber() const; + + bool operator!() const; + + OptionValue parseBoolean(const String&) const; + OptionValue parseInt(const String&) const; + OptionValue parseModifierKey(const String&) const; + OptionValue parseCorner(const String&) const; + OptionValue parseCorners(const String&) const; + Config::Interval + parseInterval(const ArgList& args) const; + void parseNameWithArgs( + const String& type, const String& line, + const String& delim, String::size_type& index, + String& name, ArgList& args) const; + IPlatformScreen::KeyInfo* + parseKeystroke(const String& keystroke) const; + IPlatformScreen::KeyInfo* + parseKeystroke(const String& keystroke, + const std::set<String>& screens) const; + IPlatformScreen::ButtonInfo* + parseMouse(const String& mouse) const; + KeyModifierMask parseModifier(const String& modifiers) const; + std::istream& getStream() const { return m_stream; }; + +private: + // not implemented + ConfigReadContext& operator=(const ConfigReadContext&); + + static String concatArgs(const ArgList& args); + +private: + std::istream& m_stream; + SInt32 m_line; +}; + +//! Configuration stream read exception +/*! +Thrown when a configuration stream cannot be parsed. +*/ +class XConfigRead : public XBase { +public: + XConfigRead(const ConfigReadContext& context, const String&); + XConfigRead(const ConfigReadContext& context, + const char* errorFmt, const String& arg); + virtual ~XConfigRead() _NOEXCEPT; + +protected: + // XBase overrides + virtual String getWhat() const throw(); + +private: + String m_error; +}; diff --git a/src/lib/server/InputFilter.cpp b/src/lib/server/InputFilter.cpp new file mode 100644 index 0000000..9e73f45 --- /dev/null +++ b/src/lib/server/InputFilter.cpp @@ -0,0 +1,1090 @@ +/* + * barrier -- mouse and keyboard sharing utility + * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2005 Chris Schoeneman + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "server/InputFilter.h" +#include "server/Server.h" +#include "server/PrimaryClient.h" +#include "barrier/KeyMap.h" +#include "base/EventQueue.h" +#include "base/Log.h" +#include "base/TMethodEventJob.h" + +#include <cstdlib> +#include <cstring> + +// ----------------------------------------------------------------------------- +// Input Filter Condition Classes +// ----------------------------------------------------------------------------- +InputFilter::Condition::Condition() +{ + // do nothing +} + +InputFilter::Condition::~Condition() +{ + // do nothing +} + +void +InputFilter::Condition::enablePrimary(PrimaryClient*) +{ + // do nothing +} + +void +InputFilter::Condition::disablePrimary(PrimaryClient*) +{ + // do nothing +} + +InputFilter::KeystrokeCondition::KeystrokeCondition( + IEventQueue* events, IPlatformScreen::KeyInfo* info) : + m_id(0), + m_key(info->m_key), + m_mask(info->m_mask), + m_events(events) +{ + free(info); +} + +InputFilter::KeystrokeCondition::KeystrokeCondition( + IEventQueue* events, KeyID key, KeyModifierMask mask) : + m_id(0), + m_key(key), + m_mask(mask), + m_events(events) +{ + // do nothing +} + +InputFilter::KeystrokeCondition::~KeystrokeCondition() +{ + // do nothing +} + +KeyID +InputFilter::KeystrokeCondition::getKey() const +{ + return m_key; +} + +KeyModifierMask +InputFilter::KeystrokeCondition::getMask() const +{ + return m_mask; +} + +InputFilter::Condition* +InputFilter::KeystrokeCondition::clone() const +{ + return new KeystrokeCondition(m_events, m_key, m_mask); +} + +String +InputFilter::KeystrokeCondition::format() const +{ + return barrier::string::sprintf("keystroke(%s)", + barrier::KeyMap::formatKey(m_key, m_mask).c_str()); +} + +InputFilter::EFilterStatus +InputFilter::KeystrokeCondition::match(const Event& event) +{ + EFilterStatus status; + + // check for hotkey events + Event::Type type = event.getType(); + if (type == m_events->forIPrimaryScreen().hotKeyDown()) { + status = kActivate; + } + else if (type == m_events->forIPrimaryScreen().hotKeyUp()) { + status = kDeactivate; + } + else { + return kNoMatch; + } + + // check if it's our hotkey + IPrimaryScreen::HotKeyInfo* kinfo = + static_cast<IPlatformScreen::HotKeyInfo*>(event.getData()); + if (kinfo->m_id != m_id) { + return kNoMatch; + } + + return status; +} + +void +InputFilter::KeystrokeCondition::enablePrimary(PrimaryClient* primary) +{ + m_id = primary->registerHotKey(m_key, m_mask); +} + +void +InputFilter::KeystrokeCondition::disablePrimary(PrimaryClient* primary) +{ + primary->unregisterHotKey(m_id); + m_id = 0; +} + +InputFilter::MouseButtonCondition::MouseButtonCondition( + IEventQueue* events, IPlatformScreen::ButtonInfo* info) : + m_button(info->m_button), + m_mask(info->m_mask), + m_events(events) +{ + free(info); +} + +InputFilter::MouseButtonCondition::MouseButtonCondition( + IEventQueue* events, ButtonID button, KeyModifierMask mask) : + m_button(button), + m_mask(mask), + m_events(events) +{ + // do nothing +} + +InputFilter::MouseButtonCondition::~MouseButtonCondition() +{ + // do nothing +} + +ButtonID +InputFilter::MouseButtonCondition::getButton() const +{ + return m_button; +} + +KeyModifierMask +InputFilter::MouseButtonCondition::getMask() const +{ + return m_mask; +} + +InputFilter::Condition* +InputFilter::MouseButtonCondition::clone() const +{ + return new MouseButtonCondition(m_events, m_button, m_mask); +} + +String +InputFilter::MouseButtonCondition::format() const +{ + String key = barrier::KeyMap::formatKey(kKeyNone, m_mask); + if (!key.empty()) { + key += "+"; + } + return barrier::string::sprintf("mousebutton(%s%d)", key.c_str(), m_button); +} + +InputFilter::EFilterStatus +InputFilter::MouseButtonCondition::match(const Event& event) +{ + static const KeyModifierMask s_ignoreMask = + KeyModifierAltGr | KeyModifierCapsLock | + KeyModifierNumLock | KeyModifierScrollLock; + + EFilterStatus status; + + // check for hotkey events + Event::Type type = event.getType(); + if (type == m_events->forIPrimaryScreen().buttonDown()) { + status = kActivate; + } + else if (type == m_events->forIPrimaryScreen().buttonUp()) { + status = kDeactivate; + } + else { + return kNoMatch; + } + + // check if it's the right button and modifiers. ignore modifiers + // that cannot be combined with a mouse button. + IPlatformScreen::ButtonInfo* minfo = + static_cast<IPlatformScreen::ButtonInfo*>(event.getData()); + if (minfo->m_button != m_button || + (minfo->m_mask & ~s_ignoreMask) != m_mask) { + return kNoMatch; + } + + return status; +} + +InputFilter::ScreenConnectedCondition::ScreenConnectedCondition( + IEventQueue* events, const String& screen) : + m_screen(screen), + m_events(events) +{ + // do nothing +} + +InputFilter::ScreenConnectedCondition::~ScreenConnectedCondition() +{ + // do nothing +} + +InputFilter::Condition* +InputFilter::ScreenConnectedCondition::clone() const +{ + return new ScreenConnectedCondition(m_events, m_screen); +} + +String +InputFilter::ScreenConnectedCondition::format() const +{ + return barrier::string::sprintf("connect(%s)", m_screen.c_str()); +} + +InputFilter::EFilterStatus +InputFilter::ScreenConnectedCondition::match(const Event& event) +{ + if (event.getType() == m_events->forServer().connected()) { + Server::ScreenConnectedInfo* info = + static_cast<Server::ScreenConnectedInfo*>(event.getData()); + if (m_screen == info->m_screen || m_screen.empty()) { + return kActivate; + } + } + + return kNoMatch; +} + +// ----------------------------------------------------------------------------- +// Input Filter Action Classes +// ----------------------------------------------------------------------------- +InputFilter::Action::Action() +{ + // do nothing +} + +InputFilter::Action::~Action() +{ + // do nothing +} + +InputFilter::LockCursorToScreenAction::LockCursorToScreenAction( + IEventQueue* events, Mode mode) : + m_mode(mode), + m_events(events) +{ + // do nothing +} + +InputFilter::LockCursorToScreenAction::Mode +InputFilter::LockCursorToScreenAction::getMode() const +{ + return m_mode; +} + +InputFilter::Action* +InputFilter::LockCursorToScreenAction::clone() const +{ + return new LockCursorToScreenAction(*this); +} + +String +InputFilter::LockCursorToScreenAction::format() const +{ + static const char* s_mode[] = { "off", "on", "toggle" }; + + return barrier::string::sprintf("lockCursorToScreen(%s)", s_mode[m_mode]); +} + +void +InputFilter::LockCursorToScreenAction::perform(const Event& event) +{ + static const Server::LockCursorToScreenInfo::State s_state[] = { + Server::LockCursorToScreenInfo::kOff, + Server::LockCursorToScreenInfo::kOn, + Server::LockCursorToScreenInfo::kToggle + }; + + // send event + Server::LockCursorToScreenInfo* info = + Server::LockCursorToScreenInfo::alloc(s_state[m_mode]); + m_events->addEvent(Event(m_events->forServer().lockCursorToScreen(), + event.getTarget(), info, + Event::kDeliverImmediately)); +} + +InputFilter::SwitchToScreenAction::SwitchToScreenAction( + IEventQueue* events, const String& screen) : + m_screen(screen), + m_events(events) +{ + // do nothing +} + +String +InputFilter::SwitchToScreenAction::getScreen() const +{ + return m_screen; +} + +InputFilter::Action* +InputFilter::SwitchToScreenAction::clone() const +{ + return new SwitchToScreenAction(*this); +} + +String +InputFilter::SwitchToScreenAction::format() const +{ + return barrier::string::sprintf("switchToScreen(%s)", m_screen.c_str()); +} + +void +InputFilter::SwitchToScreenAction::perform(const Event& event) +{ + // pick screen name. if m_screen is empty then use the screen from + // event if it has one. + String screen = m_screen; + if (screen.empty() && event.getType() == m_events->forServer().connected()) { + Server::ScreenConnectedInfo* info = + static_cast<Server::ScreenConnectedInfo*>(event.getData()); + screen = info->m_screen; + } + + // send event + Server::SwitchToScreenInfo* info = + Server::SwitchToScreenInfo::alloc(screen); + m_events->addEvent(Event(m_events->forServer().switchToScreen(), + event.getTarget(), info, + Event::kDeliverImmediately)); +} + +InputFilter::SwitchInDirectionAction::SwitchInDirectionAction( + IEventQueue* events, EDirection direction) : + m_direction(direction), + m_events(events) +{ + // do nothing +} + +EDirection +InputFilter::SwitchInDirectionAction::getDirection() const +{ + return m_direction; +} + +InputFilter::Action* +InputFilter::SwitchInDirectionAction::clone() const +{ + return new SwitchInDirectionAction(*this); +} + +String +InputFilter::SwitchInDirectionAction::format() const +{ + static const char* s_names[] = { + "", + "left", + "right", + "up", + "down" + }; + + return barrier::string::sprintf("switchInDirection(%s)", s_names[m_direction]); +} + +void +InputFilter::SwitchInDirectionAction::perform(const Event& event) +{ + Server::SwitchInDirectionInfo* info = + Server::SwitchInDirectionInfo::alloc(m_direction); + m_events->addEvent(Event(m_events->forServer().switchInDirection(), + event.getTarget(), info, + Event::kDeliverImmediately)); +} + +InputFilter::KeyboardBroadcastAction::KeyboardBroadcastAction( + IEventQueue* events, Mode mode) : + m_mode(mode), + m_events(events) +{ + // do nothing +} + +InputFilter::KeyboardBroadcastAction::KeyboardBroadcastAction( + IEventQueue* events, + Mode mode, + const std::set<String>& screens) : + m_mode(mode), + m_screens(IKeyState::KeyInfo::join(screens)), + m_events(events) +{ + // do nothing +} + +InputFilter::KeyboardBroadcastAction::Mode +InputFilter::KeyboardBroadcastAction::getMode() const +{ + return m_mode; +} + +std::set<String> +InputFilter::KeyboardBroadcastAction::getScreens() const +{ + std::set<String> screens; + IKeyState::KeyInfo::split(m_screens.c_str(), screens); + return screens; +} + +InputFilter::Action* +InputFilter::KeyboardBroadcastAction::clone() const +{ + return new KeyboardBroadcastAction(*this); +} + +String +InputFilter::KeyboardBroadcastAction::format() const +{ + static const char* s_mode[] = { "off", "on", "toggle" }; + static const char* s_name = "keyboardBroadcast"; + + if (m_screens.empty() || m_screens[0] == '*') { + return barrier::string::sprintf("%s(%s)", s_name, s_mode[m_mode]); + } + else { + return barrier::string::sprintf("%s(%s,%.*s)", s_name, s_mode[m_mode], + m_screens.size() - 2, + m_screens.c_str() + 1); + } +} + +void +InputFilter::KeyboardBroadcastAction::perform(const Event& event) +{ + static const Server::KeyboardBroadcastInfo::State s_state[] = { + Server::KeyboardBroadcastInfo::kOff, + Server::KeyboardBroadcastInfo::kOn, + Server::KeyboardBroadcastInfo::kToggle + }; + + // send event + Server::KeyboardBroadcastInfo* info = + Server::KeyboardBroadcastInfo::alloc(s_state[m_mode], m_screens); + m_events->addEvent(Event(m_events->forServer().keyboardBroadcast(), + event.getTarget(), info, + Event::kDeliverImmediately)); +} + +InputFilter::KeystrokeAction::KeystrokeAction( + IEventQueue* events, IPlatformScreen::KeyInfo* info, bool press) : + m_keyInfo(info), + m_press(press), + m_events(events) +{ + // do nothing +} + +InputFilter::KeystrokeAction::~KeystrokeAction() +{ + free(m_keyInfo); +} + +void +InputFilter::KeystrokeAction::adoptInfo(IPlatformScreen::KeyInfo* info) +{ + free(m_keyInfo); + m_keyInfo = info; +} + +const IPlatformScreen::KeyInfo* +InputFilter::KeystrokeAction::getInfo() const +{ + return m_keyInfo; +} + +bool +InputFilter::KeystrokeAction::isOnPress() const +{ + return m_press; +} + +InputFilter::Action* +InputFilter::KeystrokeAction::clone() const +{ + IKeyState::KeyInfo* info = IKeyState::KeyInfo::alloc(*m_keyInfo); + return new KeystrokeAction(m_events, info, m_press); +} + +String +InputFilter::KeystrokeAction::format() const +{ + const char* type = formatName(); + + if (m_keyInfo->m_screens[0] == '\0') { + return barrier::string::sprintf("%s(%s)", type, + barrier::KeyMap::formatKey(m_keyInfo->m_key, + m_keyInfo->m_mask).c_str()); + } + else if (m_keyInfo->m_screens[0] == '*') { + return barrier::string::sprintf("%s(%s,*)", type, + barrier::KeyMap::formatKey(m_keyInfo->m_key, + m_keyInfo->m_mask).c_str()); + } + else { + return barrier::string::sprintf("%s(%s,%.*s)", type, + barrier::KeyMap::formatKey(m_keyInfo->m_key, + m_keyInfo->m_mask).c_str(), + strlen(m_keyInfo->m_screens + 1) - 1, + m_keyInfo->m_screens + 1); + } +} + +void +InputFilter::KeystrokeAction::perform(const Event& event) +{ + Event::Type type = m_press ? + m_events->forIKeyState().keyDown() : + m_events->forIKeyState().keyUp(); + + m_events->addEvent(Event(m_events->forIPrimaryScreen().fakeInputBegin(), + event.getTarget(), NULL, + Event::kDeliverImmediately)); + m_events->addEvent(Event(type, event.getTarget(), m_keyInfo, + Event::kDeliverImmediately | + Event::kDontFreeData)); + m_events->addEvent(Event(m_events->forIPrimaryScreen().fakeInputEnd(), + event.getTarget(), NULL, + Event::kDeliverImmediately)); +} + +const char* +InputFilter::KeystrokeAction::formatName() const +{ + return (m_press ? "keyDown" : "keyUp"); +} + +InputFilter::MouseButtonAction::MouseButtonAction( + IEventQueue* events, IPlatformScreen::ButtonInfo* info, bool press) : + m_buttonInfo(info), + m_press(press), + m_events(events) +{ + // do nothing +} + +InputFilter::MouseButtonAction::~MouseButtonAction() +{ + free(m_buttonInfo); +} + +const IPlatformScreen::ButtonInfo* +InputFilter::MouseButtonAction::getInfo() const +{ + return m_buttonInfo; +} + +bool +InputFilter::MouseButtonAction::isOnPress() const +{ + return m_press; +} + +InputFilter::Action* +InputFilter::MouseButtonAction::clone() const +{ + IPlatformScreen::ButtonInfo* info = + IPrimaryScreen::ButtonInfo::alloc(*m_buttonInfo); + return new MouseButtonAction(m_events, info, m_press); +} + +String +InputFilter::MouseButtonAction::format() const +{ + const char* type = formatName(); + + String key = barrier::KeyMap::formatKey(kKeyNone, m_buttonInfo->m_mask); + return barrier::string::sprintf("%s(%s%s%d)", type, + key.c_str(), key.empty() ? "" : "+", + m_buttonInfo->m_button); +} + +void +InputFilter::MouseButtonAction::perform(const Event& event) + +{ + // send modifiers + IPlatformScreen::KeyInfo* modifierInfo = NULL; + if (m_buttonInfo->m_mask != 0) { + KeyID key = m_press ? kKeySetModifiers : kKeyClearModifiers; + modifierInfo = + IKeyState::KeyInfo::alloc(key, m_buttonInfo->m_mask, 0, 1); + m_events->addEvent(Event(m_events->forIKeyState().keyDown(), + event.getTarget(), modifierInfo, + Event::kDeliverImmediately)); + } + + // send button + Event::Type type = m_press ? m_events->forIPrimaryScreen().buttonDown() : + m_events->forIPrimaryScreen().buttonUp(); + m_events->addEvent(Event(type, event.getTarget(), m_buttonInfo, + Event::kDeliverImmediately | + Event::kDontFreeData)); +} + +const char* +InputFilter::MouseButtonAction::formatName() const +{ + return (m_press ? "mouseDown" : "mouseUp"); +} + +// +// InputFilter::Rule +// + +InputFilter::Rule::Rule() : + m_condition(NULL) +{ + // do nothing +} + +InputFilter::Rule::Rule(Condition* adoptedCondition) : + m_condition(adoptedCondition) +{ + // do nothing +} + +InputFilter::Rule::Rule(const Rule& rule) : + m_condition(NULL) +{ + copy(rule); +} + +InputFilter::Rule::~Rule() +{ + clear(); +} + +InputFilter::Rule& +InputFilter::Rule::operator=(const Rule& rule) +{ + if (&rule != this) { + copy(rule); + } + return *this; +} + +void +InputFilter::Rule::clear() +{ + delete m_condition; + for (ActionList::iterator i = m_activateActions.begin(); + i != m_activateActions.end(); ++i) { + delete *i; + } + for (ActionList::iterator i = m_deactivateActions.begin(); + i != m_deactivateActions.end(); ++i) { + delete *i; + } + + m_condition = NULL; + m_activateActions.clear(); + m_deactivateActions.clear(); +} + +void +InputFilter::Rule::copy(const Rule& rule) +{ + clear(); + if (rule.m_condition != NULL) { + m_condition = rule.m_condition->clone(); + } + for (ActionList::const_iterator i = rule.m_activateActions.begin(); + i != rule.m_activateActions.end(); ++i) { + m_activateActions.push_back((*i)->clone()); + } + for (ActionList::const_iterator i = rule.m_deactivateActions.begin(); + i != rule.m_deactivateActions.end(); ++i) { + m_deactivateActions.push_back((*i)->clone()); + } +} + +void +InputFilter::Rule::setCondition(Condition* adopted) +{ + delete m_condition; + m_condition = adopted; +} + +void +InputFilter::Rule::adoptAction(Action* action, bool onActivation) +{ + if (action != NULL) { + if (onActivation) { + m_activateActions.push_back(action); + } + else { + m_deactivateActions.push_back(action); + } + } +} + +void +InputFilter::Rule::removeAction(bool onActivation, UInt32 index) +{ + if (onActivation) { + delete m_activateActions[index]; + m_activateActions.erase(m_activateActions.begin() + index); + } + else { + delete m_deactivateActions[index]; + m_deactivateActions.erase(m_deactivateActions.begin() + index); + } +} + +void +InputFilter::Rule::replaceAction(Action* adopted, + bool onActivation, UInt32 index) +{ + if (adopted == NULL) { + removeAction(onActivation, index); + } + else if (onActivation) { + delete m_activateActions[index]; + m_activateActions[index] = adopted; + } + else { + delete m_deactivateActions[index]; + m_deactivateActions[index] = adopted; + } +} + +void +InputFilter::Rule::enable(PrimaryClient* primaryClient) +{ + if (m_condition != NULL) { + m_condition->enablePrimary(primaryClient); + } +} + +void +InputFilter::Rule::disable(PrimaryClient* primaryClient) +{ + if (m_condition != NULL) { + m_condition->disablePrimary(primaryClient); + } +} + +bool +InputFilter::Rule::handleEvent(const Event& event) +{ + // NULL condition never matches + if (m_condition == NULL) { + return false; + } + + // match + const ActionList* actions; + switch (m_condition->match(event)) { + default: + // not handled + return false; + + case kActivate: + actions = &m_activateActions; + LOG((CLOG_DEBUG1 "activate actions")); + break; + + case kDeactivate: + actions = &m_deactivateActions; + LOG((CLOG_DEBUG1 "deactivate actions")); + break; + } + + // perform actions + for (ActionList::const_iterator i = actions->begin(); + i != actions->end(); ++i) { + LOG((CLOG_DEBUG1 "hotkey: %s", (*i)->format().c_str())); + (*i)->perform(event); + } + + return true; +} + +String +InputFilter::Rule::format() const +{ + String s; + if (m_condition != NULL) { + // condition + s += m_condition->format(); + s += " = "; + + // activate actions + ActionList::const_iterator i = m_activateActions.begin(); + if (i != m_activateActions.end()) { + s += (*i)->format(); + while (++i != m_activateActions.end()) { + s += ", "; + s += (*i)->format(); + } + } + + // deactivate actions + if (!m_deactivateActions.empty()) { + s += "; "; + i = m_deactivateActions.begin(); + if (i != m_deactivateActions.end()) { + s += (*i)->format(); + while (++i != m_deactivateActions.end()) { + s += ", "; + s += (*i)->format(); + } + } + } + } + return s; +} + +const InputFilter::Condition* +InputFilter::Rule::getCondition() const +{ + return m_condition; +} + +UInt32 +InputFilter::Rule::getNumActions(bool onActivation) const +{ + if (onActivation) { + return static_cast<UInt32>(m_activateActions.size()); + } + else { + return static_cast<UInt32>(m_deactivateActions.size()); + } +} + +const InputFilter::Action& +InputFilter::Rule::getAction(bool onActivation, UInt32 index) const +{ + if (onActivation) { + return *m_activateActions[index]; + } + else { + return *m_deactivateActions[index]; + } +} + + +// ----------------------------------------------------------------------------- +// Input Filter Class +// ----------------------------------------------------------------------------- +InputFilter::InputFilter(IEventQueue* events) : + m_primaryClient(NULL), + m_events(events) +{ + // do nothing +} + +InputFilter::InputFilter(const InputFilter& x) : + m_ruleList(x.m_ruleList), + m_primaryClient(NULL), + m_events(x.m_events) +{ + setPrimaryClient(x.m_primaryClient); +} + +InputFilter::~InputFilter() +{ + setPrimaryClient(NULL); +} + +InputFilter& +InputFilter::operator=(const InputFilter& x) +{ + if (&x != this) { + PrimaryClient* oldClient = m_primaryClient; + setPrimaryClient(NULL); + + m_ruleList = x.m_ruleList; + + setPrimaryClient(oldClient); + } + return *this; +} + +void +InputFilter::addFilterRule(const Rule& rule) +{ + m_ruleList.push_back(rule); + if (m_primaryClient != NULL) { + m_ruleList.back().enable(m_primaryClient); + } +} + +void +InputFilter::removeFilterRule(UInt32 index) +{ + if (m_primaryClient != NULL) { + m_ruleList[index].disable(m_primaryClient); + } + m_ruleList.erase(m_ruleList.begin() + index); +} + +InputFilter::Rule& +InputFilter::getRule(UInt32 index) +{ + return m_ruleList[index]; +} + +void +InputFilter::setPrimaryClient(PrimaryClient* client) +{ + if (m_primaryClient == client) { + return; + } + + if (m_primaryClient != NULL) { + for (RuleList::iterator rule = m_ruleList.begin(); + rule != m_ruleList.end(); ++rule) { + rule->disable(m_primaryClient); + } + + m_events->removeHandler(m_events->forIKeyState().keyDown(), + m_primaryClient->getEventTarget()); + m_events->removeHandler(m_events->forIKeyState().keyUp(), + m_primaryClient->getEventTarget()); + m_events->removeHandler(m_events->forIKeyState().keyRepeat(), + m_primaryClient->getEventTarget()); + m_events->removeHandler(m_events->forIPrimaryScreen().buttonDown(), + m_primaryClient->getEventTarget()); + m_events->removeHandler(m_events->forIPrimaryScreen().buttonUp(), + m_primaryClient->getEventTarget()); + m_events->removeHandler(m_events->forIPrimaryScreen().hotKeyDown(), + m_primaryClient->getEventTarget()); + m_events->removeHandler(m_events->forIPrimaryScreen().hotKeyUp(), + m_primaryClient->getEventTarget()); + m_events->removeHandler(m_events->forServer().connected(), + m_primaryClient->getEventTarget()); + } + + m_primaryClient = client; + + if (m_primaryClient != NULL) { + m_events->adoptHandler(m_events->forIKeyState().keyDown(), + m_primaryClient->getEventTarget(), + new TMethodEventJob<InputFilter>(this, + &InputFilter::handleEvent)); + m_events->adoptHandler(m_events->forIKeyState().keyUp(), + m_primaryClient->getEventTarget(), + new TMethodEventJob<InputFilter>(this, + &InputFilter::handleEvent)); + m_events->adoptHandler(m_events->forIKeyState().keyRepeat(), + m_primaryClient->getEventTarget(), + new TMethodEventJob<InputFilter>(this, + &InputFilter::handleEvent)); + m_events->adoptHandler(m_events->forIPrimaryScreen().buttonDown(), + m_primaryClient->getEventTarget(), + new TMethodEventJob<InputFilter>(this, + &InputFilter::handleEvent)); + m_events->adoptHandler(m_events->forIPrimaryScreen().buttonUp(), + m_primaryClient->getEventTarget(), + new TMethodEventJob<InputFilter>(this, + &InputFilter::handleEvent)); + m_events->adoptHandler(m_events->forIPrimaryScreen().hotKeyDown(), + m_primaryClient->getEventTarget(), + new TMethodEventJob<InputFilter>(this, + &InputFilter::handleEvent)); + m_events->adoptHandler(m_events->forIPrimaryScreen().hotKeyUp(), + m_primaryClient->getEventTarget(), + new TMethodEventJob<InputFilter>(this, + &InputFilter::handleEvent)); + m_events->adoptHandler(m_events->forServer().connected(), + m_primaryClient->getEventTarget(), + new TMethodEventJob<InputFilter>(this, + &InputFilter::handleEvent)); + + for (RuleList::iterator rule = m_ruleList.begin(); + rule != m_ruleList.end(); ++rule) { + rule->enable(m_primaryClient); + } + } +} + +String +InputFilter::format(const String& linePrefix) const +{ + String s; + for (RuleList::const_iterator i = m_ruleList.begin(); + i != m_ruleList.end(); ++i) { + s += linePrefix; + s += i->format(); + s += "\n"; + } + return s; +} + +UInt32 +InputFilter::getNumRules() const +{ + return static_cast<UInt32>(m_ruleList.size()); +} + +bool +InputFilter::operator==(const InputFilter& x) const +{ + // if there are different numbers of rules then we can't be equal + if (m_ruleList.size() != x.m_ruleList.size()) { + return false; + } + + // compare rule lists. the easiest way to do that is to format each + // rule into a string, sort the strings, then compare the results. + std::vector<String> aList, bList; + for (RuleList::const_iterator i = m_ruleList.begin(); + i != m_ruleList.end(); ++i) { + aList.push_back(i->format()); + } + for (RuleList::const_iterator i = x.m_ruleList.begin(); + i != x.m_ruleList.end(); ++i) { + bList.push_back(i->format()); + } + std::partial_sort(aList.begin(), aList.end(), aList.end()); + std::partial_sort(bList.begin(), bList.end(), bList.end()); + return (aList == bList); +} + +bool +InputFilter::operator!=(const InputFilter& x) const +{ + return !operator==(x); +} + +void +InputFilter::handleEvent(const Event& event, void*) +{ + // copy event and adjust target + Event myEvent(event.getType(), this, event.getData(), + event.getFlags() | Event::kDontFreeData | + Event::kDeliverImmediately); + + // let each rule try to match the event until one does + for (RuleList::iterator rule = m_ruleList.begin(); + rule != m_ruleList.end(); ++rule) { + if (rule->handleEvent(myEvent)) { + // handled + return; + } + } + + // not handled so pass through + m_events->addEvent(myEvent); +} diff --git a/src/lib/server/InputFilter.h b/src/lib/server/InputFilter.h new file mode 100644 index 0000000..73afe97 --- /dev/null +++ b/src/lib/server/InputFilter.h @@ -0,0 +1,361 @@ +/* + * barrier -- mouse and keyboard sharing utility + * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2005 Chris Schoeneman + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "barrier/key_types.h" +#include "barrier/mouse_types.h" +#include "barrier/protocol_types.h" +#include "barrier/IPlatformScreen.h" +#include "base/String.h" +#include "common/stdmap.h" +#include "common/stdset.h" + +class PrimaryClient; +class Event; +class IEventQueue; + +class InputFilter { +public: + // ------------------------------------------------------------------------- + // Input Filter Condition Classes + // ------------------------------------------------------------------------- + enum EFilterStatus { + kNoMatch, + kActivate, + kDeactivate + }; + + class Condition { + public: + Condition(); + virtual ~Condition(); + + virtual Condition* clone() const = 0; + virtual String format() const = 0; + + virtual EFilterStatus match(const Event&) = 0; + + virtual void enablePrimary(PrimaryClient*); + virtual void disablePrimary(PrimaryClient*); + }; + + // KeystrokeCondition + class KeystrokeCondition : public Condition { + public: + KeystrokeCondition(IEventQueue* events, IPlatformScreen::KeyInfo*); + KeystrokeCondition(IEventQueue* events, KeyID key, KeyModifierMask mask); + virtual ~KeystrokeCondition(); + + KeyID getKey() const; + KeyModifierMask getMask() const; + + // Condition overrides + virtual Condition* clone() const; + virtual String format() const; + virtual EFilterStatus match(const Event&); + virtual void enablePrimary(PrimaryClient*); + virtual void disablePrimary(PrimaryClient*); + + private: + UInt32 m_id; + KeyID m_key; + KeyModifierMask m_mask; + IEventQueue* m_events; + }; + + // MouseButtonCondition + class MouseButtonCondition : public Condition { + public: + MouseButtonCondition(IEventQueue* events, IPlatformScreen::ButtonInfo*); + MouseButtonCondition(IEventQueue* events, ButtonID, KeyModifierMask mask); + virtual ~MouseButtonCondition(); + + ButtonID getButton() const; + KeyModifierMask getMask() const; + + // Condition overrides + virtual Condition* clone() const; + virtual String format() const; + virtual EFilterStatus match(const Event&); + + private: + ButtonID m_button; + KeyModifierMask m_mask; + IEventQueue* m_events; + }; + + // ScreenConnectedCondition + class ScreenConnectedCondition : public Condition { + public: + ScreenConnectedCondition(IEventQueue* events, const String& screen); + virtual ~ScreenConnectedCondition(); + + // Condition overrides + virtual Condition* clone() const; + virtual String format() const; + virtual EFilterStatus match(const Event&); + + private: + String m_screen; + IEventQueue* m_events; + }; + + // ------------------------------------------------------------------------- + // Input Filter Action Classes + // ------------------------------------------------------------------------- + + class Action { + public: + Action(); + virtual ~Action(); + + virtual Action* clone() const = 0; + virtual String format() const = 0; + + virtual void perform(const Event&) = 0; + }; + + // LockCursorToScreenAction + class LockCursorToScreenAction : public Action { + public: + enum Mode { kOff, kOn, kToggle }; + + LockCursorToScreenAction(IEventQueue* events, Mode = kToggle); + + Mode getMode() const; + + // Action overrides + virtual Action* clone() const; + virtual String format() const; + virtual void perform(const Event&); + + private: + Mode m_mode; + IEventQueue* m_events; + }; + + // SwitchToScreenAction + class SwitchToScreenAction : public Action { + public: + SwitchToScreenAction(IEventQueue* events, const String& screen); + + String getScreen() const; + + // Action overrides + virtual Action* clone() const; + virtual String format() const; + virtual void perform(const Event&); + + private: + String m_screen; + IEventQueue* m_events; + }; + + // SwitchInDirectionAction + class SwitchInDirectionAction : public Action { + public: + SwitchInDirectionAction(IEventQueue* events, EDirection); + + EDirection getDirection() const; + + // Action overrides + virtual Action* clone() const; + virtual String format() const; + virtual void perform(const Event&); + + private: + EDirection m_direction; + IEventQueue* m_events; + }; + + // KeyboardBroadcastAction + class KeyboardBroadcastAction : public Action { + public: + enum Mode { kOff, kOn, kToggle }; + + KeyboardBroadcastAction(IEventQueue* events, Mode = kToggle); + KeyboardBroadcastAction(IEventQueue* events, Mode, const std::set<String>& screens); + + Mode getMode() const; + std::set<String> getScreens() const; + + // Action overrides + virtual Action* clone() const; + virtual String format() const; + virtual void perform(const Event&); + + private: + Mode m_mode; + String m_screens; + IEventQueue* m_events; + }; + + // KeystrokeAction + class KeystrokeAction : public Action { + public: + KeystrokeAction(IEventQueue* events, IPlatformScreen::KeyInfo* adoptedInfo, bool press); + ~KeystrokeAction(); + + void adoptInfo(IPlatformScreen::KeyInfo*); + const IPlatformScreen::KeyInfo* + getInfo() const; + bool isOnPress() const; + + // Action overrides + virtual Action* clone() const; + virtual String format() const; + virtual void perform(const Event&); + + protected: + virtual const char* formatName() const; + + private: + IPlatformScreen::KeyInfo* m_keyInfo; + bool m_press; + IEventQueue* m_events; + }; + + // MouseButtonAction -- modifier combinations not implemented yet + class MouseButtonAction : public Action { + public: + MouseButtonAction(IEventQueue* events, + IPlatformScreen::ButtonInfo* adoptedInfo, + bool press); + ~MouseButtonAction(); + + const IPlatformScreen::ButtonInfo* + getInfo() const; + bool isOnPress() const; + + // Action overrides + virtual Action* clone() const; + virtual String format() const; + virtual void perform(const Event&); + + protected: + virtual const char* formatName() const; + + private: + IPlatformScreen::ButtonInfo* m_buttonInfo; + bool m_press; + IEventQueue* m_events; + }; + + class Rule { + public: + Rule(); + Rule(Condition* adopted); + Rule(const Rule&); + ~Rule(); + + Rule& operator=(const Rule&); + + // replace the condition + void setCondition(Condition* adopted); + + // add an action to the rule + void adoptAction(Action*, bool onActivation); + + // remove an action from the rule + void removeAction(bool onActivation, UInt32 index); + + // replace an action in the rule + void replaceAction(Action* adopted, + bool onActivation, UInt32 index); + + // enable/disable + void enable(PrimaryClient*); + void disable(PrimaryClient*); + + // event handling + bool handleEvent(const Event&); + + // convert rule to a string + String format() const; + + // get the rule's condition + const Condition* + getCondition() const; + + // get number of actions + UInt32 getNumActions(bool onActivation) const; + + // get action by index + const Action& getAction(bool onActivation, UInt32 index) const; + + private: + void clear(); + void copy(const Rule&); + + private: + typedef std::vector<Action*> ActionList; + + Condition* m_condition; + ActionList m_activateActions; + ActionList m_deactivateActions; + }; + + // ------------------------------------------------------------------------- + // Input Filter Class + // ------------------------------------------------------------------------- + typedef std::vector<Rule> RuleList; + + InputFilter(IEventQueue* events); + InputFilter(const InputFilter&); + virtual ~InputFilter(); + +#ifdef TEST_ENV + InputFilter() : m_primaryClient(NULL) { } +#endif + + InputFilter& operator=(const InputFilter&); + + // add rule, adopting the condition and the actions + void addFilterRule(const Rule& rule); + + // remove a rule + void removeFilterRule(UInt32 index); + + // get rule by index + Rule& getRule(UInt32 index); + + // enable event filtering using the given primary client. disable + // if client is NULL. + virtual void setPrimaryClient(PrimaryClient* client); + + // convert rules to a string + String format(const String& linePrefix) const; + + // get number of rules + UInt32 getNumRules() const; + + //! Compare filters + bool operator==(const InputFilter&) const; + //! Compare filters + bool operator!=(const InputFilter&) const; + +private: + // event handling + void handleEvent(const Event&, void*); + +private: + RuleList m_ruleList; + PrimaryClient* m_primaryClient; + IEventQueue* m_events; +}; diff --git a/src/lib/server/PrimaryClient.cpp b/src/lib/server/PrimaryClient.cpp new file mode 100644 index 0000000..4c9fe50 --- /dev/null +++ b/src/lib/server/PrimaryClient.cpp @@ -0,0 +1,274 @@ +/* + * barrier -- mouse and keyboard sharing utility + * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2002 Chris Schoeneman + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "server/PrimaryClient.h" + +#include "barrier/Screen.h" +#include "barrier/Clipboard.h" +#include "base/Log.h" + +// +// PrimaryClient +// + +PrimaryClient::PrimaryClient(const String& name, barrier::Screen* screen) : + BaseClientProxy(name), + m_screen(screen), + m_fakeInputCount(0) +{ + // all clipboards are clean + for (UInt32 i = 0; i < kClipboardEnd; ++i) { + m_clipboardDirty[i] = false; + } +} + +PrimaryClient::~PrimaryClient() +{ + // do nothing +} + +void +PrimaryClient::reconfigure(UInt32 activeSides) +{ + m_screen->reconfigure(activeSides); +} + +UInt32 +PrimaryClient::registerHotKey(KeyID key, KeyModifierMask mask) +{ + return m_screen->registerHotKey(key, mask); +} + +void +PrimaryClient::unregisterHotKey(UInt32 id) +{ + m_screen->unregisterHotKey(id); +} + +void +PrimaryClient::fakeInputBegin() +{ + if (++m_fakeInputCount == 1) { + m_screen->fakeInputBegin(); + } +} + +void +PrimaryClient::fakeInputEnd() +{ + if (--m_fakeInputCount == 0) { + m_screen->fakeInputEnd(); + } +} + +SInt32 +PrimaryClient::getJumpZoneSize() const +{ + return m_screen->getJumpZoneSize(); +} + +void +PrimaryClient::getCursorCenter(SInt32& x, SInt32& y) const +{ + m_screen->getCursorCenter(x, y); +} + +KeyModifierMask +PrimaryClient::getToggleMask() const +{ + return m_screen->pollActiveModifiers(); +} + +bool +PrimaryClient::isLockedToScreen() const +{ + return m_screen->isLockedToScreen(); +} + +void* +PrimaryClient::getEventTarget() const +{ + return m_screen->getEventTarget(); +} + +bool +PrimaryClient::getClipboard(ClipboardID id, IClipboard* clipboard) const +{ + return m_screen->getClipboard(id, clipboard); +} + +void +PrimaryClient::getShape(SInt32& x, SInt32& y, + SInt32& width, SInt32& height) const +{ + m_screen->getShape(x, y, width, height); +} + +void +PrimaryClient::getCursorPos(SInt32& x, SInt32& y) const +{ + m_screen->getCursorPos(x, y); +} + +void +PrimaryClient::enable() +{ + m_screen->enable(); +} + +void +PrimaryClient::disable() +{ + m_screen->disable(); +} + +void +PrimaryClient::enter(SInt32 xAbs, SInt32 yAbs, + UInt32 seqNum, KeyModifierMask mask, bool screensaver) +{ + m_screen->setSequenceNumber(seqNum); + if (!screensaver) { + m_screen->warpCursor(xAbs, yAbs); + } + m_screen->enter(mask); +} + +bool +PrimaryClient::leave() +{ + return m_screen->leave(); +} + +void +PrimaryClient::setClipboard(ClipboardID id, const IClipboard* clipboard) +{ + // ignore if this clipboard is already clean + if (m_clipboardDirty[id]) { + // this clipboard is now clean + m_clipboardDirty[id] = false; + + // set clipboard + m_screen->setClipboard(id, clipboard); + } +} + +void +PrimaryClient::grabClipboard(ClipboardID id) +{ + // grab clipboard + m_screen->grabClipboard(id); + + // clipboard is dirty (because someone else owns it now) + m_clipboardDirty[id] = true; +} + +void +PrimaryClient::setClipboardDirty(ClipboardID id, bool dirty) +{ + m_clipboardDirty[id] = dirty; +} + +void +PrimaryClient::keyDown(KeyID key, KeyModifierMask mask, KeyButton button) +{ + if (m_fakeInputCount > 0) { +// XXX -- don't forward keystrokes to primary screen for now + (void)key; + (void)mask; + (void)button; +// m_screen->keyDown(key, mask, button); + } +} + +void +PrimaryClient::keyRepeat(KeyID, KeyModifierMask, SInt32, KeyButton) +{ + // ignore +} + +void +PrimaryClient::keyUp(KeyID key, KeyModifierMask mask, KeyButton button) +{ + if (m_fakeInputCount > 0) { +// XXX -- don't forward keystrokes to primary screen for now + (void)key; + (void)mask; + (void)button; +// m_screen->keyUp(key, mask, button); + } +} + +void +PrimaryClient::mouseDown(ButtonID) +{ + // ignore +} + +void +PrimaryClient::mouseUp(ButtonID) +{ + // ignore +} + +void +PrimaryClient::mouseMove(SInt32 x, SInt32 y) +{ + m_screen->warpCursor(x, y); +} + +void +PrimaryClient::mouseRelativeMove(SInt32, SInt32) +{ + // ignore +} + +void +PrimaryClient::mouseWheel(SInt32, SInt32) +{ + // ignore +} + +void +PrimaryClient::screensaver(bool) +{ + // ignore +} + +void +PrimaryClient::sendDragInfo(UInt32 fileCount, const char* info, size_t size) +{ + // ignore +} + +void +PrimaryClient::fileChunkSending(UInt8 mark, char* data, size_t dataSize) +{ + // ignore +} + +void +PrimaryClient::resetOptions() +{ + m_screen->resetOptions(); +} + +void +PrimaryClient::setOptions(const OptionsList& options) +{ + m_screen->setOptions(options); +} diff --git a/src/lib/server/PrimaryClient.h b/src/lib/server/PrimaryClient.h new file mode 100644 index 0000000..4296aaa --- /dev/null +++ b/src/lib/server/PrimaryClient.h @@ -0,0 +1,156 @@ +/* + * barrier -- mouse and keyboard sharing utility + * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2002 Chris Schoeneman + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "server/BaseClientProxy.h" +#include "barrier/protocol_types.h" + +namespace barrier { class Screen; } + +//! Primary screen as pseudo-client +/*! +The primary screen does not have a client associated with it. This +class provides a pseudo-client to allow the primary screen to be +treated as if it was a client. +*/ +class PrimaryClient : public BaseClientProxy { +public: + /*! + \c name is the name of the server and \p screen is primary screen. + */ + PrimaryClient(const String& name, barrier::Screen* screen); + ~PrimaryClient(); + +#ifdef TEST_ENV + PrimaryClient() : BaseClientProxy("") { } +#endif + + //! @name manipulators + //@{ + + //! Update configuration + /*! + Handles reconfiguration of jump zones. + */ + virtual void reconfigure(UInt32 activeSides); + + //! Register a system hotkey + /*! + Registers a system-wide hotkey for key \p key with modifiers \p mask. + Returns an id used to unregister the hotkey. + */ + virtual UInt32 registerHotKey(KeyID key, KeyModifierMask mask); + + //! Unregister a system hotkey + /*! + Unregisters a previously registered hot key. + */ + virtual void unregisterHotKey(UInt32 id); + + //! Prepare to synthesize input on primary screen + /*! + Prepares the primary screen to receive synthesized input. We do not + want to receive this synthesized input as user input so this method + ensures that we ignore it. Calls to \c fakeInputBegin() and + \c fakeInputEnd() may be nested; only the outermost have an effect. + */ + void fakeInputBegin(); + + //! Done synthesizing input on primary screen + /*! + Undoes whatever \c fakeInputBegin() did. + */ + void fakeInputEnd(); + + //@} + //! @name accessors + //@{ + + //! Get jump zone size + /*! + Return the jump zone size, the size of the regions on the edges of + the screen that cause the cursor to jump to another screen. + */ + SInt32 getJumpZoneSize() const; + + //! Get cursor center position + /*! + Return the cursor center position which is where we park the + cursor to compute cursor motion deltas and should be far from + the edges of the screen, typically the center. + */ + void getCursorCenter(SInt32& x, SInt32& y) const; + + //! Get toggle key state + /*! + Returns the primary screen's current toggle modifier key state. + */ + virtual KeyModifierMask + getToggleMask() const; + + //! Get screen lock state + /*! + Returns true if the user is locked to the screen. + */ + bool isLockedToScreen() const; + + //@} + + // FIXME -- these probably belong on IScreen + virtual void enable(); + virtual void disable(); + + // IScreen overrides + virtual void* getEventTarget() const; + virtual bool getClipboard(ClipboardID id, IClipboard*) const; + virtual void getShape(SInt32& x, SInt32& y, + SInt32& width, SInt32& height) const; + virtual void getCursorPos(SInt32& x, SInt32& y) const; + + // IClient overrides + virtual void enter(SInt32 xAbs, SInt32 yAbs, + UInt32 seqNum, KeyModifierMask mask, + bool forScreensaver); + virtual bool leave(); + virtual void setClipboard(ClipboardID, const IClipboard*); + virtual void grabClipboard(ClipboardID); + virtual void setClipboardDirty(ClipboardID, bool); + virtual void keyDown(KeyID, KeyModifierMask, KeyButton); + virtual void keyRepeat(KeyID, KeyModifierMask, + SInt32 count, KeyButton); + virtual void keyUp(KeyID, KeyModifierMask, KeyButton); + virtual void mouseDown(ButtonID); + virtual void mouseUp(ButtonID); + virtual void mouseMove(SInt32 xAbs, SInt32 yAbs); + virtual void mouseRelativeMove(SInt32 xRel, SInt32 yRel); + virtual void mouseWheel(SInt32 xDelta, SInt32 yDelta); + virtual void screensaver(bool activate); + virtual void resetOptions(); + virtual void setOptions(const OptionsList& options); + virtual void sendDragInfo(UInt32 fileCount, const char* info, size_t size); + virtual void fileChunkSending(UInt8 mark, char* data, size_t dataSize); + + virtual barrier::IStream* + getStream() const { return NULL; } + bool isPrimary() const { return true; } +private: + barrier::Screen* m_screen; + bool m_clipboardDirty[kClipboardEnd]; + SInt32 m_fakeInputCount; +}; diff --git a/src/lib/server/Server.cpp b/src/lib/server/Server.cpp new file mode 100644 index 0000000..32153a6 --- /dev/null +++ b/src/lib/server/Server.cpp @@ -0,0 +1,2405 @@ +/* + * barrier -- mouse and keyboard sharing utility + * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2002 Chris Schoeneman + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "server/Server.h" + +#include "server/ClientProxy.h" +#include "server/ClientProxyUnknown.h" +#include "server/PrimaryClient.h" +#include "server/ClientListener.h" +#include "barrier/FileChunk.h" +#include "barrier/IPlatformScreen.h" +#include "barrier/DropHelper.h" +#include "barrier/option_types.h" +#include "barrier/protocol_types.h" +#include "barrier/XScreen.h" +#include "barrier/XBarrier.h" +#include "barrier/StreamChunker.h" +#include "barrier/KeyState.h" +#include "barrier/Screen.h" +#include "barrier/PacketStreamFilter.h" +#include "net/TCPSocket.h" +#include "net/IDataSocket.h" +#include "net/IListenSocket.h" +#include "net/XSocket.h" +#include "mt/Thread.h" +#include "arch/Arch.h" +#include "base/TMethodJob.h" +#include "base/IEventQueue.h" +#include "base/Log.h" +#include "base/TMethodEventJob.h" +#include "common/stdexcept.h" + +#include <cstring> +#include <cstdlib> +#include <sstream> +#include <fstream> +#include <ctime> + +// +// Server +// + +Server::Server( + Config& config, + PrimaryClient* primaryClient, + barrier::Screen* screen, + IEventQueue* events, + ServerArgs const& args) : + m_mock(false), + m_primaryClient(primaryClient), + m_active(primaryClient), + m_seqNum(0), + m_xDelta(0), + m_yDelta(0), + m_xDelta2(0), + m_yDelta2(0), + m_config(&config), + m_inputFilter(config.getInputFilter()), + m_activeSaver(NULL), + m_switchDir(kNoDirection), + m_switchScreen(NULL), + m_switchWaitDelay(0.0), + m_switchWaitTimer(NULL), + m_switchTwoTapDelay(0.0), + m_switchTwoTapEngaged(false), + m_switchTwoTapArmed(false), + m_switchTwoTapZone(3), + m_switchNeedsShift(false), + m_switchNeedsControl(false), + m_switchNeedsAlt(false), + m_relativeMoves(false), + m_keyboardBroadcasting(false), + m_lockedToScreen(false), + m_screen(screen), + m_events(events), + m_sendFileThread(NULL), + m_writeToDropDirThread(NULL), + m_ignoreFileTransfer(false), + m_enableClipboard(true), + m_sendDragInfoThread(NULL), + m_waitDragInfoThread(true), + m_args(args) +{ + // must have a primary client and it must have a canonical name + assert(m_primaryClient != NULL); + assert(config.isScreen(primaryClient->getName())); + assert(m_screen != NULL); + + String primaryName = getName(primaryClient); + + // clear clipboards + for (ClipboardID id = 0; id < kClipboardEnd; ++id) { + ClipboardInfo& clipboard = m_clipboards[id]; + clipboard.m_clipboardOwner = primaryName; + clipboard.m_clipboardSeqNum = m_seqNum; + if (clipboard.m_clipboard.open(0)) { + clipboard.m_clipboard.empty(); + clipboard.m_clipboard.close(); + } + clipboard.m_clipboardData = clipboard.m_clipboard.marshall(); + } + + // install event handlers + m_events->adoptHandler(Event::kTimer, this, + new TMethodEventJob<Server>(this, + &Server::handleSwitchWaitTimeout)); + m_events->adoptHandler(m_events->forIKeyState().keyDown(), + m_inputFilter, + new TMethodEventJob<Server>(this, + &Server::handleKeyDownEvent)); + m_events->adoptHandler(m_events->forIKeyState().keyUp(), + m_inputFilter, + new TMethodEventJob<Server>(this, + &Server::handleKeyUpEvent)); + m_events->adoptHandler(m_events->forIKeyState().keyRepeat(), + m_inputFilter, + new TMethodEventJob<Server>(this, + &Server::handleKeyRepeatEvent)); + m_events->adoptHandler(m_events->forIPrimaryScreen().buttonDown(), + m_inputFilter, + new TMethodEventJob<Server>(this, + &Server::handleButtonDownEvent)); + m_events->adoptHandler(m_events->forIPrimaryScreen().buttonUp(), + m_inputFilter, + new TMethodEventJob<Server>(this, + &Server::handleButtonUpEvent)); + m_events->adoptHandler(m_events->forIPrimaryScreen().motionOnPrimary(), + m_primaryClient->getEventTarget(), + new TMethodEventJob<Server>(this, + &Server::handleMotionPrimaryEvent)); + m_events->adoptHandler(m_events->forIPrimaryScreen().motionOnSecondary(), + m_primaryClient->getEventTarget(), + new TMethodEventJob<Server>(this, + &Server::handleMotionSecondaryEvent)); + m_events->adoptHandler(m_events->forIPrimaryScreen().wheel(), + m_primaryClient->getEventTarget(), + new TMethodEventJob<Server>(this, + &Server::handleWheelEvent)); + m_events->adoptHandler(m_events->forIPrimaryScreen().screensaverActivated(), + m_primaryClient->getEventTarget(), + new TMethodEventJob<Server>(this, + &Server::handleScreensaverActivatedEvent)); + m_events->adoptHandler(m_events->forIPrimaryScreen().screensaverDeactivated(), + m_primaryClient->getEventTarget(), + new TMethodEventJob<Server>(this, + &Server::handleScreensaverDeactivatedEvent)); + m_events->adoptHandler(m_events->forServer().switchToScreen(), + m_inputFilter, + new TMethodEventJob<Server>(this, + &Server::handleSwitchToScreenEvent)); + m_events->adoptHandler(m_events->forServer().switchInDirection(), + m_inputFilter, + new TMethodEventJob<Server>(this, + &Server::handleSwitchInDirectionEvent)); + m_events->adoptHandler(m_events->forServer().keyboardBroadcast(), + m_inputFilter, + new TMethodEventJob<Server>(this, + &Server::handleKeyboardBroadcastEvent)); + m_events->adoptHandler(m_events->forServer().lockCursorToScreen(), + m_inputFilter, + new TMethodEventJob<Server>(this, + &Server::handleLockCursorToScreenEvent)); + m_events->adoptHandler(m_events->forIPrimaryScreen().fakeInputBegin(), + m_inputFilter, + new TMethodEventJob<Server>(this, + &Server::handleFakeInputBeginEvent)); + m_events->adoptHandler(m_events->forIPrimaryScreen().fakeInputEnd(), + m_inputFilter, + new TMethodEventJob<Server>(this, + &Server::handleFakeInputEndEvent)); + + if (m_args.m_enableDragDrop) { + m_events->adoptHandler(m_events->forFile().fileChunkSending(), + this, + new TMethodEventJob<Server>(this, + &Server::handleFileChunkSendingEvent)); + m_events->adoptHandler(m_events->forFile().fileRecieveCompleted(), + this, + new TMethodEventJob<Server>(this, + &Server::handleFileRecieveCompletedEvent)); + } + + // add connection + addClient(m_primaryClient); + + // set initial configuration + setConfig(config); + + // enable primary client + m_primaryClient->enable(); + m_inputFilter->setPrimaryClient(m_primaryClient); + + // Determine if scroll lock is already set. If so, lock the cursor to the primary screen + if (m_primaryClient->getToggleMask() & KeyModifierScrollLock) { + LOG((CLOG_NOTE "Scroll Lock is on, locking cursor to screen")); + m_lockedToScreen = true; + } + +} + +Server::~Server() +{ + if (m_mock) { + return; + } + + // remove event handlers and timers + m_events->removeHandler(m_events->forIKeyState().keyDown(), + m_inputFilter); + m_events->removeHandler(m_events->forIKeyState().keyUp(), + m_inputFilter); + m_events->removeHandler(m_events->forIKeyState().keyRepeat(), + m_inputFilter); + m_events->removeHandler(m_events->forIPrimaryScreen().buttonDown(), + m_inputFilter); + m_events->removeHandler(m_events->forIPrimaryScreen().buttonUp(), + m_inputFilter); + m_events->removeHandler(m_events->forIPrimaryScreen().motionOnPrimary(), + m_primaryClient->getEventTarget()); + m_events->removeHandler(m_events->forIPrimaryScreen().motionOnSecondary(), + m_primaryClient->getEventTarget()); + m_events->removeHandler(m_events->forIPrimaryScreen().wheel(), + m_primaryClient->getEventTarget()); + m_events->removeHandler(m_events->forIPrimaryScreen().screensaverActivated(), + m_primaryClient->getEventTarget()); + m_events->removeHandler(m_events->forIPrimaryScreen().screensaverDeactivated(), + m_primaryClient->getEventTarget()); + m_events->removeHandler(m_events->forIPrimaryScreen().fakeInputBegin(), + m_inputFilter); + m_events->removeHandler(m_events->forIPrimaryScreen().fakeInputEnd(), + m_inputFilter); + m_events->removeHandler(Event::kTimer, this); + stopSwitch(); + + // force immediate disconnection of secondary clients + disconnect(); + for (OldClients::iterator index = m_oldClients.begin(); + index != m_oldClients.end(); ++index) { + BaseClientProxy* client = index->first; + m_events->deleteTimer(index->second); + m_events->removeHandler(Event::kTimer, client); + m_events->removeHandler(m_events->forClientProxy().disconnected(), client); + delete client; + } + + // remove input filter + m_inputFilter->setPrimaryClient(NULL); + + // disable and disconnect primary client + m_primaryClient->disable(); + removeClient(m_primaryClient); +} + +bool +Server::setConfig(const Config& config) +{ + // refuse configuration if it doesn't include the primary screen + if (!config.isScreen(m_primaryClient->getName())) { + return false; + } + + // close clients that are connected but being dropped from the + // configuration. + closeClients(config); + + // cut over + processOptions(); + + // add ScrollLock as a hotkey to lock to the screen. this was a + // built-in feature in earlier releases and is now supported via + // the user configurable hotkey mechanism. if the user has already + // registered ScrollLock for something else then that will win but + // we will unfortunately generate a warning. if the user has + // configured a LockCursorToScreenAction then we don't add + // ScrollLock as a hotkey. + if (!m_config->hasLockToScreenAction()) { + IPlatformScreen::KeyInfo* key = + IPlatformScreen::KeyInfo::alloc(kKeyScrollLock, 0, 0, 0); + InputFilter::Rule rule(new InputFilter::KeystrokeCondition(m_events, key)); + rule.adoptAction(new InputFilter::LockCursorToScreenAction(m_events), true); + m_inputFilter->addFilterRule(rule); + } + + // tell primary screen about reconfiguration + m_primaryClient->reconfigure(getActivePrimarySides()); + + // tell all (connected) clients about current options + for (ClientList::const_iterator index = m_clients.begin(); + index != m_clients.end(); ++index) { + BaseClientProxy* client = index->second; + sendOptions(client); + } + + return true; +} + +void +Server::adoptClient(BaseClientProxy* client) +{ + assert(client != NULL); + + // watch for client disconnection + m_events->adoptHandler(m_events->forClientProxy().disconnected(), client, + new TMethodEventJob<Server>(this, + &Server::handleClientDisconnected, client)); + + // name must be in our configuration + if (!m_config->isScreen(client->getName())) { + LOG((CLOG_WARN "unrecognised client name \"%s\", check server config", client->getName().c_str())); + closeClient(client, kMsgEUnknown); + return; + } + + // add client to client list + if (!addClient(client)) { + // can only have one screen with a given name at any given time + LOG((CLOG_WARN "a client with name \"%s\" is already connected", getName(client).c_str())); + closeClient(client, kMsgEBusy); + return; + } + LOG((CLOG_NOTE "client \"%s\" has connected", getName(client).c_str())); + + // send configuration options to client + sendOptions(client); + + // activate screen saver on new client if active on the primary screen + if (m_activeSaver != NULL) { + client->screensaver(true); + } + + // send notification + Server::ScreenConnectedInfo* info = + new Server::ScreenConnectedInfo(getName(client)); + m_events->addEvent(Event(m_events->forServer().connected(), + m_primaryClient->getEventTarget(), info)); +} + +void +Server::disconnect() +{ + // close all secondary clients + if (m_clients.size() > 1 || !m_oldClients.empty()) { + Config emptyConfig(m_events); + closeClients(emptyConfig); + } + else { + m_events->addEvent(Event(m_events->forServer().disconnected(), this)); + } +} + +UInt32 +Server::getNumClients() const +{ + return (SInt32)m_clients.size(); +} + +void +Server::getClients(std::vector<String>& list) const +{ + list.clear(); + for (ClientList::const_iterator index = m_clients.begin(); + index != m_clients.end(); ++index) { + list.push_back(index->first); + } +} + +String +Server::getName(const BaseClientProxy* client) const +{ + String name = m_config->getCanonicalName(client->getName()); + if (name.empty()) { + name = client->getName(); + } + return name; +} + +UInt32 +Server::getActivePrimarySides() const +{ + UInt32 sides = 0; + if (!isLockedToScreenServer()) { + if (hasAnyNeighbor(m_primaryClient, kLeft)) { + sides |= kLeftMask; + } + if (hasAnyNeighbor(m_primaryClient, kRight)) { + sides |= kRightMask; + } + if (hasAnyNeighbor(m_primaryClient, kTop)) { + sides |= kTopMask; + } + if (hasAnyNeighbor(m_primaryClient, kBottom)) { + sides |= kBottomMask; + } + } + return sides; +} + +bool +Server::isLockedToScreenServer() const +{ + // locked if scroll-lock is toggled on + return m_lockedToScreen; +} + +bool +Server::isLockedToScreen() const +{ + // locked if we say we're locked + if (isLockedToScreenServer()) { + return true; + } + + // locked if primary says we're locked + if (m_primaryClient->isLockedToScreen()) { + return true; + } + + // not locked + return false; +} + +SInt32 +Server::getJumpZoneSize(BaseClientProxy* client) const +{ + if (client == m_primaryClient) { + return m_primaryClient->getJumpZoneSize(); + } + else { + return 0; + } +} + +void +Server::switchScreen(BaseClientProxy* dst, + SInt32 x, SInt32 y, bool forScreensaver) +{ + assert(dst != NULL); + +#ifndef NDEBUG + { + SInt32 dx, dy, dw, dh; + dst->getShape(dx, dy, dw, dh); + assert(x >= dx && y >= dy && x < dx + dw && y < dy + dh); + } +#endif + assert(m_active != NULL); + + LOG((CLOG_INFO "switch from \"%s\" to \"%s\" at %d,%d", getName(m_active).c_str(), getName(dst).c_str(), x, y)); + + // stop waiting to switch + stopSwitch(); + + // record new position + m_x = x; + m_y = y; + m_xDelta = 0; + m_yDelta = 0; + m_xDelta2 = 0; + m_yDelta2 = 0; + + // wrapping means leaving the active screen and entering it again. + // since that's a waste of time we skip that and just warp the + // mouse. + if (m_active != dst) { + // leave active screen + if (!m_active->leave()) { + // cannot leave screen + LOG((CLOG_WARN "can't leave screen")); + return; + } + + // update the primary client's clipboards if we're leaving the + // primary screen. + if (m_active == m_primaryClient && m_enableClipboard) { + for (ClipboardID id = 0; id < kClipboardEnd; ++id) { + ClipboardInfo& clipboard = m_clipboards[id]; + if (clipboard.m_clipboardOwner == getName(m_primaryClient)) { + onClipboardChanged(m_primaryClient, + id, clipboard.m_clipboardSeqNum); + } + } + } + + // cut over + m_active = dst; + + // increment enter sequence number + ++m_seqNum; + + // enter new screen + m_active->enter(x, y, m_seqNum, + m_primaryClient->getToggleMask(), + forScreensaver); + + if (m_enableClipboard) { + // send the clipboard data to new active screen + for (ClipboardID id = 0; id < kClipboardEnd; ++id) { + m_active->setClipboard(id, &m_clipboards[id].m_clipboard); + } + } + + Server::SwitchToScreenInfo* info = + Server::SwitchToScreenInfo::alloc(m_active->getName()); + m_events->addEvent(Event(m_events->forServer().screenSwitched(), this, info)); + } + else { + m_active->mouseMove(x, y); + } +} + +void +Server::jumpToScreen(BaseClientProxy* newScreen) +{ + assert(newScreen != NULL); + + // record the current cursor position on the active screen + m_active->setJumpCursorPos(m_x, m_y); + + // get the last cursor position on the target screen + SInt32 x, y; + newScreen->getJumpCursorPos(x, y); + + switchScreen(newScreen, x, y, false); +} + +float +Server::mapToFraction(BaseClientProxy* client, + EDirection dir, SInt32 x, SInt32 y) const +{ + SInt32 sx, sy, sw, sh; + client->getShape(sx, sy, sw, sh); + switch (dir) { + case kLeft: + case kRight: + return static_cast<float>(y - sy + 0.5f) / static_cast<float>(sh); + + case kTop: + case kBottom: + return static_cast<float>(x - sx + 0.5f) / static_cast<float>(sw); + + case kNoDirection: + assert(0 && "bad direction"); + break; + } + return 0.0f; +} + +void +Server::mapToPixel(BaseClientProxy* client, + EDirection dir, float f, SInt32& x, SInt32& y) const +{ + SInt32 sx, sy, sw, sh; + client->getShape(sx, sy, sw, sh); + switch (dir) { + case kLeft: + case kRight: + y = static_cast<SInt32>(f * sh) + sy; + break; + + case kTop: + case kBottom: + x = static_cast<SInt32>(f * sw) + sx; + break; + + case kNoDirection: + assert(0 && "bad direction"); + break; + } +} + +bool +Server::hasAnyNeighbor(BaseClientProxy* client, EDirection dir) const +{ + assert(client != NULL); + + return m_config->hasNeighbor(getName(client), dir); +} + +BaseClientProxy* +Server::getNeighbor(BaseClientProxy* src, + EDirection dir, SInt32& x, SInt32& y) const +{ + // note -- must be locked on entry + + assert(src != NULL); + + // get source screen name + String srcName = getName(src); + assert(!srcName.empty()); + LOG((CLOG_DEBUG2 "find neighbor on %s of \"%s\"", Config::dirName(dir), srcName.c_str())); + + // convert position to fraction + float t = mapToFraction(src, dir, x, y); + + // search for the closest neighbor that exists in direction dir + float tTmp; + for (;;) { + String dstName(m_config->getNeighbor(srcName, dir, t, &tTmp)); + + // if nothing in that direction then return NULL. if the + // destination is the source then we can make no more + // progress in this direction. since we haven't found a + // connected neighbor we return NULL. + if (dstName.empty()) { + LOG((CLOG_DEBUG2 "no neighbor on %s of \"%s\"", Config::dirName(dir), srcName.c_str())); + return NULL; + } + + // look up neighbor cell. if the screen is connected and + // ready then we can stop. + ClientList::const_iterator index = m_clients.find(dstName); + if (index != m_clients.end()) { + LOG((CLOG_DEBUG2 "\"%s\" is on %s of \"%s\" at %f", dstName.c_str(), Config::dirName(dir), srcName.c_str(), t)); + mapToPixel(index->second, dir, tTmp, x, y); + return index->second; + } + + // skip over unconnected screen + LOG((CLOG_DEBUG2 "ignored \"%s\" on %s of \"%s\"", dstName.c_str(), Config::dirName(dir), srcName.c_str())); + srcName = dstName; + + // use position on skipped screen + t = tTmp; + } +} + +BaseClientProxy* +Server::mapToNeighbor(BaseClientProxy* src, + EDirection srcSide, SInt32& x, SInt32& y) const +{ + // note -- must be locked on entry + + assert(src != NULL); + + // get the first neighbor + BaseClientProxy* dst = getNeighbor(src, srcSide, x, y); + if (dst == NULL) { + return NULL; + } + + // get the source screen's size + SInt32 dx, dy, dw, dh; + BaseClientProxy* lastGoodScreen = src; + lastGoodScreen->getShape(dx, dy, dw, dh); + + // find destination screen, adjusting x or y (but not both). the + // searches are done in a sort of canonical screen space where + // the upper-left corner is 0,0 for each screen. we adjust from + // actual to canonical position on entry to and from canonical to + // actual on exit from the search. + switch (srcSide) { + case kLeft: + x -= dx; + while (dst != NULL) { + lastGoodScreen = dst; + lastGoodScreen->getShape(dx, dy, dw, dh); + x += dw; + if (x >= 0) { + break; + } + LOG((CLOG_DEBUG2 "skipping over screen %s", getName(dst).c_str())); + dst = getNeighbor(lastGoodScreen, srcSide, x, y); + } + assert(lastGoodScreen != NULL); + x += dx; + break; + + case kRight: + x -= dx; + while (dst != NULL) { + x -= dw; + lastGoodScreen = dst; + lastGoodScreen->getShape(dx, dy, dw, dh); + if (x < dw) { + break; + } + LOG((CLOG_DEBUG2 "skipping over screen %s", getName(dst).c_str())); + dst = getNeighbor(lastGoodScreen, srcSide, x, y); + } + assert(lastGoodScreen != NULL); + x += dx; + break; + + case kTop: + y -= dy; + while (dst != NULL) { + lastGoodScreen = dst; + lastGoodScreen->getShape(dx, dy, dw, dh); + y += dh; + if (y >= 0) { + break; + } + LOG((CLOG_DEBUG2 "skipping over screen %s", getName(dst).c_str())); + dst = getNeighbor(lastGoodScreen, srcSide, x, y); + } + assert(lastGoodScreen != NULL); + y += dy; + break; + + case kBottom: + y -= dy; + while (dst != NULL) { + y -= dh; + lastGoodScreen = dst; + lastGoodScreen->getShape(dx, dy, dw, dh); + if (y < dh) { + break; + } + LOG((CLOG_DEBUG2 "skipping over screen %s", getName(dst).c_str())); + dst = getNeighbor(lastGoodScreen, srcSide, x, y); + } + assert(lastGoodScreen != NULL); + y += dy; + break; + + case kNoDirection: + assert(0 && "bad direction"); + return NULL; + } + + // save destination screen + assert(lastGoodScreen != NULL); + dst = lastGoodScreen; + + // if entering primary screen then be sure to move in far enough + // to avoid the jump zone. if entering a side that doesn't have + // a neighbor (i.e. an asymmetrical side) then we don't need to + // move inwards because that side can't provoke a jump. + avoidJumpZone(dst, srcSide, x, y); + + return dst; +} + +void +Server::avoidJumpZone(BaseClientProxy* dst, + EDirection dir, SInt32& x, SInt32& y) const +{ + // we only need to avoid jump zones on the primary screen + if (dst != m_primaryClient) { + return; + } + + const String dstName(getName(dst)); + SInt32 dx, dy, dw, dh; + dst->getShape(dx, dy, dw, dh); + float t = mapToFraction(dst, dir, x, y); + SInt32 z = getJumpZoneSize(dst); + + // move in far enough to avoid the jump zone. if entering a side + // that doesn't have a neighbor (i.e. an asymmetrical side) then we + // don't need to move inwards because that side can't provoke a jump. + switch (dir) { + case kLeft: + if (!m_config->getNeighbor(dstName, kRight, t, NULL).empty() && + x > dx + dw - 1 - z) + x = dx + dw - 1 - z; + break; + + case kRight: + if (!m_config->getNeighbor(dstName, kLeft, t, NULL).empty() && + x < dx + z) + x = dx + z; + break; + + case kTop: + if (!m_config->getNeighbor(dstName, kBottom, t, NULL).empty() && + y > dy + dh - 1 - z) + y = dy + dh - 1 - z; + break; + + case kBottom: + if (!m_config->getNeighbor(dstName, kTop, t, NULL).empty() && + y < dy + z) + y = dy + z; + break; + + case kNoDirection: + assert(0 && "bad direction"); + } +} + +bool +Server::isSwitchOkay(BaseClientProxy* newScreen, + EDirection dir, SInt32 x, SInt32 y, + SInt32 xActive, SInt32 yActive) +{ + LOG((CLOG_DEBUG1 "try to leave \"%s\" on %s", getName(m_active).c_str(), Config::dirName(dir))); + + // is there a neighbor? + if (newScreen == NULL) { + // there's no neighbor. we don't want to switch and we don't + // want to try to switch later. + LOG((CLOG_DEBUG1 "no neighbor %s", Config::dirName(dir))); + stopSwitch(); + return false; + } + + // should we switch or not? + bool preventSwitch = false; + bool allowSwitch = false; + + // note if the switch direction has changed. save the new + // direction and screen if so. + bool isNewDirection = (dir != m_switchDir); + if (isNewDirection || m_switchScreen == NULL) { + m_switchDir = dir; + m_switchScreen = newScreen; + } + + // is this a double tap and do we care? + if (!allowSwitch && m_switchTwoTapDelay > 0.0) { + if (isNewDirection || + !isSwitchTwoTapStarted() || !shouldSwitchTwoTap()) { + // tapping a different or new edge or second tap not + // fast enough. prepare for second tap. + preventSwitch = true; + startSwitchTwoTap(); + } + else { + // got second tap + allowSwitch = true; + } + } + + // if waiting before a switch then prepare to switch later + if (!allowSwitch && m_switchWaitDelay > 0.0) { + if (isNewDirection || !isSwitchWaitStarted()) { + startSwitchWait(x, y); + } + preventSwitch = true; + } + + // are we in a locked corner? first check if screen has the option set + // and, if not, check the global options. + const Config::ScreenOptions* options = + m_config->getOptions(getName(m_active)); + if (options == NULL || options->count(kOptionScreenSwitchCorners) == 0) { + options = m_config->getOptions(""); + } + if (options != NULL && options->count(kOptionScreenSwitchCorners) > 0) { + // get corner mask and size + Config::ScreenOptions::const_iterator i = + options->find(kOptionScreenSwitchCorners); + UInt32 corners = static_cast<UInt32>(i->second); + i = options->find(kOptionScreenSwitchCornerSize); + SInt32 size = 0; + if (i != options->end()) { + size = i->second; + } + + // see if we're in a locked corner + if ((getCorner(m_active, xActive, yActive, size) & corners) != 0) { + // yep, no switching + LOG((CLOG_DEBUG1 "locked in corner")); + preventSwitch = true; + stopSwitch(); + } + } + + // ignore if mouse is locked to screen and don't try to switch later + if (!preventSwitch && isLockedToScreen()) { + LOG((CLOG_DEBUG1 "locked to screen")); + preventSwitch = true; + stopSwitch(); + } + + // check for optional needed modifiers + KeyModifierMask mods = this->m_primaryClient->getToggleMask(); + + if (!preventSwitch && ( + (this->m_switchNeedsShift && ((mods & KeyModifierShift) != KeyModifierShift)) || + (this->m_switchNeedsControl && ((mods & KeyModifierControl) != KeyModifierControl)) || + (this->m_switchNeedsAlt && ((mods & KeyModifierAlt) != KeyModifierAlt)) + )) { + LOG((CLOG_DEBUG1 "need modifiers to switch")); + preventSwitch = true; + stopSwitch(); + } + + return !preventSwitch; +} + +void +Server::noSwitch(SInt32 x, SInt32 y) +{ + armSwitchTwoTap(x, y); + stopSwitchWait(); +} + +void +Server::stopSwitch() +{ + if (m_switchScreen != NULL) { + m_switchScreen = NULL; + m_switchDir = kNoDirection; + stopSwitchTwoTap(); + stopSwitchWait(); + } +} + +void +Server::startSwitchTwoTap() +{ + m_switchTwoTapEngaged = true; + m_switchTwoTapArmed = false; + m_switchTwoTapTimer.reset(); + LOG((CLOG_DEBUG1 "waiting for second tap")); +} + +void +Server::armSwitchTwoTap(SInt32 x, SInt32 y) +{ + if (m_switchTwoTapEngaged) { + if (m_switchTwoTapTimer.getTime() > m_switchTwoTapDelay) { + // second tap took too long. disengage. + stopSwitchTwoTap(); + } + else if (!m_switchTwoTapArmed) { + // still time for a double tap. see if we left the tap + // zone and, if so, arm the two tap. + SInt32 ax, ay, aw, ah; + m_active->getShape(ax, ay, aw, ah); + SInt32 tapZone = m_primaryClient->getJumpZoneSize(); + if (tapZone < m_switchTwoTapZone) { + tapZone = m_switchTwoTapZone; + } + if (x >= ax + tapZone && x < ax + aw - tapZone && + y >= ay + tapZone && y < ay + ah - tapZone) { + // win32 can generate bogus mouse events that appear to + // move in the opposite direction that the mouse actually + // moved. try to ignore that crap here. + switch (m_switchDir) { + case kLeft: + m_switchTwoTapArmed = (m_xDelta > 0 && m_xDelta2 > 0); + break; + + case kRight: + m_switchTwoTapArmed = (m_xDelta < 0 && m_xDelta2 < 0); + break; + + case kTop: + m_switchTwoTapArmed = (m_yDelta > 0 && m_yDelta2 > 0); + break; + + case kBottom: + m_switchTwoTapArmed = (m_yDelta < 0 && m_yDelta2 < 0); + break; + + default: + break; + } + } + } + } +} + +void +Server::stopSwitchTwoTap() +{ + m_switchTwoTapEngaged = false; + m_switchTwoTapArmed = false; +} + +bool +Server::isSwitchTwoTapStarted() const +{ + return m_switchTwoTapEngaged; +} + +bool +Server::shouldSwitchTwoTap() const +{ + // this is the second tap if two-tap is armed and this tap + // came fast enough + return (m_switchTwoTapArmed && + m_switchTwoTapTimer.getTime() <= m_switchTwoTapDelay); +} + +void +Server::startSwitchWait(SInt32 x, SInt32 y) +{ + stopSwitchWait(); + m_switchWaitX = x; + m_switchWaitY = y; + m_switchWaitTimer = m_events->newOneShotTimer(m_switchWaitDelay, this); + LOG((CLOG_DEBUG1 "waiting to switch")); +} + +void +Server::stopSwitchWait() +{ + if (m_switchWaitTimer != NULL) { + m_events->deleteTimer(m_switchWaitTimer); + m_switchWaitTimer = NULL; + } +} + +bool +Server::isSwitchWaitStarted() const +{ + return (m_switchWaitTimer != NULL); +} + +UInt32 +Server::getCorner(BaseClientProxy* client, + SInt32 x, SInt32 y, SInt32 size) const +{ + assert(client != NULL); + + // get client screen shape + SInt32 ax, ay, aw, ah; + client->getShape(ax, ay, aw, ah); + + // check for x,y on the left or right + SInt32 xSide; + if (x <= ax) { + xSide = -1; + } + else if (x >= ax + aw - 1) { + xSide = 1; + } + else { + xSide = 0; + } + + // check for x,y on the top or bottom + SInt32 ySide; + if (y <= ay) { + ySide = -1; + } + else if (y >= ay + ah - 1) { + ySide = 1; + } + else { + ySide = 0; + } + + // if against the left or right then check if y is within size + if (xSide != 0) { + if (y < ay + size) { + return (xSide < 0) ? kTopLeftMask : kTopRightMask; + } + else if (y >= ay + ah - size) { + return (xSide < 0) ? kBottomLeftMask : kBottomRightMask; + } + } + + // if against the left or right then check if y is within size + if (ySide != 0) { + if (x < ax + size) { + return (ySide < 0) ? kTopLeftMask : kBottomLeftMask; + } + else if (x >= ax + aw - size) { + return (ySide < 0) ? kTopRightMask : kBottomRightMask; + } + } + + return kNoCornerMask; +} + +void +Server::stopRelativeMoves() +{ + if (m_relativeMoves && m_active != m_primaryClient) { + // warp to the center of the active client so we know where we are + SInt32 ax, ay, aw, ah; + m_active->getShape(ax, ay, aw, ah); + m_x = ax + (aw >> 1); + m_y = ay + (ah >> 1); + m_xDelta = 0; + m_yDelta = 0; + m_xDelta2 = 0; + m_yDelta2 = 0; + LOG((CLOG_DEBUG2 "synchronize move on %s by %d,%d", getName(m_active).c_str(), m_x, m_y)); + m_active->mouseMove(m_x, m_y); + } +} + +void +Server::sendOptions(BaseClientProxy* client) const +{ + OptionsList optionsList; + + // look up options for client + const Config::ScreenOptions* options = + m_config->getOptions(getName(client)); + if (options != NULL) { + // convert options to a more convenient form for sending + optionsList.reserve(2 * options->size()); + for (Config::ScreenOptions::const_iterator index = options->begin(); + index != options->end(); ++index) { + optionsList.push_back(index->first); + optionsList.push_back(static_cast<UInt32>(index->second)); + } + } + + // look up global options + options = m_config->getOptions(""); + if (options != NULL) { + // convert options to a more convenient form for sending + optionsList.reserve(optionsList.size() + 2 * options->size()); + for (Config::ScreenOptions::const_iterator index = options->begin(); + index != options->end(); ++index) { + optionsList.push_back(index->first); + optionsList.push_back(static_cast<UInt32>(index->second)); + } + } + + // send the options + client->resetOptions(); + client->setOptions(optionsList); +} + +void +Server::processOptions() +{ + const Config::ScreenOptions* options = m_config->getOptions(""); + if (options == NULL) { + return; + } + + m_switchNeedsShift = false; // it seems if i don't add these + m_switchNeedsControl = false; // lines, the 'reload config' option + m_switchNeedsAlt = false; // doesnt' work correct. + + bool newRelativeMoves = m_relativeMoves; + for (Config::ScreenOptions::const_iterator index = options->begin(); + index != options->end(); ++index) { + const OptionID id = index->first; + const OptionValue value = index->second; + if (id == kOptionScreenSwitchDelay) { + m_switchWaitDelay = 1.0e-3 * static_cast<double>(value); + if (m_switchWaitDelay < 0.0) { + m_switchWaitDelay = 0.0; + } + stopSwitchWait(); + } + else if (id == kOptionScreenSwitchTwoTap) { + m_switchTwoTapDelay = 1.0e-3 * static_cast<double>(value); + if (m_switchTwoTapDelay < 0.0) { + m_switchTwoTapDelay = 0.0; + } + stopSwitchTwoTap(); + } + else if (id == kOptionScreenSwitchNeedsControl) { + m_switchNeedsControl = (value != 0); + } + else if (id == kOptionScreenSwitchNeedsShift) { + m_switchNeedsShift = (value != 0); + } + else if (id == kOptionScreenSwitchNeedsAlt) { + m_switchNeedsAlt = (value != 0); + } + else if (id == kOptionRelativeMouseMoves) { + newRelativeMoves = (value != 0); + } + else if (id == kOptionClipboardSharing) { + m_enableClipboard = (value != 0); + + if (m_enableClipboard == false) { + LOG((CLOG_NOTE "clipboard sharing is disabled")); + } + } + } + if (m_relativeMoves && !newRelativeMoves) { + stopRelativeMoves(); + } + m_relativeMoves = newRelativeMoves; +} + +void +Server::handleShapeChanged(const Event&, void* vclient) +{ + // ignore events from unknown clients + BaseClientProxy* client = static_cast<BaseClientProxy*>(vclient); + if (m_clientSet.count(client) == 0) { + return; + } + + LOG((CLOG_DEBUG "screen \"%s\" shape changed", getName(client).c_str())); + + // update jump coordinate + SInt32 x, y; + client->getCursorPos(x, y); + client->setJumpCursorPos(x, y); + + // update the mouse coordinates + if (client == m_active) { + m_x = x; + m_y = y; + } + + // handle resolution change to primary screen + if (client == m_primaryClient) { + if (client == m_active) { + onMouseMovePrimary(m_x, m_y); + } + else { + onMouseMoveSecondary(0, 0); + } + } +} + +void +Server::handleClipboardGrabbed(const Event& event, void* vclient) +{ + if (!m_enableClipboard) { + return; + } + + // ignore events from unknown clients + BaseClientProxy* grabber = static_cast<BaseClientProxy*>(vclient); + if (m_clientSet.count(grabber) == 0) { + return; + } + const IScreen::ClipboardInfo* info = + static_cast<const IScreen::ClipboardInfo*>(event.getData()); + + // ignore grab if sequence number is old. always allow primary + // screen to grab. + ClipboardInfo& clipboard = m_clipboards[info->m_id]; + if (grabber != m_primaryClient && + info->m_sequenceNumber < clipboard.m_clipboardSeqNum) { + LOG((CLOG_INFO "ignored screen \"%s\" grab of clipboard %d", getName(grabber).c_str(), info->m_id)); + return; + } + + // mark screen as owning clipboard + LOG((CLOG_INFO "screen \"%s\" grabbed clipboard %d from \"%s\"", getName(grabber).c_str(), info->m_id, clipboard.m_clipboardOwner.c_str())); + clipboard.m_clipboardOwner = getName(grabber); + clipboard.m_clipboardSeqNum = info->m_sequenceNumber; + + // clear the clipboard data (since it's not known at this point) + if (clipboard.m_clipboard.open(0)) { + clipboard.m_clipboard.empty(); + clipboard.m_clipboard.close(); + } + clipboard.m_clipboardData = clipboard.m_clipboard.marshall(); + + // tell all other screens to take ownership of clipboard. tell the + // grabber that it's clipboard isn't dirty. + for (ClientList::iterator index = m_clients.begin(); + index != m_clients.end(); ++index) { + BaseClientProxy* client = index->second; + if (client == grabber) { + client->setClipboardDirty(info->m_id, false); + } + else { + client->grabClipboard(info->m_id); + } + } +} + +void +Server::handleClipboardChanged(const Event& event, void* vclient) +{ + // ignore events from unknown clients + BaseClientProxy* sender = static_cast<BaseClientProxy*>(vclient); + if (m_clientSet.count(sender) == 0) { + return; + } + const IScreen::ClipboardInfo* info = + static_cast<const IScreen::ClipboardInfo*>(event.getData()); + onClipboardChanged(sender, info->m_id, info->m_sequenceNumber); +} + +void +Server::handleKeyDownEvent(const Event& event, void*) +{ + IPlatformScreen::KeyInfo* info = + static_cast<IPlatformScreen::KeyInfo*>(event.getData()); + onKeyDown(info->m_key, info->m_mask, info->m_button, info->m_screens); +} + +void +Server::handleKeyUpEvent(const Event& event, void*) +{ + IPlatformScreen::KeyInfo* info = + static_cast<IPlatformScreen::KeyInfo*>(event.getData()); + onKeyUp(info->m_key, info->m_mask, info->m_button, info->m_screens); +} + +void +Server::handleKeyRepeatEvent(const Event& event, void*) +{ + IPlatformScreen::KeyInfo* info = + static_cast<IPlatformScreen::KeyInfo*>(event.getData()); + onKeyRepeat(info->m_key, info->m_mask, info->m_count, info->m_button); +} + +void +Server::handleButtonDownEvent(const Event& event, void*) +{ + IPlatformScreen::ButtonInfo* info = + static_cast<IPlatformScreen::ButtonInfo*>(event.getData()); + onMouseDown(info->m_button); +} + +void +Server::handleButtonUpEvent(const Event& event, void*) +{ + IPlatformScreen::ButtonInfo* info = + static_cast<IPlatformScreen::ButtonInfo*>(event.getData()); + onMouseUp(info->m_button); +} + +void +Server::handleMotionPrimaryEvent(const Event& event, void*) +{ + IPlatformScreen::MotionInfo* info = + static_cast<IPlatformScreen::MotionInfo*>(event.getData()); + onMouseMovePrimary(info->m_x, info->m_y); +} + +void +Server::handleMotionSecondaryEvent(const Event& event, void*) +{ + IPlatformScreen::MotionInfo* info = + static_cast<IPlatformScreen::MotionInfo*>(event.getData()); + onMouseMoveSecondary(info->m_x, info->m_y); +} + +void +Server::handleWheelEvent(const Event& event, void*) +{ + IPlatformScreen::WheelInfo* info = + static_cast<IPlatformScreen::WheelInfo*>(event.getData()); + onMouseWheel(info->m_xDelta, info->m_yDelta); +} + +void +Server::handleScreensaverActivatedEvent(const Event&, void*) +{ + onScreensaver(true); +} + +void +Server::handleScreensaverDeactivatedEvent(const Event&, void*) +{ + onScreensaver(false); +} + +void +Server::handleSwitchWaitTimeout(const Event&, void*) +{ + // ignore if mouse is locked to screen + if (isLockedToScreen()) { + LOG((CLOG_DEBUG1 "locked to screen")); + stopSwitch(); + return; + } + + // switch screen + switchScreen(m_switchScreen, m_switchWaitX, m_switchWaitY, false); +} + +void +Server::handleClientDisconnected(const Event&, void* vclient) +{ + // client has disconnected. it might be an old client or an + // active client. we don't care so just handle it both ways. + BaseClientProxy* client = static_cast<BaseClientProxy*>(vclient); + removeActiveClient(client); + removeOldClient(client); + + delete client; +} + +void +Server::handleClientCloseTimeout(const Event&, void* vclient) +{ + // client took too long to disconnect. just dump it. + BaseClientProxy* client = static_cast<BaseClientProxy*>(vclient); + LOG((CLOG_NOTE "forced disconnection of client \"%s\"", getName(client).c_str())); + removeOldClient(client); + + delete client; +} + +void +Server::handleSwitchToScreenEvent(const Event& event, void*) +{ + SwitchToScreenInfo* info = + static_cast<SwitchToScreenInfo*>(event.getData()); + + ClientList::const_iterator index = m_clients.find(info->m_screen); + if (index == m_clients.end()) { + LOG((CLOG_DEBUG1 "screen \"%s\" not active", info->m_screen)); + } + else { + jumpToScreen(index->second); + } +} + +void +Server::handleSwitchInDirectionEvent(const Event& event, void*) +{ + SwitchInDirectionInfo* info = + static_cast<SwitchInDirectionInfo*>(event.getData()); + + // jump to screen in chosen direction from center of this screen + SInt32 x = m_x, y = m_y; + BaseClientProxy* newScreen = + getNeighbor(m_active, info->m_direction, x, y); + if (newScreen == NULL) { + LOG((CLOG_DEBUG1 "no neighbor %s", Config::dirName(info->m_direction))); + } + else { + jumpToScreen(newScreen); + } +} + +void +Server::handleKeyboardBroadcastEvent(const Event& event, void*) +{ + KeyboardBroadcastInfo* info = (KeyboardBroadcastInfo*)event.getData(); + + // choose new state + bool newState; + switch (info->m_state) { + case KeyboardBroadcastInfo::kOff: + newState = false; + break; + + default: + case KeyboardBroadcastInfo::kOn: + newState = true; + break; + + case KeyboardBroadcastInfo::kToggle: + newState = !m_keyboardBroadcasting; + break; + } + + // enter new state + if (newState != m_keyboardBroadcasting || + info->m_screens != m_keyboardBroadcastingScreens) { + m_keyboardBroadcasting = newState; + m_keyboardBroadcastingScreens = info->m_screens; + LOG((CLOG_DEBUG "keyboard broadcasting %s: %s", m_keyboardBroadcasting ? "on" : "off", m_keyboardBroadcastingScreens.c_str())); + } +} + +void +Server::handleLockCursorToScreenEvent(const Event& event, void*) +{ + LockCursorToScreenInfo* info = (LockCursorToScreenInfo*)event.getData(); + + // choose new state + bool newState; + switch (info->m_state) { + case LockCursorToScreenInfo::kOff: + newState = false; + break; + + default: + case LockCursorToScreenInfo::kOn: + newState = true; + break; + + case LockCursorToScreenInfo::kToggle: + newState = !m_lockedToScreen; + break; + } + + // enter new state + if (newState != m_lockedToScreen) { + m_lockedToScreen = newState; + LOG((CLOG_NOTE "cursor %s current screen", m_lockedToScreen ? "locked to" : "unlocked from")); + + m_primaryClient->reconfigure(getActivePrimarySides()); + if (!isLockedToScreenServer()) { + stopRelativeMoves(); + } + } +} + +void +Server::handleFakeInputBeginEvent(const Event&, void*) +{ + m_primaryClient->fakeInputBegin(); +} + +void +Server::handleFakeInputEndEvent(const Event&, void*) +{ + m_primaryClient->fakeInputEnd(); +} + +void +Server::handleFileChunkSendingEvent(const Event& event, void*) +{ + onFileChunkSending(event.getData()); +} + +void +Server::handleFileRecieveCompletedEvent(const Event& event, void*) +{ + onFileRecieveCompleted(); +} + +void +Server::onClipboardChanged(BaseClientProxy* sender, + ClipboardID id, UInt32 seqNum) +{ + ClipboardInfo& clipboard = m_clipboards[id]; + + // ignore update if sequence number is old + if (seqNum < clipboard.m_clipboardSeqNum) { + LOG((CLOG_INFO "ignored screen \"%s\" update of clipboard %d (missequenced)", getName(sender).c_str(), id)); + return; + } + + // should be the expected client + assert(sender == m_clients.find(clipboard.m_clipboardOwner)->second); + + // get data + sender->getClipboard(id, &clipboard.m_clipboard); + + // ignore if data hasn't changed + String data = clipboard.m_clipboard.marshall(); + if (data == clipboard.m_clipboardData) { + LOG((CLOG_DEBUG "ignored screen \"%s\" update of clipboard %d (unchanged)", clipboard.m_clipboardOwner.c_str(), id)); + return; + } + + // got new data + LOG((CLOG_INFO "screen \"%s\" updated clipboard %d", clipboard.m_clipboardOwner.c_str(), id)); + clipboard.m_clipboardData = data; + + // tell all clients except the sender that the clipboard is dirty + for (ClientList::const_iterator index = m_clients.begin(); + index != m_clients.end(); ++index) { + BaseClientProxy* client = index->second; + client->setClipboardDirty(id, client != sender); + } + + // send the new clipboard to the active screen + m_active->setClipboard(id, &clipboard.m_clipboard); +} + +void +Server::onScreensaver(bool activated) +{ + LOG((CLOG_DEBUG "onScreenSaver %s", activated ? "activated" : "deactivated")); + + if (activated) { + // save current screen and position + m_activeSaver = m_active; + m_xSaver = m_x; + m_ySaver = m_y; + + // jump to primary screen + if (m_active != m_primaryClient) { + switchScreen(m_primaryClient, 0, 0, true); + } + } + else { + // jump back to previous screen and position. we must check + // that the position is still valid since the screen may have + // changed resolutions while the screen saver was running. + if (m_activeSaver != NULL && m_activeSaver != m_primaryClient) { + // check position + BaseClientProxy* screen = m_activeSaver; + SInt32 x, y, w, h; + screen->getShape(x, y, w, h); + SInt32 zoneSize = getJumpZoneSize(screen); + if (m_xSaver < x + zoneSize) { + m_xSaver = x + zoneSize; + } + else if (m_xSaver >= x + w - zoneSize) { + m_xSaver = x + w - zoneSize - 1; + } + if (m_ySaver < y + zoneSize) { + m_ySaver = y + zoneSize; + } + else if (m_ySaver >= y + h - zoneSize) { + m_ySaver = y + h - zoneSize - 1; + } + + // jump + switchScreen(screen, m_xSaver, m_ySaver, false); + } + + // reset state + m_activeSaver = NULL; + } + + // send message to all clients + for (ClientList::const_iterator index = m_clients.begin(); + index != m_clients.end(); ++index) { + BaseClientProxy* client = index->second; + client->screensaver(activated); + } +} + +void +Server::onKeyDown(KeyID id, KeyModifierMask mask, KeyButton button, + const char* screens) +{ + LOG((CLOG_DEBUG1 "onKeyDown id=%d mask=0x%04x button=0x%04x", id, mask, button)); + assert(m_active != NULL); + + // relay + if (!m_keyboardBroadcasting && IKeyState::KeyInfo::isDefault(screens)) { + m_active->keyDown(id, mask, button); + } + else { + if (!screens && m_keyboardBroadcasting) { + screens = m_keyboardBroadcastingScreens.c_str(); + if (IKeyState::KeyInfo::isDefault(screens)) { + screens = "*"; + } + } + for (ClientList::const_iterator index = m_clients.begin(); + index != m_clients.end(); ++index) { + if (IKeyState::KeyInfo::contains(screens, index->first)) { + index->second->keyDown(id, mask, button); + } + } + } +} + +void +Server::onKeyUp(KeyID id, KeyModifierMask mask, KeyButton button, + const char* screens) +{ + LOG((CLOG_DEBUG1 "onKeyUp id=%d mask=0x%04x button=0x%04x", id, mask, button)); + assert(m_active != NULL); + + // relay + if (!m_keyboardBroadcasting && IKeyState::KeyInfo::isDefault(screens)) { + m_active->keyUp(id, mask, button); + } + else { + if (!screens && m_keyboardBroadcasting) { + screens = m_keyboardBroadcastingScreens.c_str(); + if (IKeyState::KeyInfo::isDefault(screens)) { + screens = "*"; + } + } + for (ClientList::const_iterator index = m_clients.begin(); + index != m_clients.end(); ++index) { + if (IKeyState::KeyInfo::contains(screens, index->first)) { + index->second->keyUp(id, mask, button); + } + } + } +} + +void +Server::onKeyRepeat(KeyID id, KeyModifierMask mask, + SInt32 count, KeyButton button) +{ + LOG((CLOG_DEBUG1 "onKeyRepeat id=%d mask=0x%04x count=%d button=0x%04x", id, mask, count, button)); + assert(m_active != NULL); + + // relay + m_active->keyRepeat(id, mask, count, button); +} + +void +Server::onMouseDown(ButtonID id) +{ + LOG((CLOG_DEBUG1 "onMouseDown id=%d", id)); + assert(m_active != NULL); + + // relay + m_active->mouseDown(id); + + // reset this variable back to default value true + m_waitDragInfoThread = true; +} + +void +Server::onMouseUp(ButtonID id) +{ + LOG((CLOG_DEBUG1 "onMouseUp id=%d", id)); + assert(m_active != NULL); + + // relay + m_active->mouseUp(id); + + if (m_ignoreFileTransfer) { + m_ignoreFileTransfer = false; + return; + } + + if (m_args.m_enableDragDrop) { + if (!m_screen->isOnScreen()) { + String& file = m_screen->getDraggingFilename(); + if (!file.empty()) { + sendFileToClient(file.c_str()); + } + } + + // always clear dragging filename + m_screen->clearDraggingFilename(); + } +} + +bool +Server::onMouseMovePrimary(SInt32 x, SInt32 y) +{ + LOG((CLOG_DEBUG4 "onMouseMovePrimary %d,%d", x, y)); + + // mouse move on primary (server's) screen + if (m_active != m_primaryClient) { + // stale event -- we're actually on a secondary screen + return false; + } + + // save last delta + m_xDelta2 = m_xDelta; + m_yDelta2 = m_yDelta; + + // save current delta + m_xDelta = x - m_x; + m_yDelta = y - m_y; + + // save position + m_x = x; + m_y = y; + + // get screen shape + SInt32 ax, ay, aw, ah; + m_active->getShape(ax, ay, aw, ah); + SInt32 zoneSize = getJumpZoneSize(m_active); + + // clamp position to screen + SInt32 xc = x, yc = y; + if (xc < ax + zoneSize) { + xc = ax; + } + else if (xc >= ax + aw - zoneSize) { + xc = ax + aw - 1; + } + if (yc < ay + zoneSize) { + yc = ay; + } + else if (yc >= ay + ah - zoneSize) { + yc = ay + ah - 1; + } + + // see if we should change screens + // when the cursor is in a corner, there may be a screen either + // horizontally or vertically. check both directions. + EDirection dirh = kNoDirection, dirv = kNoDirection; + SInt32 xh = x, yv = y; + if (x < ax + zoneSize) { + xh -= zoneSize; + dirh = kLeft; + } + else if (x >= ax + aw - zoneSize) { + xh += zoneSize; + dirh = kRight; + } + if (y < ay + zoneSize) { + yv -= zoneSize; + dirv = kTop; + } + else if (y >= ay + ah - zoneSize) { + yv += zoneSize; + dirv = kBottom; + } + if (dirh == kNoDirection && dirv == kNoDirection) { + // still on local screen + noSwitch(x, y); + return false; + } + + // check both horizontally and vertically + EDirection dirs[] = {dirh, dirv}; + SInt32 xs[] = {xh, x}, ys[] = {y, yv}; + for (int i = 0; i < 2; ++i) { + EDirection dir = dirs[i]; + if (dir == kNoDirection) { + continue; + } + x = xs[i], y = ys[i]; + + // get jump destination + BaseClientProxy* newScreen = mapToNeighbor(m_active, dir, x, y); + + // should we switch or not? + if (isSwitchOkay(newScreen, dir, x, y, xc, yc)) { + if (m_args.m_enableDragDrop + && m_screen->isDraggingStarted() + && m_active != newScreen + && m_waitDragInfoThread) { + if (m_sendDragInfoThread == NULL) { + m_sendDragInfoThread = new Thread( + new TMethodJob<Server>( + this, + &Server::sendDragInfoThread, newScreen)); + } + + return false; + } + + // switch screen + switchScreen(newScreen, x, y, false); + m_waitDragInfoThread = true; + return true; + } + } + + return false; +} + +void +Server::sendDragInfoThread(void* arg) +{ + BaseClientProxy* newScreen = static_cast<BaseClientProxy*>(arg); + + m_dragFileList.clear(); + String& dragFileList = m_screen->getDraggingFilename(); + if (!dragFileList.empty()) { + DragInformation di; + di.setFilename(dragFileList); + m_dragFileList.push_back(di); + } + +#if defined(__APPLE__) + // on mac it seems that after faking a LMB up, system would signal back + // to barrier a mouse up event, which doesn't happen on windows. as a + // result, barrier would send dragging file to client twice. This variable + // is used to ignore the first file sending. + m_ignoreFileTransfer = true; +#endif + + // send drag file info to client if there is any + if (m_dragFileList.size() > 0) { + sendDragInfo(newScreen); + m_dragFileList.clear(); + } + m_waitDragInfoThread = false; + m_sendDragInfoThread = NULL; +} + +void +Server::sendDragInfo(BaseClientProxy* newScreen) +{ + String infoString; + UInt32 fileCount = DragInformation::setupDragInfo(m_dragFileList, infoString); + + if (fileCount > 0) { + char* info = NULL; + size_t size = infoString.size(); + info = new char[size]; + memcpy(info, infoString.c_str(), size); + + LOG((CLOG_DEBUG2 "sending drag information to client")); + LOG((CLOG_DEBUG3 "dragging file list: %s", info)); + LOG((CLOG_DEBUG3 "dragging file list string size: %i", size)); + newScreen->sendDragInfo(fileCount, info, size); + } +} + +void +Server::onMouseMoveSecondary(SInt32 dx, SInt32 dy) +{ + LOG((CLOG_DEBUG2 "onMouseMoveSecondary %+d,%+d", dx, dy)); + + // mouse move on secondary (client's) screen + assert(m_active != NULL); + if (m_active == m_primaryClient) { + // stale event -- we're actually on the primary screen + return; + } + + // if doing relative motion on secondary screens and we're locked + // to the screen (which activates relative moves) then send a + // relative mouse motion. when we're doing this we pretend as if + // the mouse isn't actually moving because we're expecting some + // program on the secondary screen to warp the mouse on us, so we + // have no idea where it really is. + if (m_relativeMoves && isLockedToScreenServer()) { + LOG((CLOG_DEBUG2 "relative move on %s by %d,%d", getName(m_active).c_str(), dx, dy)); + m_active->mouseRelativeMove(dx, dy); + return; + } + + // save old position + const SInt32 xOld = m_x; + const SInt32 yOld = m_y; + + // save last delta + m_xDelta2 = m_xDelta; + m_yDelta2 = m_yDelta; + + // save current delta + m_xDelta = dx; + m_yDelta = dy; + + // accumulate motion + m_x += dx; + m_y += dy; + + // get screen shape + SInt32 ax, ay, aw, ah; + m_active->getShape(ax, ay, aw, ah); + + // find direction of neighbor and get the neighbor + bool jump = true; + BaseClientProxy* newScreen; + do { + // clamp position to screen + SInt32 xc = m_x, yc = m_y; + if (xc < ax) { + xc = ax; + } + else if (xc >= ax + aw) { + xc = ax + aw - 1; + } + if (yc < ay) { + yc = ay; + } + else if (yc >= ay + ah) { + yc = ay + ah - 1; + } + + EDirection dir; + if (m_x < ax) { + dir = kLeft; + } + else if (m_x > ax + aw - 1) { + dir = kRight; + } + else if (m_y < ay) { + dir = kTop; + } + else if (m_y > ay + ah - 1) { + dir = kBottom; + } + else { + // we haven't left the screen + newScreen = m_active; + jump = false; + + // if waiting and mouse is not on the border we're waiting + // on then stop waiting. also if it's not on the border + // then arm the double tap. + if (m_switchScreen != NULL) { + bool clearWait; + SInt32 zoneSize = m_primaryClient->getJumpZoneSize(); + switch (m_switchDir) { + case kLeft: + clearWait = (m_x >= ax + zoneSize); + break; + + case kRight: + clearWait = (m_x <= ax + aw - 1 - zoneSize); + break; + + case kTop: + clearWait = (m_y >= ay + zoneSize); + break; + + case kBottom: + clearWait = (m_y <= ay + ah - 1 + zoneSize); + break; + + default: + clearWait = false; + break; + } + if (clearWait) { + // still on local screen + noSwitch(m_x, m_y); + } + } + + // skip rest of block + break; + } + + // try to switch screen. get the neighbor. + newScreen = mapToNeighbor(m_active, dir, m_x, m_y); + + // see if we should switch + if (!isSwitchOkay(newScreen, dir, m_x, m_y, xc, yc)) { + newScreen = m_active; + jump = false; + } + } while (false); + + if (jump) { + if (m_sendFileThread != NULL) { + StreamChunker::interruptFile(); + m_sendFileThread = NULL; + } + + SInt32 newX = m_x; + SInt32 newY = m_y; + + // switch screens + switchScreen(newScreen, newX, newY, false); + } + else { + // same screen. clamp mouse to edge. + m_x = xOld + dx; + m_y = yOld + dy; + if (m_x < ax) { + m_x = ax; + LOG((CLOG_DEBUG2 "clamp to left of \"%s\"", getName(m_active).c_str())); + } + else if (m_x > ax + aw - 1) { + m_x = ax + aw - 1; + LOG((CLOG_DEBUG2 "clamp to right of \"%s\"", getName(m_active).c_str())); + } + if (m_y < ay) { + m_y = ay; + LOG((CLOG_DEBUG2 "clamp to top of \"%s\"", getName(m_active).c_str())); + } + else if (m_y > ay + ah - 1) { + m_y = ay + ah - 1; + LOG((CLOG_DEBUG2 "clamp to bottom of \"%s\"", getName(m_active).c_str())); + } + + // warp cursor if it moved. + if (m_x != xOld || m_y != yOld) { + LOG((CLOG_DEBUG2 "move on %s to %d,%d", getName(m_active).c_str(), m_x, m_y)); + m_active->mouseMove(m_x, m_y); + } + } +} + +void +Server::onMouseWheel(SInt32 xDelta, SInt32 yDelta) +{ + LOG((CLOG_DEBUG1 "onMouseWheel %+d,%+d", xDelta, yDelta)); + assert(m_active != NULL); + + // relay + m_active->mouseWheel(xDelta, yDelta); +} + +void +Server::onFileChunkSending(const void* data) +{ + FileChunk* chunk = static_cast<FileChunk*>(const_cast<void*>(data)); + + LOG((CLOG_DEBUG1 "sending file chunk")); + assert(m_active != NULL); + + // relay + m_active->fileChunkSending(chunk->m_chunk[0], &chunk->m_chunk[1], chunk->m_dataSize); +} + +void +Server::onFileRecieveCompleted() +{ + if (isReceivedFileSizeValid()) { + m_writeToDropDirThread = new Thread( + new TMethodJob<Server>( + this, &Server::writeToDropDirThread)); + } +} + +void +Server::writeToDropDirThread(void*) +{ + LOG((CLOG_DEBUG "starting write to drop dir thread")); + + while (m_screen->isFakeDraggingStarted()) { + ARCH->sleep(.1f); + } + + DropHelper::writeToDir(m_screen->getDropTarget(), m_fakeDragFileList, + m_receivedFileData); +} + +bool +Server::addClient(BaseClientProxy* client) +{ + String name = getName(client); + if (m_clients.count(name) != 0) { + return false; + } + + // add event handlers + m_events->adoptHandler(m_events->forIScreen().shapeChanged(), + client->getEventTarget(), + new TMethodEventJob<Server>(this, + &Server::handleShapeChanged, client)); + m_events->adoptHandler(m_events->forClipboard().clipboardGrabbed(), + client->getEventTarget(), + new TMethodEventJob<Server>(this, + &Server::handleClipboardGrabbed, client)); + m_events->adoptHandler(m_events->forClipboard().clipboardChanged(), + client->getEventTarget(), + new TMethodEventJob<Server>(this, + &Server::handleClipboardChanged, client)); + + // add to list + m_clientSet.insert(client); + m_clients.insert(std::make_pair(name, client)); + + // initialize client data + SInt32 x, y; + client->getCursorPos(x, y); + client->setJumpCursorPos(x, y); + + // tell primary client about the active sides + m_primaryClient->reconfigure(getActivePrimarySides()); + + return true; +} + +bool +Server::removeClient(BaseClientProxy* client) +{ + // return false if not in list + ClientSet::iterator i = m_clientSet.find(client); + if (i == m_clientSet.end()) { + return false; + } + + // remove event handlers + m_events->removeHandler(m_events->forIScreen().shapeChanged(), + client->getEventTarget()); + m_events->removeHandler(m_events->forClipboard().clipboardGrabbed(), + client->getEventTarget()); + m_events->removeHandler(m_events->forClipboard().clipboardChanged(), + client->getEventTarget()); + + // remove from list + m_clients.erase(getName(client)); + m_clientSet.erase(i); + + return true; +} + +void +Server::closeClient(BaseClientProxy* client, const char* msg) +{ + assert(client != m_primaryClient); + assert(msg != NULL); + + // send message to client. this message should cause the client + // to disconnect. we add this client to the closed client list + // and install a timer to remove the client if it doesn't respond + // quickly enough. we also remove the client from the active + // client list since we're not going to listen to it anymore. + // note that this method also works on clients that are not in + // the m_clients list. adoptClient() may call us with such a + // client. + LOG((CLOG_NOTE "disconnecting client \"%s\"", getName(client).c_str())); + + // send message + // FIXME -- avoid type cast (kinda hard, though) + ((ClientProxy*)client)->close(msg); + + // install timer. wait timeout seconds for client to close. + double timeout = 5.0; + EventQueueTimer* timer = m_events->newOneShotTimer(timeout, NULL); + m_events->adoptHandler(Event::kTimer, timer, + new TMethodEventJob<Server>(this, + &Server::handleClientCloseTimeout, client)); + + // move client to closing list + removeClient(client); + m_oldClients.insert(std::make_pair(client, timer)); + + // if this client is the active screen then we have to + // jump off of it + forceLeaveClient(client); +} + +void +Server::closeClients(const Config& config) +{ + // collect the clients that are connected but are being dropped + // from the configuration (or who's canonical name is changing). + typedef std::set<BaseClientProxy*> RemovedClients; + RemovedClients removed; + for (ClientList::iterator index = m_clients.begin(); + index != m_clients.end(); ++index) { + if (!config.isCanonicalName(index->first)) { + removed.insert(index->second); + } + } + + // don't close the primary client + removed.erase(m_primaryClient); + + // now close them. we collect the list then close in two steps + // because closeClient() modifies the collection we iterate over. + for (RemovedClients::iterator index = removed.begin(); + index != removed.end(); ++index) { + closeClient(*index, kMsgCClose); + } +} + +void +Server::removeActiveClient(BaseClientProxy* client) +{ + if (removeClient(client)) { + forceLeaveClient(client); + m_events->removeHandler(m_events->forClientProxy().disconnected(), client); + if (m_clients.size() == 1 && m_oldClients.empty()) { + m_events->addEvent(Event(m_events->forServer().disconnected(), this)); + } + } +} + +void +Server::removeOldClient(BaseClientProxy* client) +{ + OldClients::iterator i = m_oldClients.find(client); + if (i != m_oldClients.end()) { + m_events->removeHandler(m_events->forClientProxy().disconnected(), client); + m_events->removeHandler(Event::kTimer, i->second); + m_events->deleteTimer(i->second); + m_oldClients.erase(i); + if (m_clients.size() == 1 && m_oldClients.empty()) { + m_events->addEvent(Event(m_events->forServer().disconnected(), this)); + } + } +} + +void +Server::forceLeaveClient(BaseClientProxy* client) +{ + BaseClientProxy* active = + (m_activeSaver != NULL) ? m_activeSaver : m_active; + if (active == client) { + // record new position (center of primary screen) + m_primaryClient->getCursorCenter(m_x, m_y); + + // stop waiting to switch to this client + if (active == m_switchScreen) { + stopSwitch(); + } + + // don't notify active screen since it has probably already + // disconnected. + LOG((CLOG_INFO "jump from \"%s\" to \"%s\" at %d,%d", getName(active).c_str(), getName(m_primaryClient).c_str(), m_x, m_y)); + + // cut over + m_active = m_primaryClient; + + // enter new screen (unless we already have because of the + // screen saver) + if (m_activeSaver == NULL) { + m_primaryClient->enter(m_x, m_y, m_seqNum, + m_primaryClient->getToggleMask(), false); + } + } + + // if this screen had the cursor when the screen saver activated + // then we can't switch back to it when the screen saver + // deactivates. + if (m_activeSaver == client) { + m_activeSaver = NULL; + } + + // tell primary client about the active sides + m_primaryClient->reconfigure(getActivePrimarySides()); +} + + +// +// Server::ClipboardInfo +// + +Server::ClipboardInfo::ClipboardInfo() : + m_clipboard(), + m_clipboardData(), + m_clipboardOwner(), + m_clipboardSeqNum(0) +{ + // do nothing +} + + +// +// Server::LockCursorToScreenInfo +// + +Server::LockCursorToScreenInfo* +Server::LockCursorToScreenInfo::alloc(State state) +{ + LockCursorToScreenInfo* info = + (LockCursorToScreenInfo*)malloc(sizeof(LockCursorToScreenInfo)); + info->m_state = state; + return info; +} + + +// +// Server::SwitchToScreenInfo +// + +Server::SwitchToScreenInfo* +Server::SwitchToScreenInfo::alloc(const String& screen) +{ + SwitchToScreenInfo* info = + (SwitchToScreenInfo*)malloc(sizeof(SwitchToScreenInfo) + + screen.size()); + strcpy(info->m_screen, screen.c_str()); + return info; +} + + +// +// Server::SwitchInDirectionInfo +// + +Server::SwitchInDirectionInfo* +Server::SwitchInDirectionInfo::alloc(EDirection direction) +{ + SwitchInDirectionInfo* info = + (SwitchInDirectionInfo*)malloc(sizeof(SwitchInDirectionInfo)); + info->m_direction = direction; + return info; +} + +// +// Server::KeyboardBroadcastInfo +// + +Server::KeyboardBroadcastInfo* +Server::KeyboardBroadcastInfo::alloc(State state) +{ + KeyboardBroadcastInfo* info = + (KeyboardBroadcastInfo*)malloc(sizeof(KeyboardBroadcastInfo)); + info->m_state = state; + info->m_screens[0] = '\0'; + return info; +} + +Server::KeyboardBroadcastInfo* +Server::KeyboardBroadcastInfo::alloc(State state, const String& screens) +{ + KeyboardBroadcastInfo* info = + (KeyboardBroadcastInfo*)malloc(sizeof(KeyboardBroadcastInfo) + + screens.size()); + info->m_state = state; + strcpy(info->m_screens, screens.c_str()); + return info; +} + +bool +Server::isReceivedFileSizeValid() +{ + return m_expectedFileSize == m_receivedFileData.size(); +} + +void +Server::sendFileToClient(const char* filename) +{ + if (m_sendFileThread != NULL) { + StreamChunker::interruptFile(); + } + + m_sendFileThread = new Thread( + new TMethodJob<Server>( + this, &Server::sendFileThread, + static_cast<void*>(const_cast<char*>(filename)))); +} + +void +Server::sendFileThread(void* data) +{ + try { + char* filename = static_cast<char*>(data); + LOG((CLOG_DEBUG "sending file to client, filename=%s", filename)); + StreamChunker::sendFile(filename, m_events, this); + } + catch (std::runtime_error &error) { + LOG((CLOG_ERR "failed sending file chunks, error: %s", error.what())); + } + + m_sendFileThread = NULL; +} + +void +Server::dragInfoReceived(UInt32 fileNum, String content) +{ + if (!m_args.m_enableDragDrop) { + LOG((CLOG_DEBUG "drag drop not enabled, ignoring drag info.")); + return; + } + + DragInformation::parseDragInfo(m_fakeDragFileList, fileNum, content); + + m_screen->startDraggingFiles(m_fakeDragFileList); +} diff --git a/src/lib/server/Server.h b/src/lib/server/Server.h new file mode 100644 index 0000000..609af21 --- /dev/null +++ b/src/lib/server/Server.h @@ -0,0 +1,483 @@ +/* + * barrier -- mouse and keyboard sharing utility + * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2002 Chris Schoeneman + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "server/Config.h" +#include "barrier/clipboard_types.h" +#include "barrier/Clipboard.h" +#include "barrier/key_types.h" +#include "barrier/mouse_types.h" +#include "barrier/INode.h" +#include "barrier/DragInformation.h" +#include "barrier/ServerArgs.h" +#include "base/Event.h" +#include "base/Stopwatch.h" +#include "base/EventTypes.h" +#include "common/stdmap.h" +#include "common/stdset.h" +#include "common/stdvector.h" + +class BaseClientProxy; +class EventQueueTimer; +class PrimaryClient; +class InputFilter; +namespace barrier { class Screen; } +class IEventQueue; +class Thread; +class ClientListener; + +//! Barrier server +/*! +This class implements the top-level server algorithms for barrier. +*/ +class Server : public INode { +public: + //! Lock cursor to screen data + class LockCursorToScreenInfo { + public: + enum State { kOff, kOn, kToggle }; + + static LockCursorToScreenInfo* alloc(State state = kToggle); + + public: + State m_state; + }; + + //! Switch to screen data + class SwitchToScreenInfo { + public: + static SwitchToScreenInfo* alloc(const String& screen); + + public: + // this is a C-string; this type is a variable size structure + char m_screen[1]; + }; + + //! Switch in direction data + class SwitchInDirectionInfo { + public: + static SwitchInDirectionInfo* alloc(EDirection direction); + + public: + EDirection m_direction; + }; + + //! Screen connected data + class ScreenConnectedInfo { + public: + ScreenConnectedInfo(String screen) : m_screen(screen) { } + + public: + String m_screen; // was char[1] + }; + + //! Keyboard broadcast data + class KeyboardBroadcastInfo { + public: + enum State { kOff, kOn, kToggle }; + + static KeyboardBroadcastInfo* alloc(State state = kToggle); + static KeyboardBroadcastInfo* alloc(State state, + const String& screens); + + public: + State m_state; + char m_screens[1]; + }; + + /*! + Start the server with the configuration \p config and the primary + client (local screen) \p primaryClient. The client retains + ownership of \p primaryClient. + */ + Server(Config& config, PrimaryClient* primaryClient, + barrier::Screen* screen, IEventQueue* events, ServerArgs const& args); + ~Server(); + +#ifdef TEST_ENV + Server() : m_mock(true), m_config(NULL) { } + void setActive(BaseClientProxy* active) { m_active = active; } +#endif + + //! @name manipulators + //@{ + + //! Set configuration + /*! + Change the server's configuration. Returns true iff the new + configuration was accepted (it must include the server's name). + This will disconnect any clients no longer in the configuration. + */ + bool setConfig(const Config&); + + //! Add a client + /*! + Adds \p client to the server. The client is adopted and will be + destroyed when the client disconnects or is disconnected. + */ + void adoptClient(BaseClientProxy* client); + + //! Disconnect clients + /*! + Disconnect clients. This tells them to disconnect but does not wait + for them to actually do so. The server sends the disconnected event + when they're all disconnected (or immediately if none are connected). + The caller can also just destroy this object to force the disconnection. + */ + void disconnect(); + + //! Create a new thread and use it to send file to client + void sendFileToClient(const char* filename); + + //! Received dragging information from client + void dragInfoReceived(UInt32 fileNum, String content); + + //! Store ClientListener pointer + void setListener(ClientListener* p) { m_clientListener = p; } + + //@} + //! @name accessors + //@{ + + //! Get number of connected clients + /*! + Returns the number of connected clients, including the server itself. + */ + UInt32 getNumClients() const; + + //! Get the list of connected clients + /*! + Set the \c list to the names of the currently connected clients. + */ + void getClients(std::vector<String>& list) const; + + //! Return true if recieved file size is valid + bool isReceivedFileSizeValid(); + + //! Return expected file data size + size_t& getExpectedFileSize() { return m_expectedFileSize; } + + //! Return received file data + String& getReceivedFileData() { return m_receivedFileData; } + + //! Return fake drag file list + DragFileList getFakeDragFileList() { return m_fakeDragFileList; } + + //@} + +private: + // get canonical name of client + String getName(const BaseClientProxy*) const; + + // get the sides of the primary screen that have neighbors + UInt32 getActivePrimarySides() const; + + // returns true iff mouse should be locked to the current screen + // according to this object only, ignoring what the primary client + // says. + bool isLockedToScreenServer() const; + + // returns true iff mouse should be locked to the current screen + // according to this object or the primary client. + bool isLockedToScreen() const; + + // returns the jump zone of the client + SInt32 getJumpZoneSize(BaseClientProxy*) const; + + // change the active screen + void switchScreen(BaseClientProxy*, + SInt32 x, SInt32 y, bool forScreenSaver); + + // jump to screen + void jumpToScreen(BaseClientProxy*); + + // convert pixel position to fraction, using x or y depending on the + // direction. + float mapToFraction(BaseClientProxy*, EDirection, + SInt32 x, SInt32 y) const; + + // convert fraction to pixel position, writing only x or y depending + // on the direction. + void mapToPixel(BaseClientProxy*, EDirection, float f, + SInt32& x, SInt32& y) const; + + // returns true if the client has a neighbor anywhere along the edge + // indicated by the direction. + bool hasAnyNeighbor(BaseClientProxy*, EDirection) const; + + // lookup neighboring screen, mapping the coordinate independent of + // the direction to the neighbor's coordinate space. + BaseClientProxy* getNeighbor(BaseClientProxy*, EDirection, + SInt32& x, SInt32& y) const; + + // lookup neighboring screen. given a position relative to the + // source screen, find the screen we should move onto and where. + // if the position is sufficiently far from the source then we + // cross multiple screens. if there is no suitable screen then + // return NULL and x,y are not modified. + BaseClientProxy* mapToNeighbor(BaseClientProxy*, EDirection, + SInt32& x, SInt32& y) const; + + // adjusts x and y or neither to avoid ending up in a jump zone + // after entering the client in the given direction. + void avoidJumpZone(BaseClientProxy*, EDirection, + SInt32& x, SInt32& y) const; + + // test if a switch is permitted. this includes testing user + // options like switch delay and tracking any state required to + // implement them. returns true iff a switch is permitted. + bool isSwitchOkay(BaseClientProxy* dst, EDirection, + SInt32 x, SInt32 y, SInt32 xActive, SInt32 yActive); + + // update switch state due to a mouse move at \p x, \p y that + // doesn't switch screens. + void noSwitch(SInt32 x, SInt32 y); + + // stop switch timers + void stopSwitch(); + + // start two tap switch timer + void startSwitchTwoTap(); + + // arm the two tap switch timer if \p x, \p y is outside the tap zone + void armSwitchTwoTap(SInt32 x, SInt32 y); + + // stop the two tap switch timer + void stopSwitchTwoTap(); + + // returns true iff the two tap switch timer is started + bool isSwitchTwoTapStarted() const; + + // returns true iff should switch because of two tap + bool shouldSwitchTwoTap() const; + + // start delay switch timer + void startSwitchWait(SInt32 x, SInt32 y); + + // stop delay switch timer + void stopSwitchWait(); + + // returns true iff the delay switch timer is started + bool isSwitchWaitStarted() const; + + // returns the corner (EScreenSwitchCornerMasks) where x,y is on the + // given client. corners have the given size. + UInt32 getCorner(BaseClientProxy*, + SInt32 x, SInt32 y, SInt32 size) const; + + // stop relative mouse moves + void stopRelativeMoves(); + + // send screen options to \c client + void sendOptions(BaseClientProxy* client) const; + + // process options from configuration + void processOptions(); + + // event handlers + void handleShapeChanged(const Event&, void*); + void handleClipboardGrabbed(const Event&, void*); + void handleClipboardChanged(const Event&, void*); + void handleKeyDownEvent(const Event&, void*); + void handleKeyUpEvent(const Event&, void*); + void handleKeyRepeatEvent(const Event&, void*); + void handleButtonDownEvent(const Event&, void*); + void handleButtonUpEvent(const Event&, void*); + void handleMotionPrimaryEvent(const Event&, void*); + void handleMotionSecondaryEvent(const Event&, void*); + void handleWheelEvent(const Event&, void*); + void handleScreensaverActivatedEvent(const Event&, void*); + void handleScreensaverDeactivatedEvent(const Event&, void*); + void handleSwitchWaitTimeout(const Event&, void*); + void handleClientDisconnected(const Event&, void*); + void handleClientCloseTimeout(const Event&, void*); + void handleSwitchToScreenEvent(const Event&, void*); + void handleSwitchInDirectionEvent(const Event&, void*); + void handleKeyboardBroadcastEvent(const Event&,void*); + void handleLockCursorToScreenEvent(const Event&, void*); + void handleFakeInputBeginEvent(const Event&, void*); + void handleFakeInputEndEvent(const Event&, void*); + void handleFileChunkSendingEvent(const Event&, void*); + void handleFileRecieveCompletedEvent(const Event&, void*); + + // event processing + void onClipboardChanged(BaseClientProxy* sender, + ClipboardID id, UInt32 seqNum); + void onScreensaver(bool activated); + void onKeyDown(KeyID, KeyModifierMask, KeyButton, + const char* screens); + void onKeyUp(KeyID, KeyModifierMask, KeyButton, + const char* screens); + void onKeyRepeat(KeyID, KeyModifierMask, SInt32, KeyButton); + void onMouseDown(ButtonID); + void onMouseUp(ButtonID); + bool onMouseMovePrimary(SInt32 x, SInt32 y); + void onMouseMoveSecondary(SInt32 dx, SInt32 dy); + void onMouseWheel(SInt32 xDelta, SInt32 yDelta); + void onFileChunkSending(const void* data); + void onFileRecieveCompleted(); + + // add client to list and attach event handlers for client + bool addClient(BaseClientProxy*); + + // remove client from list and detach event handlers for client + bool removeClient(BaseClientProxy*); + + // close a client + void closeClient(BaseClientProxy*, const char* msg); + + // close clients not in \p config + void closeClients(const Config& config); + + // close all clients whether they've completed the handshake or not, + // except the primary client + void closeAllClients(); + + // remove clients from internal state + void removeActiveClient(BaseClientProxy*); + void removeOldClient(BaseClientProxy*); + + // force the cursor off of \p client + void forceLeaveClient(BaseClientProxy* client); + + // thread funciton for sending file + void sendFileThread(void*); + + // thread function for writing file to drop directory + void writeToDropDirThread(void*); + + // thread function for sending drag information + void sendDragInfoThread(void*); + + // send drag info to new client screen + void sendDragInfo(BaseClientProxy* newScreen); + +public: + bool m_mock; + +private: + class ClipboardInfo { + public: + ClipboardInfo(); + + public: + Clipboard m_clipboard; + String m_clipboardData; + String m_clipboardOwner; + UInt32 m_clipboardSeqNum; + }; + + // the primary screen client + PrimaryClient* m_primaryClient; + + // all clients (including the primary client) indexed by name + typedef std::map<String, BaseClientProxy*> ClientList; + typedef std::set<BaseClientProxy*> ClientSet; + ClientList m_clients; + ClientSet m_clientSet; + + // all old connections that we're waiting to hangup + typedef std::map<BaseClientProxy*, EventQueueTimer*> OldClients; + OldClients m_oldClients; + + // the client with focus + BaseClientProxy* m_active; + + // the sequence number of enter messages + UInt32 m_seqNum; + + // current mouse position (in absolute screen coordinates) on + // whichever screen is active + SInt32 m_x, m_y; + + // last mouse deltas. this is needed to smooth out double tap + // on win32 which reports bogus mouse motion at the edge of + // the screen when using low level hooks, synthesizing motion + // in the opposite direction the mouse actually moved. + SInt32 m_xDelta, m_yDelta; + SInt32 m_xDelta2, m_yDelta2; + + // current configuration + Config* m_config; + + // input filter (from m_config); + InputFilter* m_inputFilter; + + // clipboard cache + ClipboardInfo m_clipboards[kClipboardEnd]; + + // state saved when screen saver activates + BaseClientProxy* m_activeSaver; + SInt32 m_xSaver, m_ySaver; + + // common state for screen switch tests. all tests are always + // trying to reach the same screen in the same direction. + EDirection m_switchDir; + BaseClientProxy* m_switchScreen; + + // state for delayed screen switching + double m_switchWaitDelay; + EventQueueTimer* m_switchWaitTimer; + SInt32 m_switchWaitX, m_switchWaitY; + + // state for double-tap screen switching + double m_switchTwoTapDelay; + Stopwatch m_switchTwoTapTimer; + bool m_switchTwoTapEngaged; + bool m_switchTwoTapArmed; + SInt32 m_switchTwoTapZone; + + // modifiers needed before switching + bool m_switchNeedsShift; + bool m_switchNeedsControl; + bool m_switchNeedsAlt; + + // relative mouse move option + bool m_relativeMoves; + + // flag whether or not we have broadcasting enabled and the screens to + // which we should send broadcasted keys. + bool m_keyboardBroadcasting; + String m_keyboardBroadcastingScreens; + + // screen locking (former scroll lock) + bool m_lockedToScreen; + + // server screen + barrier::Screen* m_screen; + + IEventQueue* m_events; + + // file transfer + size_t m_expectedFileSize; + String m_receivedFileData; + DragFileList m_dragFileList; + DragFileList m_fakeDragFileList; + Thread* m_sendFileThread; + Thread* m_writeToDropDirThread; + String m_dragFileExt; + bool m_ignoreFileTransfer; + bool m_enableClipboard; + + Thread* m_sendDragInfoThread; + bool m_waitDragInfoThread; + + ClientListener* m_clientListener; + ServerArgs m_args; +}; |
