1 /*
2 Copyright (c) 2020, Lukas Holecek <hluk@email.cz>
3
4 This file is part of CopyQ.
5
6 CopyQ is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
10
11 CopyQ is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with CopyQ. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20 #include "app/app.h"
21 #include "app/applicationexceptionhandler.h"
22 #include "app/clipboardclient.h"
23 #include "app/clipboardserver.h"
24 #include "common/commandstatus.h"
25 #include "common/log.h"
26 #include "common/messagehandlerforqt.h"
27 #include "common/textdata.h"
28 #include "platform/platformnativeinterface.h"
29 #ifdef Q_OS_UNIX
30 # include "platform/unix/unixsignalhandler.h"
31 #endif
32 #include "scriptable/scriptable.h"
33
34 #include <QApplication>
35 #include <QFile>
36 #include <QJSEngine>
37 #include <QProcess>
38 #include <QSettings>
39
40 #ifdef HAS_TESTS
41 # include "tests/tests.h"
42 #endif // HAS_TESTS
43
44 #include <exception>
45
46 Q_DECLARE_METATYPE(QByteArray*)
47
48 namespace {
49
evaluate(const QString & functionName,const QStringList & arguments,int argc,char ** argv,const QString & sessionName)50 int evaluate(
51 const QString &functionName,
52 const QStringList &arguments, int argc, char **argv,
53 const QString &sessionName)
54 {
55 App app( platformNativeInterface()->createConsoleApplication(argc, argv), sessionName );
56 setLogLabel("Prompt");
57
58 QJSEngine engine;
59 Scriptable scriptable(&engine, nullptr);
60
61 QJSValue function = engine.globalObject().property(functionName);
62 QJSValueList functionArguments;
63
64 functionArguments.reserve( arguments.size() );
65 for (const auto &argument : arguments)
66 functionArguments.append(argument);
67
68 const auto result = function.call(functionArguments);
69 const bool hasUncaughtException = result.isError() || scriptable.hasUncaughtException();
70
71 const auto output = scriptable.fromString(result.toString());
72 if ( !output.isEmpty() && canUseStandardOutput() ) {
73 QFile f;
74 if (hasUncaughtException)
75 f.open(stderr, QIODevice::WriteOnly);
76 else
77 f.open(stdout, QIODevice::WriteOnly);
78
79 f.write(output);
80 if ( !output.endsWith("\n") )
81 f.write("\n");
82 f.close();
83 }
84
85 const int exitCode = hasUncaughtException ? CommandException : 0;
86 app.exit(exitCode);
87 return exitCode;
88 }
89
containsOnlyValidCharacters(const QString & sessionName)90 bool containsOnlyValidCharacters(const QString &sessionName)
91 {
92 for (const auto &c : sessionName) {
93 if ( !c.isLetterOrNumber() && c != '-' && c != '_' )
94 return false;
95 }
96
97 return true;
98 }
99
isValidSessionName(const QString & sessionName)100 bool isValidSessionName(const QString &sessionName)
101 {
102 return !sessionName.isNull() &&
103 sessionName.length() < 16 &&
104 containsOnlyValidCharacters(sessionName);
105 }
106
restoreSessionName(const QString & sessionId)107 QString restoreSessionName(const QString &sessionId)
108 {
109 const QSettings settings(QSettings::IniFormat, QSettings::UserScope, "copyq", "copyq_no_session");
110 const auto sessionNameKey = "session_" + sessionId;
111 const auto sessionName = settings.value(sessionNameKey).toString();
112 return sessionName;
113 }
114
startServer(int argc,char * argv[],QString sessionName)115 int startServer(int argc, char *argv[], QString sessionName)
116 {
117 // By default, enable automatic screen scaling in Qt for high-DPI displays
118 // (this works better at least in Windows).
119 if ( qEnvironmentVariableIsEmpty("QT_AUTO_SCREEN_SCALE_FACTOR") )
120 qputenv("QT_AUTO_SCREEN_SCALE_FACTOR", "1");
121
122 auto qapp = platformNativeInterface()->createServerApplication(argc, argv);
123 if ( qapp->isSessionRestored() ) {
124 const auto sessionId = qapp->sessionId();
125 sessionName = restoreSessionName(sessionId);
126 COPYQ_LOG( QString("Restoring session ID \"%1\", session name \"%2\"")
127 .arg(sessionId, sessionName) );
128 if ( !sessionName.isEmpty() && !isValidSessionName(sessionName) ) {
129 log("Failed to restore session name", LogError);
130 return 2;
131 }
132 }
133
134 ClipboardServer app(qapp, sessionName);
135 return app.exec();
136 }
137
startServerInBackground(const QString & applicationPath,QString sessionName)138 void startServerInBackground(const QString &applicationPath, QString sessionName)
139 {
140 const bool couldUseStandardOutput = canUseStandardOutput();
141 if (couldUseStandardOutput)
142 qputenv("COPYQ_NO_OUTPUT", "1");
143
144 const QStringList arguments{QString::fromLatin1("-s"), sessionName};
145 const bool started = QProcess::startDetached(applicationPath, arguments);
146
147 if (!couldUseStandardOutput)
148 qunsetenv("COPYQ_NO_OUTPUT");
149
150 if (!started)
151 log( QLatin1String("Failed to start the server"), LogError );
152 }
153
startClient(int argc,char * argv[],const QStringList & arguments,const QString & sessionName)154 int startClient(int argc, char *argv[], const QStringList &arguments, const QString &sessionName)
155 {
156 ClipboardClient app(argc, argv, arguments, sessionName);
157 return app.exec();
158 }
159
needsHelp(const QString & arg)160 bool needsHelp(const QString &arg)
161 {
162 return arg == "-h" ||
163 arg == "--help" ||
164 arg == "help";
165 }
166
needsVersion(const QString & arg)167 bool needsVersion(const QString &arg)
168 {
169 return arg == "-v" ||
170 arg == "--version" ||
171 arg == "version";
172 }
173
needsInfo(const QString & arg)174 bool needsInfo(const QString &arg)
175 {
176 return arg == "--info" ||
177 arg == "info";
178 }
179
needsLogs(const QString & arg)180 bool needsLogs(const QString &arg)
181 {
182 return arg == "--logs" ||
183 arg == "logs";
184 }
185
needsStartServer(const QString & arg)186 bool needsStartServer(const QString &arg)
187 {
188 return arg == "--start-server";
189 }
190
191 #ifdef HAS_TESTS
needsTests(const QString & arg)192 bool needsTests(const QString &arg)
193 {
194 return arg == "--tests" ||
195 arg == "tests";
196 }
197 #endif
198
getSessionName(const QStringList & arguments,int * skipArguments)199 QString getSessionName(const QStringList &arguments, int *skipArguments)
200 {
201 const QString firstArgument = arguments.value(0);
202 *skipArguments = 0;
203
204 if (firstArgument == "-s" || firstArgument == "--session" || firstArgument == "session") {
205 *skipArguments = 2;
206 return arguments.value(1);
207 }
208
209 if ( firstArgument.startsWith("--session=") ) {
210 *skipArguments = 1;
211 return firstArgument.mid( firstArgument.indexOf('=') + 1 );
212 }
213
214 // Skip session arguments passed from session manager.
215 if (arguments.size() == 2 && firstArgument == "-session")
216 *skipArguments = 2;
217
218 return getTextData( qgetenv("COPYQ_SESSION_NAME") );
219 }
220
startApplication(int argc,char ** argv)221 int startApplication(int argc, char **argv)
222 {
223 installMessageHandlerForQt();
224
225 #ifdef Q_OS_UNIX
226 if ( !initUnixSignalHandler() )
227 log( QString("Failed to create handler for Unix signals!"), LogError );
228 #endif
229
230 const QStringList arguments =
231 platformNativeInterface()->getCommandLineArguments(argc, argv);
232
233 // Get session name (default is empty).
234 int skipArguments;
235 const QString sessionName = getSessionName(arguments, &skipArguments);
236
237 if ( !isValidSessionName(sessionName) ) {
238 log( QObject::tr("Session name must contain at most 16 characters\n"
239 "which can be letters, digits, '-' or '_'!"), LogError );
240 return 2;
241 }
242
243 // Print version, help or run tests.
244 if ( arguments.size() > skipArguments ) {
245 const auto arg = arguments[skipArguments];
246
247 if ( needsStartServer(arg) ) {
248 startServerInBackground( QString::fromUtf8(argv[0]), sessionName );
249 return skipArguments + 1 == arguments.size() ? 0
250 : startClient(argc, argv, arguments.mid(skipArguments + 1), sessionName);
251 }
252
253 if ( needsVersion(arg) )
254 return evaluate( "version", QStringList(), argc, argv, sessionName );
255
256 if ( needsHelp(arg) )
257 return evaluate( "help", arguments.mid(skipArguments + 1), argc, argv, sessionName );
258
259 if ( needsInfo(arg) )
260 return evaluate( "info", arguments.mid(skipArguments + 1), argc, argv, sessionName );
261
262 if ( needsLogs(arg) )
263 return evaluate( "logs", arguments.mid(skipArguments + 1), argc, argv, sessionName );
264
265 #ifdef HAS_TESTS
266 if ( needsTests(arg) ) {
267 // Skip the "tests" argument and pass the rest to tests.
268 return runTests(argc - skipArguments - 1, argv + skipArguments + 1);
269 }
270 #endif
271 }
272
273 // If server hasn't been run yet and no argument were specified
274 // then run this process as server.
275 if ( skipArguments == arguments.size() )
276 return startServer(argc, argv, sessionName);
277
278 // If argument was specified and server is running
279 // then run this process as client.
280 return startClient(argc, argv, arguments.mid(skipArguments), sessionName);
281 }
282
283 } // namespace
284
main(int argc,char ** argv)285 int main(int argc, char **argv)
286 {
287 try {
288 return startApplication(argc, argv);
289 } catch (const std::exception &e) {
290 logException(e.what());
291 throw;
292 } catch (...) {
293 logException();
294 throw;
295 }
296 }
297