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