1 /*
2  * Stellarium Remote Sync plugin
3  * Copyright (C) 2015 Florian Schaukowitsch
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License
7  * as published by the Free Software Foundation; either version 2
8  * of the License, or (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA  02110-1335, USA.
18  */
19 
20 #include "RemoteSync.hpp"
21 #include "RemoteSyncDialog.hpp"
22 
23 #include "SyncServer.hpp"
24 #include "SyncClient.hpp"
25 
26 #include "CLIProcessor.hpp"
27 #include "StelUtils.hpp"
28 #include "StelApp.hpp"
29 #include "StelCore.hpp"
30 #include "StelModuleMgr.hpp"
31 
32 #include <QApplication>
33 #include <QDebug>
34 #include <QSettings>
35 
36 #include <stdexcept>
37 
38 Q_LOGGING_CATEGORY(remoteSync,"stel.plugin.remoteSync")
39 
40 //! This method is the one called automatically by the StelModuleMgr just after loading the dynamic library
getStelModule() const41 StelModule* RemoteSyncStelPluginInterface::getStelModule() const
42 {
43 	return new RemoteSync();
44 }
45 
getPluginInfo() const46 StelPluginInfo RemoteSyncStelPluginInterface::getPluginInfo() const
47 {
48 	// Allow to load the resources when used as a static plugin
49 	Q_INIT_RESOURCE(RemoteSync);
50 
51 	StelPluginInfo info;
52 	info.id = "RemoteSync";
53 	info.displayedName = N_("Remote Sync");
54 	info.authors = "Florian Schaukowitsch and Georg Zotti";
55 	info.contact = "http://homepage.univie.ac.at/Georg.Zotti";
56 	info.description = N_("Provides state synchronization for multiple Stellarium instances running in a network. See manual for detailed description.");
57 	info.acknowledgements = N_("This plugin was created in the 2015/2016 campaigns of the ESA Summer of Code in Space programme.");
58 	info.version = REMOTESYNC_PLUGIN_VERSION;
59 	info.license = REMOTESYNC_PLUGIN_LICENSE;
60 	return info;
61 }
62 
63 
64 // A list that holds properties that cannot be sync'ed for technical reasons.
65 // Currently only HipsMgr.surveys cannot be synchronized.
66 QStringList RemoteSync::propertyBlacklist;
67 
68 
RemoteSync()69 RemoteSync::RemoteSync()
70 	: clientServerPort(20180)
71 	, serverPort(20180)
72 	, connectionLostBehavior(ClientBehavior::RECONNECT)
73 	, quitBehavior(ClientBehavior::NONE)
74 	, state(IDLE)
75 	, server(Q_NULLPTR)
76 	, client(Q_NULLPTR)
77 	, allowVersionMismatch(false)
78 {
79 	setObjectName("RemoteSync");
80 
81 	configDialog = new RemoteSyncDialog();
82 	conf = StelApp::getInstance().getSettings();
83 
84 	reconnectTimer.setSingleShot(true);
85 	connect(&reconnectTimer, SIGNAL(timeout()), this, SLOT(connectToServer()));
86 
87 	// There are a few unsynchronizable properties. They must be listed here!
88 	propertyBlacklist.push_back("HipsMgr.surveys");
89 }
90 
~RemoteSync()91 RemoteSync::~RemoteSync()
92 {
93 	delete configDialog;
94 }
95 
configureGui(bool show)96 bool RemoteSync::configureGui(bool show)
97 {
98 	if (show)
99 		configDialog->setVisible(true);
100 	return true;
101 }
102 
argsGetOptionWithArg(const QStringList & args,QString shortOpt,QString longOpt,QVariant defaultValue)103 QVariant RemoteSync::argsGetOptionWithArg(const QStringList& args, QString shortOpt, QString longOpt, QVariant defaultValue)
104 {
105 	// Don't see anything after a -- as an option
106 	int lastOptIdx = args.indexOf("--");
107 	if (lastOptIdx == -1)
108 		lastOptIdx = args.size();
109 
110 	for (int i=0; i<lastOptIdx; i++)
111 	{
112 		bool match(false);
113 		QString argStr;
114 
115 		// form -n=arg
116 		if ((!shortOpt.isEmpty() && args.at(i).left(shortOpt.length()+1)==shortOpt+"="))
117 		{
118 			match=true;
119 			argStr=args.at(i).right(args.at(i).length() - shortOpt.length() - 1);
120 		}
121 		// form --number=arg
122 		else if (args.at(i).left(longOpt.length()+1)==longOpt+"=")
123 		{
124 			match=true;
125 			argStr=args.at(i).right(args.at(i).length() - longOpt.length() - 1);
126 		}
127 		// forms -n arg and --number arg
128 		else if ((!shortOpt.isEmpty() && args.at(i)==shortOpt) || args.at(i)==longOpt)
129 		{
130 			if (i+1>=lastOptIdx)
131 			{
132 				// i.e., option given as last option, but without arguments. Last chance: default value!
133 				if (defaultValue.isValid())
134 				{
135 					return defaultValue;
136 				}
137 				else
138 				{
139 					throw (std::runtime_error(qPrintable("optarg_missing ("+longOpt+")")));
140 				}
141 			}
142 			else
143 			{
144 				match=true;
145 				argStr=args.at(i+1);
146 				i++;  // skip option argument in next iteration
147 			}
148 		}
149 
150 		if (match)
151 		{
152 			return QVariant(argStr);
153 		}
154 	}
155 	return defaultValue;
156 }
157 
init()158 void RemoteSync::init()
159 {
160 	if (!conf->childGroups().contains("RemoteSync"))
161 		restoreDefaultSettings();
162 
163 	loadSettings();
164 
165 	qCDebug(remoteSync)<<"Plugin initialized";
166 
167 	//parse command line args
168 	QStringList args = StelApp::getCommandlineArguments();
169 	QString syncMode = argsGetOptionWithArg(args,"","--syncMode","").toString();
170 	QString syncHost = argsGetOptionWithArg(args,"","--syncHost","").toString();
171 	int syncPort = argsGetOptionWithArg(args,"","--syncPort",0).toInt();
172 
173 	if(syncMode=="server")
174 	{
175 		if(syncPort!=0)
176 			setServerPort(syncPort);
177 		qCDebug(remoteSync)<<"Starting server from command line";
178 		startServer();
179 	}
180 	else if(syncMode=="client")
181 	{
182 		if(!syncHost.isEmpty())
183 			setClientServerHost(syncHost);
184 		if(syncPort!=0)
185 			setClientServerPort(syncPort);
186 		qCDebug(remoteSync)<<"Connecting to server from command line";
187 		connectToServer();
188 	}
189 
190 	connect(StelApp::getInstance().getCore(), SIGNAL(configurationDataSaved()), this, SLOT(saveSettings()));
191 }
192 
update(double deltaTime)193 void RemoteSync::update(double deltaTime)
194 {
195 	Q_UNUSED(deltaTime)
196 	if(server)
197 	{
198 		//pass update on to server, client does not need this
199 		server->update();
200 	}
201 }
202 
getCallOrder(StelModuleActionName actionName) const203 double RemoteSync::getCallOrder(StelModuleActionName actionName) const
204 {
205 	//we want update() to be called as late as possible
206 	if(actionName == ActionUpdate)
207 		return 100000.0;
208 
209 	return StelModule::getCallOrder(actionName);
210 }
211 
212 
setClientServerHost(const QString & clientServerHost)213 void RemoteSync::setClientServerHost(const QString &clientServerHost)
214 {
215 	if(clientServerHost != this->clientServerHost)
216 	{
217 		this->clientServerHost = clientServerHost;
218 		emit clientServerHostChanged(clientServerHost);
219 	}
220 }
221 
setClientServerPort(const int port)222 void RemoteSync::setClientServerPort(const int port)
223 {
224 	if(port != this->clientServerPort)
225 	{
226 		this->clientServerPort = static_cast<quint16>(port);
227 		emit clientServerPortChanged(port);
228 	}
229 }
230 
setServerPort(const int port)231 void RemoteSync::setServerPort(const int port)
232 {
233 	if(port!= serverPort)
234 	{
235 		serverPort = static_cast<quint16>(port);
236 		emit serverPortChanged(port);
237 	}
238 }
239 
setClientSyncOptions(SyncClient::SyncOptions options)240 void RemoteSync::setClientSyncOptions(SyncClient::SyncOptions options)
241 {
242 	if(options!=syncOptions)
243 	{
244 		syncOptions = options;
245 		emit clientSyncOptionsChanged(options);
246 	}
247 }
248 
setStelPropFilter(const QStringList & stelPropFilter)249 void RemoteSync::setStelPropFilter(const QStringList &stelPropFilter)
250 {
251 	if(stelPropFilter!=this->stelPropFilter)
252 	{
253 		this->stelPropFilter = stelPropFilter;
254 		emit stelPropFilterChanged(stelPropFilter);
255 	}
256 }
257 
setConnectionLostBehavior(const ClientBehavior bh)258 void RemoteSync::setConnectionLostBehavior(const ClientBehavior bh)
259 {
260 	if(connectionLostBehavior!=bh)
261 	{
262 		connectionLostBehavior = bh;
263 		emit connectionLostBehaviorChanged(bh);
264 	}
265 }
266 
setQuitBehavior(const ClientBehavior bh)267 void RemoteSync::setQuitBehavior(const ClientBehavior bh)
268 {
269 	if(quitBehavior!=bh)
270 	{
271 		quitBehavior = bh;
272 		emit quitBehaviorChanged(bh);
273 	}
274 }
275 
startServer()276 void RemoteSync::startServer()
277 {
278 	if(state == IDLE)
279 	{
280 		server = new SyncServer(this, allowVersionMismatch);
281 		if(server->start(serverPort))
282 			setState(SERVER);
283 		else
284 		{
285 			setError(server->errorString());
286 			delete server;
287 			server = Q_NULLPTR;
288 		}
289 	}
290 	else
291 		qCWarning(remoteSync)<<"startServer: invalid state";
292 }
293 
stopServer()294 void RemoteSync::stopServer()
295 {
296 	if(state == SERVER)
297 	{
298 		connect(server, SIGNAL(serverStopped()), server, SLOT(deleteLater()));
299 		server->stop();
300 		server = Q_NULLPTR;
301 		setState(IDLE);
302 	}
303 	else
304 		qCWarning(remoteSync)<<"stopServer: invalid state";
305 }
306 
connectToServer()307 void RemoteSync::connectToServer()
308 {
309 	if(state == IDLE || state == CLIENT_WAIT_RECONNECT)
310 	{
311 		client = new SyncClient(syncOptions, stelPropFilter, this);
312 		connect(client, SIGNAL(connected()), this, SLOT(clientConnected()));
313 		connect(client, SIGNAL(disconnected(bool)), this, SLOT(clientDisconnected(bool)));
314 		setState(CLIENT_CONNECTING);
315 		client->connectToServer(clientServerHost,clientServerPort);
316 	}
317 	else
318 		qCWarning(remoteSync)<<"connectToServer: invalid state";
319 }
320 
clientConnected()321 void RemoteSync::clientConnected()
322 {
323 	Q_ASSERT(state == CLIENT_CONNECTING);
324 	setState(CLIENT);
325 }
326 
clientDisconnected(bool clean)327 void RemoteSync::clientDisconnected(bool clean)
328 {
329 	QString errStr = client->errorString();
330 	client->deleteLater();
331 	client = Q_NULLPTR;
332 
333 	if(!clean)
334 	{
335 		setError(errStr);
336 	}
337 
338 	setState(applyClientBehavior(clean ? quitBehavior : connectionLostBehavior));
339 }
340 
applyClientBehavior(ClientBehavior bh)341 RemoteSync::SyncState RemoteSync::applyClientBehavior(ClientBehavior bh)
342 {
343 	if(state!=CLIENT_CLOSING) //when client closes we do nothing
344 	{
345 		switch (bh) {
346 			case RECONNECT:
347 				reconnectTimer.start();
348 				return CLIENT_WAIT_RECONNECT;
349 			case QUIT:
350 				StelApp::getInstance().quit();
351 				break;
352 			default:
353 				break;
354 		}
355 	}
356 
357 	return IDLE;
358 }
359 
disconnectFromServer()360 void RemoteSync::disconnectFromServer()
361 {
362 	if(state == CLIENT)
363 	{
364 		setState(CLIENT_CLOSING);
365 		client->disconnectFromServer();
366 	}
367 	else if(state == CLIENT_WAIT_RECONNECT)
368 	{
369 		reconnectTimer.stop();
370 		setState(IDLE);
371 	}
372 	else
373 		qCWarning(remoteSync)<<"disconnectFromServer: invalid state"<<state;
374 }
375 
restoreDefaultSettings()376 void RemoteSync::restoreDefaultSettings()
377 {
378 	Q_ASSERT(conf);
379 	// Remove the old values...
380 	conf->remove("RemoteSync");
381 	// ...load the default values...
382 	loadSettings();
383 	// ...and then save them.
384 	saveSettings();
385 }
386 
loadSettings()387 void RemoteSync::loadSettings()
388 {
389 	conf->beginGroup("RemoteSync");
390 	setClientServerHost(conf->value("clientServerHost","127.0.0.1").toString());
391 	setClientServerPort(static_cast<quint16>(conf->value("clientServerPort",20180).toUInt()));
392 	setServerPort(static_cast<quint16>(conf->value("serverPort",20180).toUInt()));
393 	setClientSyncOptions(SyncClient::SyncOptions(conf->value("clientSyncOptions", SyncClient::ALL).toInt()));
394 	setStelPropFilter(unpackStringList(conf->value("stelPropFilter").toString()));
395 	setConnectionLostBehavior(static_cast<ClientBehavior>(conf->value("connectionLostBehavior",1).toInt()));
396 	setQuitBehavior(static_cast<ClientBehavior>(conf->value("quitBehavior").toInt()));
397 	reconnectTimer.setInterval(conf->value("clientReconnectInterval", 5000).toInt());
398 	allowVersionMismatch=conf->value("allowVersionMismatch", false).toBool();
399 	conf->endGroup();
400 }
401 
saveSettings()402 void RemoteSync::saveSettings()
403 {
404 	conf->beginGroup("RemoteSync");
405 	conf->setValue("clientServerHost",clientServerHost);
406 	conf->setValue("clientServerPort",clientServerPort);
407 	conf->setValue("serverPort",serverPort);
408 	conf->setValue("clientSyncOptions",static_cast<int>(syncOptions));
409 	conf->setValue("stelPropFilter", packStringList(stelPropFilter));
410 	conf->setValue("connectionLostBehavior", connectionLostBehavior);
411 	conf->setValue("quitBehavior", quitBehavior);
412 	conf->setValue("clientReconnectInterval", reconnectTimer.interval());
413 	conf->setValue("allowVersionMismatch", allowVersionMismatch);
414 	conf->endGroup();
415 }
416 
packStringList(const QStringList props)417 QString RemoteSync::packStringList(const QStringList props)
418 {
419 	return props.join("|");
420 }
421 
unpackStringList(const QString packedProps)422 QStringList RemoteSync::unpackStringList(const QString packedProps)
423 {
424 	return packedProps.split("|");
425 }
426 
setState(RemoteSync::SyncState state)427 void RemoteSync::setState(RemoteSync::SyncState state)
428 {
429 	if(state != this->state)
430 	{
431 		this->state = state;
432 		qCDebug(remoteSync)<<"New state:"<<state;
433 		emit stateChanged(state);
434 	}
435 }
436 
setError(const QString & errorString)437 void RemoteSync::setError(const QString &errorString)
438 {
439 	this->errorString = errorString;
440 	emit errorOccurred(errorString);
441 }
442 
operator <<(QDebug deb,RemoteSync::SyncState state)443 QDebug operator<<(QDebug deb, RemoteSync::SyncState state)
444 {
445 	switch (state) {
446 		case RemoteSync::IDLE:
447 			deb<<"IDLE";
448 			break;
449 		case RemoteSync::SERVER:
450 			deb<<"SERVER";
451 			break;
452 		case RemoteSync::CLIENT:
453 			deb<<"CLIENT";
454 			break;
455 		case RemoteSync::CLIENT_CONNECTING:
456 			deb<<"CLIENT_CONNECTING";
457 			break;
458 		case RemoteSync::CLIENT_WAIT_RECONNECT:
459 			deb<<"CLIENT_WAIT_RECONNECT";
460 			break;
461 		default:
462 			deb<<"RemoteSync::SyncState(" <<int(state)<<')';
463 			break;
464 	}
465 
466 	return deb;
467 }
468 
isPropertyBlacklisted(const QString & name)469 bool RemoteSync::isPropertyBlacklisted(const QString &name)
470 {
471 	return propertyBlacklist.contains(name);
472 }
473