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