2120 lines
49 KiB
C++
2120 lines
49 KiB
C++
/*
|
|
* synergy -- mouse and keyboard sharing utility
|
|
* 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 COPYING 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.
|
|
*/
|
|
|
|
#include "CServer.h"
|
|
#include "CHTTPServer.h"
|
|
#include "CPrimaryClient.h"
|
|
#include "IPrimaryScreenFactory.h"
|
|
#include "CInputPacketStream.h"
|
|
#include "COutputPacketStream.h"
|
|
#include "CProtocolUtil.h"
|
|
#include "CClientProxy1_0.h"
|
|
#include "CClientProxy1_1.h"
|
|
#include "OptionTypes.h"
|
|
#include "ProtocolTypes.h"
|
|
#include "XScreen.h"
|
|
#include "XSynergy.h"
|
|
#include "CTCPListenSocket.h"
|
|
#include "IDataSocket.h"
|
|
#include "ISocketFactory.h"
|
|
#include "XSocket.h"
|
|
#include "IStreamFilterFactory.h"
|
|
#include "CLock.h"
|
|
#include "CThread.h"
|
|
#include "CTimerThread.h"
|
|
#include "XMT.h"
|
|
#include "XThread.h"
|
|
#include "CFunctionJob.h"
|
|
#include "CLog.h"
|
|
#include "CStopwatch.h"
|
|
#include "TMethodJob.h"
|
|
#include "CArch.h"
|
|
|
|
//
|
|
// CServer
|
|
//
|
|
|
|
const SInt32 CServer::s_httpMaxSimultaneousRequests = 3;
|
|
|
|
CServer::CServer(const CString& serverName) :
|
|
m_name(serverName),
|
|
m_error(false),
|
|
m_bindTimeout(5.0 * 60.0),
|
|
m_screenFactory(NULL),
|
|
m_socketFactory(NULL),
|
|
m_streamFilterFactory(NULL),
|
|
m_acceptClientThread(NULL),
|
|
m_active(NULL),
|
|
m_primaryClient(NULL),
|
|
m_seqNum(0),
|
|
m_activeSaver(NULL),
|
|
m_httpServer(NULL),
|
|
m_httpAvailable(&m_mutex, s_httpMaxSimultaneousRequests),
|
|
m_switchDir(kNoDirection),
|
|
m_switchScreen(NULL),
|
|
m_switchWaitDelay(0.0),
|
|
m_switchWaitEngaged(false),
|
|
m_switchTwoTapDelay(0.0),
|
|
m_switchTwoTapEngaged(false),
|
|
m_switchTwoTapArmed(false),
|
|
m_status(kNotRunning)
|
|
{
|
|
// do nothing
|
|
}
|
|
|
|
CServer::~CServer()
|
|
{
|
|
delete m_screenFactory;
|
|
delete m_socketFactory;
|
|
delete m_streamFilterFactory;
|
|
}
|
|
|
|
void
|
|
CServer::open()
|
|
{
|
|
// open the screen
|
|
try {
|
|
LOG((CLOG_INFO "opening screen"));
|
|
openPrimaryScreen();
|
|
setStatus(kNotRunning);
|
|
}
|
|
catch (XScreen& e) {
|
|
// can't open screen
|
|
setStatus(kError, e.what());
|
|
LOG((CLOG_INFO "failed to open screen"));
|
|
throw;
|
|
}
|
|
catch (XUnknownClient& e) {
|
|
// can't open screen
|
|
setStatus(kServerNameUnknown);
|
|
LOG((CLOG_CRIT "unknown screen name `%s'", e.getName().c_str()));
|
|
throw;
|
|
}
|
|
}
|
|
|
|
void
|
|
CServer::mainLoop()
|
|
{
|
|
// check preconditions
|
|
{
|
|
CLock lock(&m_mutex);
|
|
assert(m_primaryClient != NULL);
|
|
}
|
|
|
|
try {
|
|
setStatus(kNotRunning);
|
|
LOG((CLOG_NOTE "starting server"));
|
|
|
|
// start listening for new clients
|
|
m_acceptClientThread = new CThread(startThread(
|
|
new TMethodJob<CServer>(this,
|
|
&CServer::acceptClients)));
|
|
|
|
// start listening for HTTP requests
|
|
if (m_config.getHTTPAddress().isValid()) {
|
|
m_httpServer = new CHTTPServer(this);
|
|
startThread(new TMethodJob<CServer>(this,
|
|
&CServer::acceptHTTPClients));
|
|
}
|
|
|
|
// handle events
|
|
m_primaryClient->mainLoop();
|
|
|
|
// clean up
|
|
LOG((CLOG_NOTE "stopping server"));
|
|
|
|
// use a macro to write the stuff that should go into a finally
|
|
// block so we can repeat it easily. stroustrup's view that
|
|
// "resource acquistion is initialization" is a better solution
|
|
// than a finally block is parochial. they both have their
|
|
// place. adding finally to C++ would've been a drop in a big
|
|
// bucket.
|
|
#define FINALLY do { \
|
|
stopThreads(); \
|
|
delete m_httpServer; \
|
|
m_httpServer = NULL; \
|
|
runStatusJobs(); \
|
|
} while (false)
|
|
FINALLY;
|
|
}
|
|
catch (XMT& e) {
|
|
LOG((CLOG_ERR "server error: %s", e.what()));
|
|
setStatus(kError, e.what());
|
|
|
|
// clean up
|
|
LOG((CLOG_NOTE "stopping server"));
|
|
FINALLY;
|
|
throw;
|
|
}
|
|
catch (XBase& e) {
|
|
LOG((CLOG_ERR "server error: %s", e.what()));
|
|
setStatus(kError, e.what());
|
|
|
|
// clean up
|
|
LOG((CLOG_NOTE "stopping server"));
|
|
FINALLY;
|
|
}
|
|
catch (XThread&) {
|
|
setStatus(kNotRunning);
|
|
|
|
// clean up
|
|
LOG((CLOG_NOTE "stopping server"));
|
|
FINALLY;
|
|
throw;
|
|
}
|
|
catch (...) {
|
|
LOG((CLOG_DEBUG "unknown server error"));
|
|
setStatus(kError);
|
|
|
|
// clean up
|
|
LOG((CLOG_NOTE "stopping server"));
|
|
FINALLY;
|
|
throw;
|
|
}
|
|
#undef FINALLY
|
|
|
|
// throw if there was an error
|
|
if (m_error) {
|
|
LOG((CLOG_DEBUG "forwarding child thread exception"));
|
|
throw XServerRethrow();
|
|
}
|
|
}
|
|
|
|
void
|
|
CServer::exitMainLoop()
|
|
{
|
|
m_primaryClient->exitMainLoop();
|
|
}
|
|
|
|
void
|
|
CServer::exitMainLoopWithError()
|
|
{
|
|
{
|
|
CLock lock(&m_mutex);
|
|
m_error = true;
|
|
}
|
|
exitMainLoop();
|
|
}
|
|
|
|
void
|
|
CServer::close()
|
|
{
|
|
if (m_primaryClient != NULL) {
|
|
closePrimaryScreen();
|
|
}
|
|
LOG((CLOG_INFO "closed screen"));
|
|
}
|
|
|
|
bool
|
|
CServer::setConfig(const CConfig& config)
|
|
{
|
|
// refuse configuration if it doesn't include the primary screen
|
|
{
|
|
CLock lock(&m_mutex);
|
|
if (m_primaryClient != NULL &&
|
|
!config.isScreen(m_primaryClient->getName())) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// close clients that are connected but being dropped from the
|
|
// configuration.
|
|
closeClients(config);
|
|
|
|
// cut over
|
|
CLock lock(&m_mutex);
|
|
m_config = config;
|
|
|
|
// process global options
|
|
const CConfig::CScreenOptions* options = m_config.getOptions("");
|
|
if (options != NULL && options->size() > 0) {
|
|
for (CConfig::CScreenOptions::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;
|
|
}
|
|
m_switchWaitEngaged = false;
|
|
}
|
|
else if (id == kOptionScreenSwitchTwoTap) {
|
|
m_switchTwoTapDelay = 1.0e-3 * static_cast<double>(value);
|
|
if (m_switchTwoTapDelay < 0.0) {
|
|
m_switchTwoTapDelay = 0.0;
|
|
}
|
|
m_switchTwoTapEngaged = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// tell primary screen about reconfiguration
|
|
if (m_primaryClient != NULL) {
|
|
m_primaryClient->reconfigure(getActivePrimarySides());
|
|
}
|
|
|
|
// tell all (connected) clients about current options
|
|
for (CClientList::const_iterator index = m_clients.begin();
|
|
index != m_clients.end(); ++index) {
|
|
IClient* client = index->second;
|
|
sendOptions(client);
|
|
}
|
|
|
|
// notify of status
|
|
runStatusJobs();
|
|
|
|
return true;
|
|
}
|
|
|
|
void
|
|
CServer::setScreenFactory(IPrimaryScreenFactory* adopted)
|
|
{
|
|
CLock lock(&m_mutex);
|
|
delete m_screenFactory;
|
|
m_screenFactory = adopted;
|
|
}
|
|
|
|
void
|
|
CServer::setSocketFactory(ISocketFactory* adopted)
|
|
{
|
|
CLock lock(&m_mutex);
|
|
delete m_socketFactory;
|
|
m_socketFactory = adopted;
|
|
}
|
|
|
|
void
|
|
CServer::setStreamFilterFactory(IStreamFilterFactory* adopted)
|
|
{
|
|
CLock lock(&m_mutex);
|
|
delete m_streamFilterFactory;
|
|
m_streamFilterFactory = adopted;
|
|
}
|
|
|
|
void
|
|
CServer::addStatusJob(IJob* job)
|
|
{
|
|
m_statusJobs.addJob(job);
|
|
}
|
|
|
|
void
|
|
CServer::removeStatusJob(IJob* job)
|
|
{
|
|
m_statusJobs.removeJob(job);
|
|
}
|
|
|
|
CString
|
|
CServer::getPrimaryScreenName() const
|
|
{
|
|
return m_name;
|
|
}
|
|
|
|
UInt32
|
|
CServer::getNumClients() const
|
|
{
|
|
CLock lock(&m_mutex);
|
|
return m_clients.size();
|
|
}
|
|
|
|
void
|
|
CServer::getClients(std::vector<CString>& list) const
|
|
{
|
|
CLock lock(&m_mutex);
|
|
list.clear();
|
|
for (CClientList::const_iterator index = m_clients.begin();
|
|
index != m_clients.end(); ++index) {
|
|
list.push_back(index->first);
|
|
}
|
|
}
|
|
|
|
CServer::EStatus
|
|
CServer::getStatus(CString* msg) const
|
|
{
|
|
CLock lock(&m_mutex);
|
|
if (msg != NULL) {
|
|
*msg = m_statusMessage;
|
|
}
|
|
return m_status;
|
|
}
|
|
|
|
void
|
|
CServer::getConfig(CConfig* config) const
|
|
{
|
|
assert(config != NULL);
|
|
|
|
CLock lock(&m_mutex);
|
|
*config = m_config;
|
|
}
|
|
|
|
void
|
|
CServer::runStatusJobs() const
|
|
{
|
|
m_statusJobs.runJobs();
|
|
}
|
|
|
|
void
|
|
CServer::setStatus(EStatus status, const char* msg)
|
|
{
|
|
{
|
|
CLock lock(&m_mutex);
|
|
m_status = status;
|
|
if (m_status == kError) {
|
|
m_statusMessage = (msg == NULL) ? "Error" : msg;
|
|
}
|
|
else {
|
|
m_statusMessage = (msg == NULL) ? "" : msg;
|
|
}
|
|
}
|
|
runStatusJobs();
|
|
}
|
|
|
|
UInt32
|
|
CServer::getActivePrimarySides() const
|
|
{
|
|
// note -- m_mutex must be locked on entry
|
|
UInt32 sides = 0;
|
|
if (!m_config.getNeighbor(getPrimaryScreenName(), kLeft).empty()) {
|
|
sides |= kLeftMask;
|
|
}
|
|
if (!m_config.getNeighbor(getPrimaryScreenName(), kRight).empty()) {
|
|
sides |= kRightMask;
|
|
}
|
|
if (!m_config.getNeighbor(getPrimaryScreenName(), kTop).empty()) {
|
|
sides |= kTopMask;
|
|
}
|
|
if (!m_config.getNeighbor(getPrimaryScreenName(), kBottom).empty()) {
|
|
sides |= kBottomMask;
|
|
}
|
|
return sides;
|
|
}
|
|
|
|
void
|
|
CServer::onError()
|
|
{
|
|
setStatus(kError);
|
|
|
|
// stop all running threads but don't wait too long since some
|
|
// threads may be unable to proceed until this thread returns.
|
|
stopThreads(3.0);
|
|
|
|
// done with the HTTP server
|
|
CLock lock(&m_mutex);
|
|
delete m_httpServer;
|
|
m_httpServer = NULL;
|
|
|
|
// note -- we do not attempt to close down the primary screen
|
|
}
|
|
|
|
void
|
|
CServer::onInfoChanged(const CString& name, const CClientInfo& info)
|
|
{
|
|
CLock lock(&m_mutex);
|
|
|
|
// look up client
|
|
CClientList::iterator index = m_clients.find(name);
|
|
if (index == m_clients.end()) {
|
|
throw XBadClient();
|
|
}
|
|
IClient* client = index->second;
|
|
assert(client != NULL);
|
|
|
|
// update the remote mouse coordinates
|
|
if (client == m_active) {
|
|
m_x = info.m_mx;
|
|
m_y = info.m_my;
|
|
}
|
|
LOG((CLOG_INFO "screen \"%s\" shape=%d,%d %dx%d zone=%d pos=%d,%d", name.c_str(), info.m_x, info.m_y, info.m_w, info.m_h, info.m_zoneSize, info.m_mx, info.m_my));
|
|
|
|
// handle resolution change to primary screen
|
|
if (client == m_primaryClient) {
|
|
if (client == m_active) {
|
|
onMouseMovePrimaryNoLock(m_x, m_y);
|
|
}
|
|
else {
|
|
onMouseMoveSecondaryNoLock(0, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool
|
|
CServer::onGrabClipboard(const CString& name, ClipboardID id, UInt32 seqNum)
|
|
{
|
|
CLock lock(&m_mutex);
|
|
|
|
// screen must be connected
|
|
CClientList::iterator grabber = m_clients.find(name);
|
|
if (grabber == m_clients.end()) {
|
|
throw XBadClient();
|
|
}
|
|
|
|
// ignore grab if sequence number is old. always allow primary
|
|
// screen to grab.
|
|
CClipboardInfo& clipboard = m_clipboards[id];
|
|
if (name != m_primaryClient->getName() &&
|
|
seqNum < clipboard.m_clipboardSeqNum) {
|
|
LOG((CLOG_INFO "ignored screen \"%s\" grab of clipboard %d", name.c_str(), id));
|
|
return false;
|
|
}
|
|
|
|
// mark screen as owning clipboard
|
|
LOG((CLOG_INFO "screen \"%s\" grabbed clipboard %d from \"%s\"", name.c_str(), id, clipboard.m_clipboardOwner.c_str()));
|
|
clipboard.m_clipboardOwner = name;
|
|
clipboard.m_clipboardSeqNum = seqNum;
|
|
|
|
// 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 (CClientList::iterator index = m_clients.begin();
|
|
index != m_clients.end(); ++index) {
|
|
IClient* client = index->second;
|
|
if (index == grabber) {
|
|
client->setClipboardDirty(id, false);
|
|
}
|
|
else {
|
|
client->grabClipboard(id);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void
|
|
CServer::onClipboardChanged(ClipboardID id, UInt32 seqNum, const CString& data)
|
|
{
|
|
CLock lock(&m_mutex);
|
|
onClipboardChangedNoLock(id, seqNum, data);
|
|
}
|
|
|
|
void
|
|
CServer::onClipboardChangedNoLock(ClipboardID id,
|
|
UInt32 seqNum, const CString& data)
|
|
{
|
|
CClipboardInfo& 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)", clipboard.m_clipboardOwner.c_str(), id));
|
|
return;
|
|
}
|
|
|
|
// ignore if data hasn't changed
|
|
if (data == clipboard.m_clipboardData) {
|
|
LOG((CLOG_DEBUG "ignored screen \"%s\" update of clipboard %d (unchanged)", clipboard.m_clipboardOwner.c_str(), id));
|
|
return;
|
|
}
|
|
|
|
// unmarshall into our clipboard buffer
|
|
LOG((CLOG_INFO "screen \"%s\" updated clipboard %d", clipboard.m_clipboardOwner.c_str(), id));
|
|
clipboard.m_clipboardData = data;
|
|
clipboard.m_clipboard.unmarshall(clipboard.m_clipboardData, 0);
|
|
|
|
// tell all clients except the sender that the clipboard is dirty
|
|
CClientList::const_iterator sender =
|
|
m_clients.find(clipboard.m_clipboardOwner);
|
|
for (CClientList::const_iterator index = m_clients.begin();
|
|
index != m_clients.end(); ++index) {
|
|
IClient* client = index->second;
|
|
client->setClipboardDirty(id, index != sender);
|
|
}
|
|
|
|
// send the new clipboard to the active screen
|
|
m_active->setClipboard(id, m_clipboards[id].m_clipboardData);
|
|
}
|
|
|
|
void
|
|
CServer::onScreensaver(bool activated)
|
|
{
|
|
LOG((CLOG_DEBUG "onScreenSaver %s", activated ? "activated" : "deactivated"));
|
|
CLock lock(&m_mutex);
|
|
|
|
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
|
|
IClient* screen = m_activeSaver;
|
|
SInt32 x, y, w, h;
|
|
screen->getShape(x, y, w, h);
|
|
SInt32 zoneSize = screen->getJumpZoneSize();
|
|
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 (CClientList::const_iterator index = m_clients.begin();
|
|
index != m_clients.end(); ++index) {
|
|
IClient* client = index->second;
|
|
client->screensaver(activated);
|
|
}
|
|
}
|
|
|
|
void
|
|
CServer::onOneShotTimerExpired(UInt32 id)
|
|
{
|
|
CLock lock(&m_mutex);
|
|
|
|
// ignore if it's an old timer or if switch wait isn't engaged anymore
|
|
if (!m_switchWaitEngaged || id != m_switchWaitTimer) {
|
|
return;
|
|
}
|
|
|
|
// ignore if mouse is locked to screen
|
|
if (isLockedToScreenNoLock()) {
|
|
LOG((CLOG_DEBUG1 "locked to screen"));
|
|
clearSwitchState();
|
|
return;
|
|
}
|
|
|
|
// switch screen
|
|
switchScreen(m_switchScreen, m_switchWaitX, m_switchWaitY, false);
|
|
}
|
|
|
|
void
|
|
CServer::onKeyDown(KeyID id, KeyModifierMask mask, KeyButton button)
|
|
{
|
|
LOG((CLOG_DEBUG1 "onKeyDown id=%d mask=0x%04x button=0x%04x", id, mask, button));
|
|
CLock lock(&m_mutex);
|
|
assert(m_active != NULL);
|
|
|
|
// handle command keys
|
|
if (onCommandKey(id, mask, true)) {
|
|
return;
|
|
}
|
|
|
|
// relay
|
|
m_active->keyDown(id, mask, button);
|
|
}
|
|
|
|
void
|
|
CServer::onKeyUp(KeyID id, KeyModifierMask mask, KeyButton button)
|
|
{
|
|
LOG((CLOG_DEBUG1 "onKeyUp id=%d mask=0x%04x button=0x%04x", id, mask, button));
|
|
CLock lock(&m_mutex);
|
|
assert(m_active != NULL);
|
|
|
|
// handle command keys
|
|
if (onCommandKey(id, mask, false)) {
|
|
return;
|
|
}
|
|
|
|
// relay
|
|
m_active->keyUp(id, mask, button);
|
|
}
|
|
|
|
void
|
|
CServer::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));
|
|
CLock lock(&m_mutex);
|
|
assert(m_active != NULL);
|
|
|
|
// handle command keys
|
|
if (onCommandKey(id, mask, false)) {
|
|
onCommandKey(id, mask, true);
|
|
return;
|
|
}
|
|
|
|
// relay
|
|
m_active->keyRepeat(id, mask, count, button);
|
|
}
|
|
|
|
void
|
|
CServer::onMouseDown(ButtonID id)
|
|
{
|
|
LOG((CLOG_DEBUG1 "onMouseDown id=%d", id));
|
|
CLock lock(&m_mutex);
|
|
assert(m_active != NULL);
|
|
|
|
// relay
|
|
m_active->mouseDown(id);
|
|
}
|
|
|
|
void
|
|
CServer::onMouseUp(ButtonID id)
|
|
{
|
|
LOG((CLOG_DEBUG1 "onMouseUp id=%d", id));
|
|
CLock lock(&m_mutex);
|
|
assert(m_active != NULL);
|
|
|
|
// relay
|
|
m_active->mouseUp(id);
|
|
}
|
|
|
|
bool
|
|
CServer::onMouseMovePrimary(SInt32 x, SInt32 y)
|
|
{
|
|
LOG((CLOG_DEBUG2 "onMouseMovePrimary %d,%d", x, y));
|
|
CLock lock(&m_mutex);
|
|
return onMouseMovePrimaryNoLock(x, y);
|
|
}
|
|
|
|
bool
|
|
CServer::onMouseMovePrimaryNoLock(SInt32 x, SInt32 y)
|
|
{
|
|
// mouse move on primary (server's) screen
|
|
assert(m_primaryClient != NULL);
|
|
assert(m_active == m_primaryClient);
|
|
|
|
// get screen shape
|
|
SInt32 ax, ay, aw, ah;
|
|
m_active->getShape(ax, ay, aw, ah);
|
|
SInt32 zoneSize = m_active->getJumpZoneSize();
|
|
|
|
// see if we should change screens
|
|
EDirection dir;
|
|
if (x < ax + zoneSize) {
|
|
x -= zoneSize;
|
|
dir = kLeft;
|
|
}
|
|
else if (x >= ax + aw - zoneSize) {
|
|
x += zoneSize;
|
|
dir = kRight;
|
|
}
|
|
else if (y < ay + zoneSize) {
|
|
y -= zoneSize;
|
|
dir = kTop;
|
|
}
|
|
else if (y >= ay + ah - zoneSize) {
|
|
y += zoneSize;
|
|
dir = kBottom;
|
|
}
|
|
else {
|
|
// still on local screen
|
|
onNoSwitch();
|
|
return false;
|
|
}
|
|
|
|
// get jump destination
|
|
IClient* newScreen = getNeighbor(m_active, dir, x, y);
|
|
|
|
// should we switch or not?
|
|
if (isSwitchOkay(newScreen, dir, x, y)) {
|
|
// switch screen
|
|
switchScreen(newScreen, x, y, false);
|
|
return true;
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void
|
|
CServer::onMouseMoveSecondary(SInt32 dx, SInt32 dy)
|
|
{
|
|
LOG((CLOG_DEBUG2 "onMouseMoveSecondary %+d,%+d", dx, dy));
|
|
CLock lock(&m_mutex);
|
|
onMouseMoveSecondaryNoLock(dx, dy);
|
|
}
|
|
|
|
void
|
|
CServer::onMouseMoveSecondaryNoLock(SInt32 dx, SInt32 dy)
|
|
{
|
|
// mouse move on secondary (client's) screen
|
|
assert(m_active != NULL);
|
|
if (m_active == m_primaryClient) {
|
|
// we're actually on the primary screen. this can happen
|
|
// when the primary screen begins processing a mouse move
|
|
// for a secondary screen, then the active (secondary)
|
|
// screen disconnects causing us to jump to the primary
|
|
// screen, and finally the primary screen finishes
|
|
// processing the mouse move, still thinking it's for
|
|
// a secondary screen. we just ignore the motion.
|
|
return;
|
|
}
|
|
|
|
// save old position
|
|
const SInt32 xOld = m_x;
|
|
const SInt32 yOld = m_y;
|
|
|
|
// 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
|
|
IClient* newScreen;
|
|
do {
|
|
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;
|
|
|
|
// 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) {
|
|
onNoSwitch();
|
|
}
|
|
}
|
|
|
|
// skip rest of block
|
|
break;
|
|
}
|
|
|
|
// try to switch screen. get the neighbor.
|
|
newScreen = getNeighbor(m_active, dir, m_x, m_y);
|
|
|
|
// see if we should switch
|
|
if (!isSwitchOkay(newScreen, dir, m_x, m_y)) {
|
|
newScreen = m_active;
|
|
}
|
|
} while (false);
|
|
|
|
if (newScreen != m_active) {
|
|
// switch screens
|
|
switchScreen(newScreen, m_x, m_y, 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\"", m_active->getName().c_str()));
|
|
}
|
|
else if (m_x > ax + aw - 1) {
|
|
m_x = ax + aw - 1;
|
|
LOG((CLOG_DEBUG2 "clamp to right of \"%s\"", m_active->getName().c_str()));
|
|
}
|
|
if (m_y < ay) {
|
|
m_y = ay;
|
|
LOG((CLOG_DEBUG2 "clamp to top of \"%s\"", m_active->getName().c_str()));
|
|
}
|
|
else if (m_y > ay + ah - 1) {
|
|
m_y = ay + ah - 1;
|
|
LOG((CLOG_DEBUG2 "clamp to bottom of \"%s\"", m_active->getName().c_str()));
|
|
}
|
|
|
|
// warp cursor if it moved.
|
|
if (m_x != xOld || m_y != yOld) {
|
|
LOG((CLOG_DEBUG2 "move on %s to %d,%d", m_active->getName().c_str(), m_x, m_y));
|
|
m_active->mouseMove(m_x, m_y);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
CServer::onMouseWheel(SInt32 delta)
|
|
{
|
|
LOG((CLOG_DEBUG1 "onMouseWheel %+d", delta));
|
|
CLock lock(&m_mutex);
|
|
assert(m_active != NULL);
|
|
|
|
// relay
|
|
m_active->mouseWheel(delta);
|
|
}
|
|
|
|
bool
|
|
CServer::onCommandKey(KeyID /*id*/, KeyModifierMask /*mask*/, bool /*down*/)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool
|
|
CServer::isLockedToScreenNoLock() const
|
|
{
|
|
// locked if scroll-lock is toggled on
|
|
if ((m_primaryClient->getToggleMask() & KeyModifierScrollLock) != 0) {
|
|
LOG((CLOG_DEBUG "locked by ScrollLock"));
|
|
return true;
|
|
}
|
|
|
|
// locked if primary says we're locked
|
|
if (m_primaryClient->isLockedToScreen()) {
|
|
return true;
|
|
}
|
|
|
|
// not locked
|
|
return false;
|
|
}
|
|
|
|
void
|
|
CServer::switchScreen(IClient* dst, SInt32 x, SInt32 y, bool forScreensaver)
|
|
{
|
|
// note -- must be locked on entry
|
|
|
|
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", m_active->getName().c_str(), dst->getName().c_str(), x, y));
|
|
|
|
// stop waiting to switch
|
|
clearSwitchState();
|
|
|
|
// record new position
|
|
m_x = x;
|
|
m_y = y;
|
|
|
|
// 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) {
|
|
for (ClipboardID id = 0; id < kClipboardEnd; ++id) {
|
|
CClipboardInfo& clipboard = m_clipboards[id];
|
|
if (clipboard.m_clipboardOwner == m_primaryClient->getName()) {
|
|
CString clipboardData;
|
|
m_primaryClient->getClipboard(id, clipboardData);
|
|
onClipboardChangedNoLock(id,
|
|
clipboard.m_clipboardSeqNum, clipboardData);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
|
|
// send the clipboard data to new active screen
|
|
for (ClipboardID id = 0; id < kClipboardEnd; ++id) {
|
|
m_active->setClipboard(id, m_clipboards[id].m_clipboardData);
|
|
}
|
|
}
|
|
else {
|
|
m_active->mouseMove(x, y);
|
|
}
|
|
}
|
|
|
|
IClient*
|
|
CServer::getNeighbor(IClient* src, EDirection dir) const
|
|
{
|
|
// note -- must be locked on entry
|
|
|
|
assert(src != NULL);
|
|
|
|
// get source screen name
|
|
CString srcName = src->getName();
|
|
assert(!srcName.empty());
|
|
LOG((CLOG_DEBUG2 "find neighbor on %s of \"%s\"", CConfig::dirName(dir), srcName.c_str()));
|
|
|
|
// get first neighbor. if it's the source then the source jumps
|
|
// to itself and we return the source.
|
|
CString dstName(m_config.getNeighbor(srcName, dir));
|
|
if (dstName == srcName) {
|
|
LOG((CLOG_DEBUG2 "\"%s\" is on %s of \"%s\"", dstName.c_str(), CConfig::dirName(dir), srcName.c_str()));
|
|
return src;
|
|
}
|
|
|
|
// keep checking
|
|
for (;;) {
|
|
// 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() || dstName == srcName) {
|
|
LOG((CLOG_DEBUG2 "no neighbor on %s of \"%s\"", CConfig::dirName(dir), srcName.c_str()));
|
|
return NULL;
|
|
}
|
|
|
|
// look up neighbor cell. if the screen is connected and
|
|
// ready then we can stop.
|
|
CClientList::const_iterator index = m_clients.find(dstName);
|
|
if (index != m_clients.end()) {
|
|
LOG((CLOG_DEBUG2 "\"%s\" is on %s of \"%s\"", dstName.c_str(), CConfig::dirName(dir), srcName.c_str()));
|
|
return index->second;
|
|
}
|
|
|
|
// skip over unconnected screen
|
|
LOG((CLOG_DEBUG2 "ignored \"%s\" on %s of \"%s\"", dstName.c_str(), CConfig::dirName(dir), srcName.c_str()));
|
|
srcName = dstName;
|
|
|
|
// look up name of neighbor of skipped screen
|
|
dstName = m_config.getNeighbor(srcName, dir);
|
|
}
|
|
}
|
|
|
|
IClient*
|
|
CServer::getNeighbor(IClient* src,
|
|
EDirection srcSide, SInt32& x, SInt32& y) const
|
|
{
|
|
// note -- must be locked on entry
|
|
|
|
assert(src != NULL);
|
|
|
|
// get the first neighbor
|
|
IClient* dst = getNeighbor(src, srcSide);
|
|
if (dst == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
// get the source screen's size (needed for kRight and kBottom)
|
|
SInt32 sx, sy, sw, sh;
|
|
SInt32 dx, dy, dw, dh;
|
|
IClient* lastGoodScreen = src;
|
|
lastGoodScreen->getShape(sx, sy, sw, sh);
|
|
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", dst->getName().c_str()));
|
|
dst = getNeighbor(lastGoodScreen, srcSide);
|
|
}
|
|
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", dst->getName().c_str()));
|
|
dst = getNeighbor(lastGoodScreen, srcSide);
|
|
}
|
|
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", dst->getName().c_str()));
|
|
dst = getNeighbor(lastGoodScreen, srcSide);
|
|
}
|
|
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 < sh) {
|
|
break;
|
|
}
|
|
LOG((CLOG_DEBUG2 "skipping over screen %s", dst->getName().c_str()));
|
|
dst = getNeighbor(lastGoodScreen, srcSide);
|
|
}
|
|
assert(lastGoodScreen != NULL);
|
|
y += dy;
|
|
break;
|
|
}
|
|
|
|
// 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.
|
|
if (dst == m_primaryClient) {
|
|
const CString dstName(dst->getName());
|
|
switch (srcSide) {
|
|
case kLeft:
|
|
if (!m_config.getNeighbor(dstName, kRight).empty() &&
|
|
x > dx + dw - 1 - dst->getJumpZoneSize())
|
|
x = dx + dw - 1 - dst->getJumpZoneSize();
|
|
break;
|
|
|
|
case kRight:
|
|
if (!m_config.getNeighbor(dstName, kLeft).empty() &&
|
|
x < dx + dst->getJumpZoneSize())
|
|
x = dx + dst->getJumpZoneSize();
|
|
break;
|
|
|
|
case kTop:
|
|
if (!m_config.getNeighbor(dstName, kBottom).empty() &&
|
|
y > dy + dh - 1 - dst->getJumpZoneSize())
|
|
y = dy + dh - 1 - dst->getJumpZoneSize();
|
|
break;
|
|
|
|
case kBottom:
|
|
if (!m_config.getNeighbor(dstName, kTop).empty() &&
|
|
y < dy + dst->getJumpZoneSize())
|
|
y = dy + dst->getJumpZoneSize();
|
|
break;
|
|
}
|
|
}
|
|
|
|
// adjust the coordinate orthogonal to srcSide to account for
|
|
// resolution differences. for example, if y is 200 pixels from
|
|
// the top on a screen 1000 pixels high (20% from the top) when
|
|
// we cross the left edge onto a screen 600 pixels high then y
|
|
// should be set 120 pixels from the top (again 20% from the
|
|
// top).
|
|
switch (srcSide) {
|
|
case kLeft:
|
|
case kRight:
|
|
y -= sy;
|
|
if (y < 0) {
|
|
y = 0;
|
|
}
|
|
else if (y >= sh) {
|
|
y = dh - 1;
|
|
}
|
|
else {
|
|
y = static_cast<SInt32>(0.5 + y *
|
|
static_cast<double>(dh - 1) / (sh - 1));
|
|
}
|
|
y += dy;
|
|
break;
|
|
|
|
case kTop:
|
|
case kBottom:
|
|
x -= sx;
|
|
if (x < 0) {
|
|
x = 0;
|
|
}
|
|
else if (x >= sw) {
|
|
x = dw - 1;
|
|
}
|
|
else {
|
|
x = static_cast<SInt32>(0.5 + x *
|
|
static_cast<double>(dw - 1) / (sw - 1));
|
|
}
|
|
x += dx;
|
|
break;
|
|
}
|
|
|
|
return dst;
|
|
}
|
|
|
|
bool
|
|
CServer::isSwitchOkay(IClient* newScreen, EDirection dir, SInt32 x, SInt32 y)
|
|
{
|
|
LOG((CLOG_DEBUG1 "try to leave \"%s\" on %s", m_active->getName().c_str(), CConfig::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", CConfig::dirName(dir)));
|
|
clearSwitchState();
|
|
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 || !m_switchTwoTapEngaged) {
|
|
// tapping a different or new edge. prepare for second tap.
|
|
preventSwitch = true;
|
|
m_switchTwoTapEngaged = true;
|
|
m_switchTwoTapArmed = false;
|
|
m_switchTwoTapTimer.reset();
|
|
LOG((CLOG_DEBUG1 "waiting for second tap"));
|
|
}
|
|
else {
|
|
// second tap if we were armed. if soon enough then switch.
|
|
if (m_switchTwoTapArmed &&
|
|
m_switchTwoTapTimer.getTime() <= m_switchTwoTapDelay) {
|
|
allowSwitch = true;
|
|
}
|
|
else {
|
|
// not fast enough. reset the clock.
|
|
preventSwitch = true;
|
|
m_switchTwoTapEngaged = true;
|
|
m_switchTwoTapArmed = false;
|
|
m_switchTwoTapTimer.reset();
|
|
LOG((CLOG_DEBUG1 "waiting for second tap"));
|
|
}
|
|
}
|
|
}
|
|
|
|
// if waiting before a switch then prepare to switch later
|
|
if (!allowSwitch && m_switchWaitDelay > 0.0) {
|
|
if (isNewDirection || !m_switchWaitEngaged) {
|
|
m_switchWaitEngaged = true;
|
|
m_switchWaitX = x;
|
|
m_switchWaitY = y;
|
|
m_switchWaitTimer = m_primaryClient->addOneShotTimer(
|
|
m_switchWaitDelay);
|
|
LOG((CLOG_DEBUG1 "waiting to switch"));
|
|
}
|
|
preventSwitch = true;
|
|
}
|
|
|
|
// ignore if mouse is locked to screen
|
|
if (!preventSwitch && isLockedToScreenNoLock()) {
|
|
LOG((CLOG_DEBUG1 "locked to screen"));
|
|
preventSwitch = true;
|
|
|
|
// don't try to switch later. it's possible that we might
|
|
// not be locked to the screen when the wait delay expires
|
|
// and could switch then but we'll base the decision on
|
|
// when the user first attempts the switch. this also
|
|
// ensures that all switch tests are using the same
|
|
clearSwitchState();
|
|
}
|
|
|
|
return !preventSwitch;
|
|
}
|
|
|
|
void
|
|
CServer::onNoSwitch()
|
|
{
|
|
if (m_switchTwoTapEngaged) {
|
|
if (m_switchTwoTapTimer.getTime() > m_switchTwoTapDelay) {
|
|
// second tap took too long. disengage.
|
|
m_switchTwoTapEngaged = false;
|
|
m_switchTwoTapArmed = false;
|
|
}
|
|
else {
|
|
// we've moved away from the edge and there's still
|
|
// time to get back for a double tap.
|
|
m_switchTwoTapArmed = true;
|
|
}
|
|
}
|
|
|
|
// once the mouse moves away from the edge we no longer want to
|
|
// switch after a delay.
|
|
m_switchWaitEngaged = false;
|
|
}
|
|
|
|
void
|
|
CServer::clearSwitchState()
|
|
{
|
|
if (m_switchScreen != NULL) {
|
|
m_switchDir = kNoDirection;
|
|
m_switchScreen = NULL;
|
|
m_switchWaitEngaged = false;
|
|
m_switchTwoTapEngaged = false;
|
|
}
|
|
}
|
|
|
|
void
|
|
CServer::closeClients(const CConfig& config)
|
|
{
|
|
CThreadList threads;
|
|
{
|
|
CLock lock(&m_mutex);
|
|
|
|
// get the set of clients that are connected but are being
|
|
// dropped from the configuration (or who's canonical name
|
|
// is changing) and tell them to disconnect. note that
|
|
// since m_clientThreads doesn't include a thread for the
|
|
// primary client we will not close it.
|
|
for (CClientThreadList::iterator
|
|
index = m_clientThreads.begin();
|
|
index != m_clientThreads.end(); ) {
|
|
const CString& name = index->first;
|
|
if (!config.isCanonicalName(name)) {
|
|
// lookup IClient with name
|
|
CClientList::const_iterator index2 = m_clients.find(name);
|
|
assert(index2 != m_clients.end());
|
|
|
|
// save the thread and remove it from m_clientThreads
|
|
threads.push_back(index->second);
|
|
m_clientThreads.erase(index++);
|
|
|
|
// close that client
|
|
assert(index2->second != m_primaryClient);
|
|
index2->second->close();
|
|
|
|
// don't switch to it if we planned to
|
|
if (index2->second == m_switchScreen) {
|
|
clearSwitchState();
|
|
}
|
|
}
|
|
else {
|
|
++index;
|
|
}
|
|
}
|
|
}
|
|
|
|
// wait a moment to allow each client to close its connection
|
|
// before we close it (to avoid having our socket enter TIME_WAIT).
|
|
if (threads.size() > 0) {
|
|
ARCH->sleep(1.0);
|
|
}
|
|
|
|
// cancel the old client threads
|
|
for (CThreadList::iterator index = threads.begin();
|
|
index != threads.end(); ++index) {
|
|
index->cancel();
|
|
}
|
|
|
|
// wait for old client threads to terminate. we must not hold
|
|
// the lock while we do this so those threads can finish any
|
|
// calls to this object.
|
|
for (CThreadList::iterator index = threads.begin();
|
|
index != threads.end(); ++index) {
|
|
index->wait();
|
|
}
|
|
|
|
// clean up thread list
|
|
reapThreads();
|
|
}
|
|
|
|
CThread
|
|
CServer::startThread(IJob* job)
|
|
{
|
|
CLock lock(&m_mutex);
|
|
|
|
// reap completed threads
|
|
doReapThreads(m_threads);
|
|
|
|
// add new thread to list
|
|
CThread thread(job);
|
|
m_threads.push_back(thread);
|
|
LOG((CLOG_DEBUG1 "started thread 0x%08x", thread.getID()));
|
|
return thread;
|
|
}
|
|
|
|
void
|
|
CServer::stopThreads(double timeout)
|
|
{
|
|
LOG((CLOG_DEBUG1 "stopping threads"));
|
|
|
|
// cancel the accept client thread to prevent more clients from
|
|
// connecting while we're shutting down.
|
|
CThread* acceptClientThread;
|
|
{
|
|
CLock lock(&m_mutex);
|
|
acceptClientThread = m_acceptClientThread;
|
|
m_acceptClientThread = NULL;
|
|
}
|
|
if (acceptClientThread != NULL) {
|
|
acceptClientThread->cancel();
|
|
acceptClientThread->wait(timeout);
|
|
delete acceptClientThread;
|
|
}
|
|
|
|
// close all clients (except the primary)
|
|
{
|
|
CConfig emptyConfig;
|
|
closeClients(emptyConfig);
|
|
}
|
|
|
|
// swap thread list so nobody can mess with it
|
|
CThreadList threads;
|
|
{
|
|
CLock lock(&m_mutex);
|
|
threads.swap(m_threads);
|
|
}
|
|
|
|
// cancel every thread
|
|
for (CThreadList::iterator index = threads.begin();
|
|
index != threads.end(); ++index) {
|
|
index->cancel();
|
|
}
|
|
|
|
// now wait for the threads
|
|
CStopwatch timer(true);
|
|
while (threads.size() > 0 && (timeout < 0.0 || timer.getTime() < timeout)) {
|
|
doReapThreads(threads);
|
|
ARCH->sleep(0.01);
|
|
}
|
|
|
|
// delete remaining threads
|
|
for (CThreadList::iterator index = threads.begin();
|
|
index != threads.end(); ++index) {
|
|
LOG((CLOG_DEBUG1 "reaped running thread 0x%08x", index->getID()));
|
|
}
|
|
|
|
LOG((CLOG_DEBUG1 "stopped threads"));
|
|
}
|
|
|
|
void
|
|
CServer::reapThreads()
|
|
{
|
|
CLock lock(&m_mutex);
|
|
doReapThreads(m_threads);
|
|
}
|
|
|
|
void
|
|
CServer::doReapThreads(CThreadList& threads)
|
|
{
|
|
for (CThreadList::iterator index = threads.begin();
|
|
index != threads.end(); ) {
|
|
if (index->wait(0.0)) {
|
|
// thread terminated
|
|
LOG((CLOG_DEBUG1 "reaped thread 0x%08x", index->getID()));
|
|
index = threads.erase(index);
|
|
}
|
|
else {
|
|
// thread is running
|
|
++index;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
CServer::acceptClients(void*)
|
|
{
|
|
LOG((CLOG_DEBUG1 "starting to wait for clients"));
|
|
|
|
IListenSocket* listen = NULL;
|
|
try {
|
|
// create socket listener
|
|
if (m_socketFactory != NULL) {
|
|
listen = m_socketFactory->createListen();
|
|
}
|
|
assert(listen != NULL);
|
|
|
|
// bind to the desired port. keep retrying if we can't bind
|
|
// the address immediately.
|
|
CStopwatch timer;
|
|
for (;;) {
|
|
try {
|
|
LOG((CLOG_DEBUG1 "binding listen socket"));
|
|
listen->bind(m_config.getSynergyAddress());
|
|
break;
|
|
}
|
|
catch (XSocketAddressInUse& e) {
|
|
setStatus(kError, e.what());
|
|
LOG((CLOG_WARN "bind failed: %s", e.what()));
|
|
|
|
// give up if we've waited too long
|
|
if (timer.getTime() >= m_bindTimeout) {
|
|
LOG((CLOG_ERR "waited too long to bind, giving up"));
|
|
throw;
|
|
}
|
|
|
|
// wait a bit before retrying
|
|
ARCH->sleep(5.0);
|
|
}
|
|
}
|
|
|
|
// accept connections and begin processing them
|
|
setStatus(kRunning);
|
|
LOG((CLOG_DEBUG1 "waiting for client connections"));
|
|
for (;;) {
|
|
// accept connection
|
|
CThread::testCancel();
|
|
IDataSocket* socket = listen->accept();
|
|
LOG((CLOG_NOTE "accepted client connection"));
|
|
CThread::testCancel();
|
|
|
|
// start handshake thread
|
|
startThread(new TMethodJob<CServer>(
|
|
this, &CServer::runClient, socket));
|
|
}
|
|
}
|
|
catch (XBase& e) {
|
|
setStatus(kError, e.what());
|
|
LOG((CLOG_ERR "cannot listen for clients: %s", e.what()));
|
|
delete listen;
|
|
exitMainLoopWithError();
|
|
}
|
|
catch (...) {
|
|
setStatus(kNotRunning);
|
|
delete listen;
|
|
throw;
|
|
}
|
|
}
|
|
|
|
void
|
|
CServer::runClient(void* vsocket)
|
|
{
|
|
// get the socket pointer from the argument
|
|
assert(vsocket != NULL);
|
|
IDataSocket* socket = reinterpret_cast<IDataSocket*>(vsocket);
|
|
|
|
// create proxy
|
|
CClientProxy* proxy = NULL;
|
|
try {
|
|
proxy = handshakeClient(socket);
|
|
if (proxy == NULL) {
|
|
delete socket;
|
|
return;
|
|
}
|
|
}
|
|
catch (...) {
|
|
delete socket;
|
|
throw;
|
|
}
|
|
|
|
// add the connection
|
|
try {
|
|
addConnection(proxy);
|
|
|
|
// save this client's thread
|
|
CLock lock(&m_mutex);
|
|
m_clientThreads.insert(std::make_pair(proxy->getName(),
|
|
CThread::getCurrentThread()));
|
|
|
|
// send configuration options
|
|
sendOptions(proxy);
|
|
}
|
|
catch (XDuplicateClient& e) {
|
|
// client has duplicate name
|
|
LOG((CLOG_WARN "a client with name \"%s\" is already connected", e.getName().c_str()));
|
|
try {
|
|
CProtocolUtil::writef(proxy->getOutputStream(), kMsgEBusy);
|
|
}
|
|
catch (XSocket&) {
|
|
// ignore
|
|
}
|
|
delete proxy;
|
|
delete socket;
|
|
return;
|
|
}
|
|
catch (XUnknownClient& e) {
|
|
// client has unknown name
|
|
LOG((CLOG_WARN "a client with name \"%s\" is not in the map", e.getName().c_str()));
|
|
try {
|
|
CProtocolUtil::writef(proxy->getOutputStream(), kMsgEUnknown);
|
|
}
|
|
catch (XSocket&) {
|
|
// ignore
|
|
}
|
|
delete proxy;
|
|
delete socket;
|
|
return;
|
|
}
|
|
catch (...) {
|
|
delete proxy;
|
|
delete socket;
|
|
throw;
|
|
}
|
|
|
|
// activate screen saver on new client if active on the primary screen
|
|
{
|
|
CLock lock(&m_mutex);
|
|
if (m_activeSaver != NULL) {
|
|
proxy->screensaver(true);
|
|
}
|
|
}
|
|
|
|
// handle client messages
|
|
try {
|
|
LOG((CLOG_NOTE "client \"%s\" has connected", proxy->getName().c_str()));
|
|
proxy->mainLoop();
|
|
}
|
|
catch (XBadClient&) {
|
|
// client not behaving
|
|
LOG((CLOG_WARN "protocol error from client \"%s\"", proxy->getName().c_str()));
|
|
try {
|
|
CProtocolUtil::writef(proxy->getOutputStream(), kMsgEBad);
|
|
}
|
|
catch (XSocket&) {
|
|
// ignore. client probably aborted the connection.
|
|
}
|
|
}
|
|
catch (XBase& e) {
|
|
// misc error
|
|
LOG((CLOG_WARN "error communicating with client \"%s\": %s", proxy->getName().c_str(), e.what()));
|
|
}
|
|
catch (...) {
|
|
// mainLoop() was probably cancelled
|
|
removeConnection(proxy->getName());
|
|
delete socket;
|
|
throw;
|
|
}
|
|
|
|
// clean up
|
|
removeConnection(proxy->getName());
|
|
delete socket;
|
|
}
|
|
|
|
CClientProxy*
|
|
CServer::handshakeClient(IDataSocket* socket)
|
|
{
|
|
LOG((CLOG_DEBUG1 "negotiating with new client"));
|
|
|
|
// get the input and output streams
|
|
IInputStream* input = socket->getInputStream();
|
|
IOutputStream* output = socket->getOutputStream();
|
|
bool own = false;
|
|
|
|
// attach filters
|
|
if (m_streamFilterFactory != NULL) {
|
|
input = m_streamFilterFactory->createInput(input, own);
|
|
output = m_streamFilterFactory->createOutput(output, own);
|
|
own = true;
|
|
}
|
|
|
|
// attach the packetizing filters
|
|
input = new CInputPacketStream(input, own);
|
|
output = new COutputPacketStream(output, own);
|
|
own = true;
|
|
|
|
CClientProxy* proxy = NULL;
|
|
CString name("<unknown>");
|
|
try {
|
|
// give the client a limited time to complete the handshake
|
|
CTimerThread timer(30.0);
|
|
|
|
// say hello
|
|
LOG((CLOG_DEBUG1 "saying hello"));
|
|
CProtocolUtil::writef(output, kMsgHello,
|
|
kProtocolMajorVersion,
|
|
kProtocolMinorVersion);
|
|
output->flush();
|
|
|
|
// wait for the reply
|
|
LOG((CLOG_DEBUG1 "waiting for hello reply"));
|
|
UInt32 n = input->getSize();
|
|
|
|
// limit the maximum length of the hello
|
|
if (n > kMaxHelloLength) {
|
|
throw XBadClient();
|
|
}
|
|
|
|
// get and parse the reply to hello
|
|
SInt16 major, minor;
|
|
try {
|
|
LOG((CLOG_DEBUG1 "parsing hello reply"));
|
|
CProtocolUtil::readf(input, kMsgHelloBack,
|
|
&major, &minor, &name);
|
|
}
|
|
catch (XIO&) {
|
|
throw XBadClient();
|
|
}
|
|
|
|
// disallow invalid version numbers
|
|
if (major <= 0 || minor < 0) {
|
|
throw XIncompatibleClient(major, minor);
|
|
}
|
|
|
|
// convert name to canonical form (if any)
|
|
if (m_config.isScreen(name)) {
|
|
name = m_config.getCanonicalName(name);
|
|
}
|
|
|
|
// create client proxy for highest version supported by the client
|
|
LOG((CLOG_DEBUG1 "creating proxy for client \"%s\" version %d.%d", name.c_str(), major, minor));
|
|
if (major == 1) {
|
|
switch (minor) {
|
|
case 0:
|
|
proxy = new CClientProxy1_0(this, name, input, output);
|
|
break;
|
|
|
|
case 1:
|
|
proxy = new CClientProxy1_1(this, name, input, output);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// hangup (with error) if version isn't supported
|
|
if (proxy == NULL) {
|
|
throw XIncompatibleClient(major, minor);
|
|
}
|
|
|
|
// negotiate
|
|
// FIXME
|
|
|
|
// ask and wait for the client's info
|
|
LOG((CLOG_DEBUG1 "waiting for info for client \"%s\"", name.c_str()));
|
|
proxy->open();
|
|
|
|
return proxy;
|
|
}
|
|
catch (XIncompatibleClient& e) {
|
|
// client is incompatible
|
|
LOG((CLOG_WARN "client \"%s\" has incompatible version %d.%d)", name.c_str(), e.getMajor(), e.getMinor()));
|
|
try {
|
|
CProtocolUtil::writef(output, kMsgEIncompatible,
|
|
kProtocolMajorVersion, kProtocolMinorVersion);
|
|
}
|
|
catch (XSocket&) {
|
|
// ignore
|
|
}
|
|
}
|
|
catch (XBadClient&) {
|
|
// client not behaving
|
|
LOG((CLOG_WARN "protocol error from client \"%s\"", name.c_str()));
|
|
try {
|
|
CProtocolUtil::writef(output, kMsgEBad);
|
|
}
|
|
catch (XSocket&) {
|
|
// ignore. client probably aborted the connection.
|
|
}
|
|
}
|
|
catch (XBase& e) {
|
|
// misc error
|
|
LOG((CLOG_WARN "error communicating with client \"%s\": %s", name.c_str(), e.what()));
|
|
}
|
|
catch (...) {
|
|
// probably timed out
|
|
if (proxy != NULL) {
|
|
delete proxy;
|
|
}
|
|
else if (own) {
|
|
delete input;
|
|
delete output;
|
|
}
|
|
throw;
|
|
}
|
|
|
|
// failed
|
|
if (proxy != NULL) {
|
|
delete proxy;
|
|
}
|
|
else if (own) {
|
|
delete input;
|
|
delete output;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
void
|
|
CServer::acceptHTTPClients(void*)
|
|
{
|
|
LOG((CLOG_DEBUG1 "starting to wait for HTTP clients"));
|
|
|
|
IListenSocket* listen = NULL;
|
|
try {
|
|
// create socket listener
|
|
listen = new CTCPListenSocket;
|
|
|
|
// bind to the desired port. keep retrying if we can't bind
|
|
// the address immediately.
|
|
CStopwatch timer;
|
|
for (;;) {
|
|
try {
|
|
LOG((CLOG_DEBUG1 "binding HTTP listen socket"));
|
|
listen->bind(m_config.getHTTPAddress());
|
|
break;
|
|
}
|
|
catch (XSocketBind& e) {
|
|
LOG((CLOG_DEBUG1 "bind HTTP failed: %s", e.what()));
|
|
|
|
// give up if we've waited too long
|
|
if (timer.getTime() >= m_bindTimeout) {
|
|
LOG((CLOG_DEBUG1 "waited too long to bind HTTP, giving up"));
|
|
throw;
|
|
}
|
|
|
|
// wait a bit before retrying
|
|
ARCH->sleep(5.0);
|
|
}
|
|
}
|
|
|
|
// accept connections and begin processing them
|
|
LOG((CLOG_DEBUG1 "waiting for HTTP connections"));
|
|
for (;;) {
|
|
// limit the number of HTTP requests being handled at once
|
|
{
|
|
CLock lock(&m_httpAvailable);
|
|
while (m_httpAvailable == 0) {
|
|
m_httpAvailable.wait();
|
|
}
|
|
assert(m_httpAvailable > 0);
|
|
m_httpAvailable = m_httpAvailable - 1;
|
|
}
|
|
|
|
// accept connection
|
|
CThread::testCancel();
|
|
IDataSocket* socket = listen->accept();
|
|
LOG((CLOG_NOTE "accepted HTTP connection"));
|
|
CThread::testCancel();
|
|
|
|
// handle HTTP request
|
|
startThread(new TMethodJob<CServer>(
|
|
this, &CServer::processHTTPRequest, socket));
|
|
}
|
|
|
|
// clean up
|
|
delete listen;
|
|
}
|
|
catch (XBase& e) {
|
|
LOG((CLOG_ERR "cannot listen for HTTP clients: %s", e.what()));
|
|
delete listen;
|
|
exitMainLoopWithError();
|
|
}
|
|
catch (...) {
|
|
delete listen;
|
|
throw;
|
|
}
|
|
}
|
|
|
|
void
|
|
CServer::processHTTPRequest(void* vsocket)
|
|
{
|
|
IDataSocket* socket = reinterpret_cast<IDataSocket*>(vsocket);
|
|
try {
|
|
// process the request and force delivery
|
|
m_httpServer->processRequest(socket);
|
|
socket->getOutputStream()->flush();
|
|
|
|
// wait a moment to give the client a chance to hangup first
|
|
ARCH->sleep(3.0);
|
|
|
|
// clean up
|
|
socket->close();
|
|
delete socket;
|
|
|
|
// increment available HTTP handlers
|
|
{
|
|
CLock lock(&m_httpAvailable);
|
|
m_httpAvailable = m_httpAvailable + 1;
|
|
m_httpAvailable.signal();
|
|
}
|
|
}
|
|
catch (...) {
|
|
delete socket;
|
|
{
|
|
CLock lock(&m_httpAvailable);
|
|
m_httpAvailable = m_httpAvailable + 1;
|
|
m_httpAvailable.signal();
|
|
}
|
|
throw;
|
|
}
|
|
}
|
|
|
|
void
|
|
CServer::sendOptions(IClient* client) const
|
|
{
|
|
// note -- must be locked on entry
|
|
|
|
COptionsList optionsList;
|
|
|
|
// look up options for client
|
|
const CConfig::CScreenOptions* options =
|
|
m_config.getOptions(client->getName());
|
|
if (options != NULL && options->size() > 0) {
|
|
// convert options to a more convenient form for sending
|
|
optionsList.reserve(2 * options->size());
|
|
for (CConfig::CScreenOptions::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 && options->size() > 0) {
|
|
// convert options to a more convenient form for sending
|
|
optionsList.reserve(optionsList.size() + 2 * options->size());
|
|
for (CConfig::CScreenOptions::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->setOptions(optionsList);
|
|
}
|
|
|
|
void
|
|
CServer::openPrimaryScreen()
|
|
{
|
|
assert(m_primaryClient == NULL);
|
|
|
|
// reset sequence number
|
|
m_seqNum = 0;
|
|
|
|
// canonicalize the primary screen name
|
|
CString primaryName = m_config.getCanonicalName(getPrimaryScreenName());
|
|
if (primaryName.empty()) {
|
|
throw XUnknownClient(getPrimaryScreenName());
|
|
}
|
|
|
|
// clear clipboards
|
|
for (ClipboardID id = 0; id < kClipboardEnd; ++id) {
|
|
CClipboardInfo& 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();
|
|
}
|
|
|
|
try {
|
|
// create the primary client
|
|
m_primaryClient = new CPrimaryClient(m_screenFactory,
|
|
this, this, primaryName);
|
|
|
|
// add connection
|
|
addConnection(m_primaryClient);
|
|
m_active = m_primaryClient;
|
|
|
|
// open the screen
|
|
LOG((CLOG_DEBUG1 "opening primary screen"));
|
|
m_primaryClient->open();
|
|
|
|
// tell it about the active sides
|
|
m_primaryClient->reconfigure(getActivePrimarySides());
|
|
|
|
// tell primary client about its options
|
|
sendOptions(m_primaryClient);
|
|
}
|
|
catch (...) {
|
|
// if m_active is NULL then we haven't added the connection
|
|
// for the primary client so we don't try to remove it.
|
|
if (m_active != NULL) {
|
|
removeConnection(primaryName);
|
|
}
|
|
else {
|
|
delete m_primaryClient;
|
|
}
|
|
m_active = NULL;
|
|
m_primaryClient = NULL;
|
|
throw;
|
|
}
|
|
}
|
|
|
|
void
|
|
CServer::closePrimaryScreen()
|
|
{
|
|
assert(m_primaryClient != NULL);
|
|
|
|
// close the primary screen
|
|
try {
|
|
LOG((CLOG_DEBUG1 "closing primary screen"));
|
|
m_primaryClient->close();
|
|
}
|
|
catch (...) {
|
|
// ignore
|
|
}
|
|
|
|
// remove connection
|
|
removeConnection(m_primaryClient->getName());
|
|
m_primaryClient = NULL;
|
|
}
|
|
|
|
void
|
|
CServer::addConnection(IClient* client)
|
|
{
|
|
assert(client != NULL);
|
|
|
|
LOG((CLOG_DEBUG "adding connection \"%s\"", client->getName().c_str()));
|
|
|
|
{
|
|
CLock lock(&m_mutex);
|
|
|
|
// name must be in our configuration
|
|
if (!m_config.isScreen(client->getName())) {
|
|
throw XUnknownClient(client->getName());
|
|
}
|
|
|
|
// can only have one screen with a given name at any given time
|
|
if (m_clients.count(client->getName()) != 0) {
|
|
throw XDuplicateClient(client->getName());
|
|
}
|
|
|
|
// save screen info
|
|
m_clients.insert(std::make_pair(client->getName(), client));
|
|
LOG((CLOG_DEBUG "added connection \"%s\"", client->getName().c_str()));
|
|
}
|
|
runStatusJobs();
|
|
}
|
|
|
|
void
|
|
CServer::removeConnection(const CString& name)
|
|
{
|
|
LOG((CLOG_DEBUG "removing connection \"%s\"", name.c_str()));
|
|
bool updateStatus;
|
|
{
|
|
CLock lock(&m_mutex);
|
|
|
|
// find client
|
|
CClientList::iterator index = m_clients.find(name);
|
|
assert(index != m_clients.end());
|
|
|
|
// if this is active screen then we have to jump off of it
|
|
IClient* active = (m_activeSaver != NULL) ? m_activeSaver : m_active;
|
|
if (active == index->second && active != m_primaryClient) {
|
|
// record new position (center of primary screen)
|
|
m_primaryClient->getCursorCenter(m_x, m_y);
|
|
|
|
// stop waiting to switch if we were
|
|
if (active == m_switchScreen) {
|
|
clearSwitchState();
|
|
}
|
|
|
|
// don't notify active screen since it probably already
|
|
// disconnected.
|
|
LOG((CLOG_INFO "jump from \"%s\" to \"%s\" at %d,%d", active->getName().c_str(), m_primaryClient->getName().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 == index->second) {
|
|
m_activeSaver = NULL;
|
|
}
|
|
|
|
// done with client
|
|
delete index->second;
|
|
m_clients.erase(index);
|
|
|
|
// remove any thread for this client
|
|
m_clientThreads.erase(name);
|
|
|
|
updateStatus = (m_clients.size() <= 1);
|
|
}
|
|
|
|
if (updateStatus) {
|
|
runStatusJobs();
|
|
}
|
|
}
|
|
|
|
|
|
//
|
|
// CServer::CClipboardInfo
|
|
//
|
|
|
|
CString
|
|
CServer::XServerRethrow::getWhat() const throw()
|
|
{
|
|
return format("XServerRethrow", "child thread failed");
|
|
}
|
|
|
|
|
|
//
|
|
// CServer::CClipboardInfo
|
|
//
|
|
|
|
CServer::CClipboardInfo::CClipboardInfo() :
|
|
m_clipboard(),
|
|
m_clipboardData(),
|
|
m_clipboardOwner(),
|
|
m_clipboardSeqNum(0)
|
|
{
|
|
// do nothing
|
|
}
|