diff --git a/tools/cutehmi.daemon.2/README.md b/tools/cutehmi.daemon.2/README.md index 4c086203..efef794a 100644 --- a/tools/cutehmi.daemon.2/README.md +++ b/tools/cutehmi.daemon.2/README.md @@ -1,95 +1,97 @@ # Daemon +**Obsolete** - this tool is being replaced by [cutehmi.daemon.3](../cutehmi.daemon.3/). + Console program, which allows one to run QML project in the background. Daemon mode is currently supported only on Linux. On Windows program can be run in application mode only (`--app` option). Any extension that does not provide graphical UI QML component can be loaded as *cutehmi.daemon.2* project. Use `--extension` command line argument to specify an extension. For example to run [Count Daemon](../../extensions/CuteHMI/Examples/CountDaemon.2/) example use following command. ``` cutehmi.daemon.2 --extension="CuteHMI.Examples.CountDaemon.2" ``` Read system logs to investigate whether daemon is running (e.g. `journalctl -n20` on a system with *systemd*). One may use `--app` option to tell the program to work as a foreground process (this can be useful when testing projects). For example following command runs [Count Daemon](../../extensions/CuteHMI/Examples/CountDaemon.2/) in application mode. ``` cutehmi.daemon.2 --extension="CuteHMI.Examples.CountDaemon.2" --app ``` Default loader picks `Main.qml` as default QML component to load. Component can be specified with `--component` option. One can also use `--init` option to replace default loader with custom one. You can use `--help` command line argument to see the list of all possible command line options. Setting empty path for PID file option (`--pidfile=`) disables creation of PID file. Fore debug builds use `cutehmi.daemon.2.debug` instead of `cutehmi.daemon.2`. ## Linux ### Signals Under Unix daemon will respond to signals in a following way. - SIGTERM tells daemon to gracefully quit with exit code set to EXIT_SUCCESS (0 on almost all systems). - SIGINT tells daemon to gracefully quit, but exit code will be set to 128 + signal code. - SIGQUIT causes violent termination and exits via abort. - SIGHUP attempts to reload the project as if daemon has been restarted. ### Forking Daemon is a program, which is supposed to work as a background process. This could be achieved in many ways, but lots of traditional UNIX daemons have been accomplishing it through techniques, which involve forking. Unfortunately this spots a controversy. It comes out there are three conflicting schools on how many forks daemon should perform. According to these daemon should fork once, twice or not fork at all... Detailed description on the topic is out of the scope of this document, but here are some major points. #### Single forking daemon Single fork is performed to exit the parent process and get rid of controlling terminal. After forking, daemon can become a new session leader, so that when user logs out from the session, process is not killed along with processes associated with that session. This is how [daemon()][3] function has been implemented. #### Double forking daemon After first fork process can still acquire controlling terminal if it opens tty according to System V rules [[1]]. Second fork prevents this [[2]]. Unfortunately this approach heavily obscures process hierarchy, because daemon continues as orphaned process. Notably *systemd* has troubles handling double-forking daemons as service units. [Documentation](https://www.freedesktop.org/software/systemd/man/systemd.service.html#Type=) of *systemd* is explicit about expected behaviour of daemon for forking service type. ``` If set to forking, it is expected that the process configured with ExecStart= will call fork() as part of its start-up. The parent process is expected to exit when start-up is complete and all communication channels are set up. The child continues to run as the main service process, and the service manager will consider the unit started when the parent process exits. ``` In double-forking approach the child won't continue as main service process, but rather its own child will do. This can deceive *systemd* to think that daemon finished. #### No forking Many people advice to not fork at all, as it is a domain of the user to detrmine the way how the program shall be run. By this view program should not daemonize itself. Instead it can be run as a background process (by putting `&` after the command) and tools such as *screen* and *nohup* can be used to prevent process from being killed when user logs out [[4]]. --- There are some valid points in each of these views, so *cutehmi.daemon.2* allows you to pick number of forks by setting `--nforks={number}` parameter. For *systemd* service pick `--nforks=1` and use `Type=forking` or `--nforks=0` with `Type=simple`. Note that `--nforks=0` is not the same as application mode (`--app` option) - daemon uses system logging facility instead of standard output, responds to signals, unlocks former working directory, creates PID file, closes file descriptors and resets its umask. ## References 1. [Stephen A. Rago, W. Richard Stevens, "Advanced Programming in the UNIX® Environment: Second Edition", Chapter 13. Daemon Processes.][1] 2. [Andries Brouwer, "The Linux kernel".][2] 3. [Linux Programmer's Manual, DAEMON(3)][3] 4. [Department of Health Technology, "Processes; foreground and background, ps, top, kill, screen, nohup and daemons".][4] [1]: https://learning.oreilly.com/library/view/advanced-programming-in/0201433079/ [2]: https://www.win.tue.nl/~aeb/linux/lk/lk-10.html [3]: http://man7.org/linux/man-pages/man3/daemon.3.html [4]: http://teaching.healthtech.dtu.dk/unix/index.php/Processes;_foreground_and_background,_ps,_top,_kill,_screen,_nohup_and_daemons diff --git a/tools/cutehmi.daemon.3/LICENSE b/tools/cutehmi.daemon.3/LICENSE new file mode 100644 index 00000000..0a041280 --- /dev/null +++ b/tools/cutehmi.daemon.3/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/tools/cutehmi.daemon.3/LICENSE.C.dslash.inc b/tools/cutehmi.daemon.3/LICENSE.C.dslash.inc new file mode 100644 index 00000000..3604b204 --- /dev/null +++ b/tools/cutehmi.daemon.3/LICENSE.C.dslash.inc @@ -0,0 +1,6 @@ + +//(c)C: Copyright © %YEAR%, %AUTHORS%. All rights reserved. +//(c)C: This file is a part of CuteHMI. +//(c)C: CuteHMI is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +//(c)C: CuteHMI 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 Lesser General Public License for more details. +//(c)C: You should have received a copy of the GNU Lesser General Public License along with CuteHMI. If not, see . diff --git a/tools/cutehmi.daemon.3/LICENSE.C.hash.inc b/tools/cutehmi.daemon.3/LICENSE.C.hash.inc new file mode 100644 index 00000000..daa303be --- /dev/null +++ b/tools/cutehmi.daemon.3/LICENSE.C.hash.inc @@ -0,0 +1,6 @@ + +#(c)C: Copyright © %YEAR%, %AUTHORS%. All rights reserved. +#(c)C: This file is a part of CuteHMI. +#(c)C: CuteHMI is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +#(c)C: CuteHMI 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 Lesser General Public License for more details. +#(c)C: You should have received a copy of the GNU Lesser General Public License along with CuteHMI. If not, see . diff --git a/tools/cutehmi.daemon.2/README.md b/tools/cutehmi.daemon.3/README.md similarity index 78% copy from tools/cutehmi.daemon.2/README.md copy to tools/cutehmi.daemon.3/README.md index 4c086203..7603ef90 100644 --- a/tools/cutehmi.daemon.2/README.md +++ b/tools/cutehmi.daemon.3/README.md @@ -1,95 +1,103 @@ # Daemon Console program, which allows one to run QML project in the background. Daemon mode is currently supported only on Linux. On Windows program can be run in application mode only (`--app` option). -Any extension that does not provide graphical UI QML component can be loaded as *cutehmi.daemon.2* project. Use `--extension` -command line argument to specify an extension. For example to run -[Count Daemon](../../extensions/CuteHMI/Examples/CountDaemon.2/) example use following command. - +Any extension that does not provide graphical UI QML component can be loaded as *cutehmi.daemon.3* project. Use positional argument +to specify an extension. For example to run [Count Daemon](../../extensions/CuteHMI/Examples/CountDaemon.3/) example use following +command. ``` -cutehmi.daemon.2 --extension="CuteHMI.Examples.CountDaemon.2" +cutehmi.daemon.3 CuteHMI.Examples.CountDaemon.3 ``` Read system logs to investigate whether daemon is running (e.g. `journalctl -n20` on a system with *systemd*). One may use `--app` option to tell the program to work as a foreground process (this can be useful when testing projects). For example following command runs [Count Daemon](../../extensions/CuteHMI/Examples/CountDaemon.2/) in application mode. ``` -cutehmi.daemon.2 --extension="CuteHMI.Examples.CountDaemon.2" --app +cutehmi.daemon.3 CuteHMI.Examples.CountDaemon.2 --app ``` -Default loader picks `Main.qml` as default QML component to load. Component can be specified with `--component` option. One can also -use `--init` option to replace default loader with custom one. +Default loader picks `Daemon.qml` as default QML component to load. Component can be specified with second positional argument. + +One can also use `--init` option to replace default loader with custom one. You can use `--help` command line argument to see the list of all possible command line options. Setting empty path for PID file option (`--pidfile=`) disables creation of PID file. -Fore debug builds use `cutehmi.daemon.2.debug` instead of `cutehmi.daemon.2`. +Fore debug builds use `cutehmi.daemon.3.debug` instead of `cutehmi.daemon.3`. ## Linux ### Signals Under Unix daemon will respond to signals in a following way. - SIGTERM tells daemon to gracefully quit with exit code set to EXIT_SUCCESS (0 on almost all systems). - SIGINT tells daemon to gracefully quit, but exit code will be set to 128 + signal code. - SIGQUIT causes violent termination and exits via abort. - SIGHUP attempts to reload the project as if daemon has been restarted. ### Forking Daemon is a program, which is supposed to work as a background process. This could be achieved in many ways, but lots of traditional UNIX daemons have been accomplishing it through techniques, which involve forking. Unfortunately this spots a controversy. It comes out there are three conflicting schools on how many forks daemon should perform. According to these daemon should fork once, twice or not fork at all... Detailed description on the topic is out of the scope of this document, but here are some major points. #### Single forking daemon Single fork is performed to exit the parent process and get rid of controlling terminal. After forking, daemon can become a new session leader, so that when user logs out from the session, process is not killed along with processes associated with that session. This is how [daemon()][3] function has been implemented. #### Double forking daemon After first fork process can still acquire controlling terminal if it opens tty according to System V rules [[1]]. Second fork prevents this [[2]]. Unfortunately this approach heavily obscures process hierarchy, because daemon continues as orphaned process. Notably *systemd* has troubles handling double-forking daemons as service units. [Documentation](https://www.freedesktop.org/software/systemd/man/systemd.service.html#Type=) of *systemd* is explicit about expected behaviour of daemon for forking service type. ``` If set to forking, it is expected that the process configured with ExecStart= will call fork() as part of its start-up. The parent process is expected to exit when start-up is complete and all communication channels are set up. The child continues to run as the main service process, and the service manager will consider the unit started when the parent process exits. ``` In double-forking approach the child won't continue as main service process, but rather its own child will do. This can deceive *systemd* to think that daemon finished. #### No forking Many people advice to not fork at all, as it is a domain of the user to detrmine the way how the program shall be run. By this view program should not daemonize itself. Instead it can be run as a background process (by putting `&` after the command) and tools such as *screen* and *nohup* can be used to prevent process from being killed when user logs out [[4]]. --- -There are some valid points in each of these views, so *cutehmi.daemon.2* allows you to pick number of forks by setting -`--nforks={number}` parameter. For *systemd* service pick `--nforks=1` and use `Type=forking` or `--nforks=0` with `Type=simple`. -Note that `--nforks=0` is not the same as application mode (`--app` option) - daemon uses system logging facility instead of +There are some valid points in each of these views, so *cutehmi.daemon.3* allows you to pick number of forks by setting +`--forks={number}` parameter. For *systemd* service pick `--forks=1` and use `Type=forking` or `--forks=0` with `Type=simple`. +Note that `--forks=0` is not the same as application mode (`--app` option) - daemon uses system logging facility instead of standard output, responds to signals, unlocks former working directory, creates PID file, closes file descriptors and resets its umask. +## Changes + +Compared to previous major version following changes were made. +- Daemon looks for `Daemon` QML component instead of Main`. +- Extension is specified with first positional argument instead of `--extension` argument. +- Component is specified with second positional argument instead of `--component` argument. +- Option `--nforks` has been renamed to `--forks`. + ## References 1. [Stephen A. Rago, W. Richard Stevens, "Advanced Programming in the UNIX® Environment: Second Edition", Chapter 13. Daemon Processes.][1] 2. [Andries Brouwer, "The Linux kernel".][2] 3. [Linux Programmer's Manual, DAEMON(3)][3] 4. [Department of Health Technology, "Processes; foreground and background, ps, top, kill, screen, nohup and daemons".][4] [1]: https://learning.oreilly.com/library/view/advanced-programming-in/0201433079/ [2]: https://www.win.tue.nl/~aeb/linux/lk/lk-10.html [3]: http://man7.org/linux/man-pages/man3/daemon.3.html [4]: http://teaching.healthtech.dtu.dk/unix/index.php/Processes;_foreground_and_background,_ps,_top,_kill,_screen,_nohup_and_daemons diff --git a/tools/cutehmi.daemon.3/dev/cutehmi.daemon-1.workaround.Qt.QTBUG-73649.txt b/tools/cutehmi.daemon.3/dev/cutehmi.daemon-1.workaround.Qt.QTBUG-73649.txt new file mode 100644 index 00000000..ed4a76a7 --- /dev/null +++ b/tools/cutehmi.daemon.3/dev/cutehmi.daemon-1.workaround.Qt.QTBUG-73649.txt @@ -0,0 +1,28 @@ +Problem: + +Class QQmlApplicationEngine connects Qt.quit() signal to +QCoreApplication::quit() and QQmlApplicationEngine::exit() signal to +QCoreApplication::exit(), but it does so with AutoConnection. This causes in +some circumstances problems, which are described in Qt documentation. + +"It's good practice to always connect signals to this slot using a +QueuedConnection. If a signal connected (non-queued) to this slot is emitted +before control enters the main event loop (such as before "int main" calls +exec()), the slot has no effect and the application never exits. Using a queued +connection ensures that the slot will not be invoked until after control enters +the main event loop." -- Qt documentation on QCoreApplication::exit(). + +Investigation: + +File qtdeclarative/src/qml/qml/qqmlapplicationengine.cpp contains method +QQmlApplicationEnginePrivate::init(), which contains following lines. + +``` +q->connect(q, SIGNAL(quit()), QCoreApplication::instance(), SLOT(quit())); +q->connect(q, &QQmlApplicationEngine::exit, QCoreApplication::instance(), &QCoreApplication::exit); +``` + +Workaround: + +Disconnect signals and connect them again with QueuedConnection. + diff --git a/tools/cutehmi.daemon.3/doc/project_logo.png b/tools/cutehmi.daemon.3/doc/project_logo.png new file mode 100644 index 00000000..c5bc24be Binary files /dev/null and b/tools/cutehmi.daemon.3/doc/project_logo.png differ diff --git a/tools/cutehmi.daemon.3/project.qbs b/tools/cutehmi.daemon.3/project.qbs new file mode 100644 index 00000000..16ac1932 --- /dev/null +++ b/tools/cutehmi.daemon.3/project.qbs @@ -0,0 +1,109 @@ +import qbs 1.0 + +import cutehmi + +Project { + name: "cutehmi.daemon.3" + + condition: !qbs.targetOS.contains("android") + + references: [ + "tests/tests.qbs" + ] + + cutehmi.Tool { + name: parent.name + + consoleApplication: true + + vendor: "CuteHMI" + + domain: "cutehmi.kde.org" + + friendlyName: "Daemon" + + description: "Console program, which allows one to run QML project in the background." + + files: [ + "README.md", + "dev/cutehmi.daemon-1.workaround.Qt.QTBUG-73649.txt", + "qml/ExtensionLoader.qml", + "resources.qrc", + "src/cutehmi/daemon/CoreData.hpp", + "src/cutehmi/daemon/Daemon.cpp", + "src/cutehmi/daemon/Daemon.hpp", + "src/cutehmi/daemon/Exception.cpp", + "src/cutehmi/daemon/Exception.hpp", + "src/cutehmi/daemon/logging.cpp", + "src/cutehmi/daemon/logging.hpp", + "src/main.cpp", + ] + + property string defaultExtension + + property string defaultMinor + + property string defaultInit + + property string defaultComponent + +// property string defaultLanguage + + property bool forceDefaultOptions: false + + cpp.defines: { + var result = [] + + if (defaultExtension) + result.push("CUTEHMI_DAEMON_DEFAULT_EXTENSION=\"" + defaultExtension + "\"") + + if (defaultMinor) + result.push("CUTEHMI_DAEMON_DEFAULT_MINOR=\"" + defaultMinor + "\"") + + if (defaultInit) + result.push("CUTEHMI_DAEMON_DEFAULT_INIT=\"" + defaultInit + "\"") + + if (defaultComponent) + result.push("CUTEHMI_DAEMON_DEFAULT_COMPONENT=\"" + defaultComponent + "\"") + + if (forceDefaultOptions) + result.push("CUTEHMI_DAEMON_FORCE_DEFAULT_OPTIONS") + +// if (defaultLanguage) +// result.push("CUTEHMI_DAEMON_DEFAULT_LANGUAGE=\"" + defaultLanguage + "\"") + + return result + } + + Group { + name: "Windows" + condition: qbs.targetOS.contains("windows") + files: [ + "src/cutehmi/daemon/Daemon_win.cpp", + ] + } + Group { + name: "Linux" + condition: qbs.targetOS.contains("linux") + files: [ + "src/cutehmi/daemon/Daemon_unix.cpp", + "src/cutehmi/daemon/Daemon_unix.hpp" + ] + } + + cutehmi.dirs.artifacts: true + + Depends { name: "CuteHMI.2" } + + Depends { name: "cutehmi.doxygen" } + cutehmi.doxygen.exclude: ['dev', 'examples', 'tests'] + + Depends { name: "cutehmi.metadata" } + } +} + +//(c)C: Copyright © 2020, Michał Policht . All rights reserved. +//(c)C: This file is a part of CuteHMI. +//(c)C: CuteHMI is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +//(c)C: CuteHMI 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 Lesser General Public License for more details. +//(c)C: You should have received a copy of the GNU Lesser General Public License along with CuteHMI. If not, see . diff --git a/tools/cutehmi.daemon.3/qml/ExtensionLoader.qml b/tools/cutehmi.daemon.3/qml/ExtensionLoader.qml new file mode 100644 index 00000000..c1bb79b6 --- /dev/null +++ b/tools/cutehmi.daemon.3/qml/ExtensionLoader.qml @@ -0,0 +1,36 @@ +import QtQuick 2.0 + +import CuteHMI 2.0 as CuteHMI + +QtObject { + id: extensionContainer + + Component.onCompleted: { + if (cutehmi_daemon_extensionBaseName && cutehmi_daemon_extensionMajor && cutehmi_daemon_extensionMinor) { + var qmlData = "import " + cutehmi_daemon_extensionBaseName + " " + cutehmi_daemon_extensionMajor + "." + cutehmi_daemon_extensionMinor + qmlData += "\n" + cutehmi_daemon_extensionComponent + " {}\n" + + try { + Qt.createQmlObject(qmlData, extensionContainer) + } catch(error) { + var informativeText = qsTr("Reasons:") + informativeText += "\n" + error.qmlErrors.map(function (obj) { return "- " + obj.message }).join("\n") + "." + + var extensionName = cutehmi_daemon_extensionBaseName + " " + cutehmi_daemon_extensionMajor + "." + cutehmi_daemon_extensionMinor + console.error(qsTr("Could not load extension '%1'.").arg(extensionName)) + console.error(informativeText) + Qt.exit(1) + } + } else { + console.log("No extension specified.") + console.log("Use --help to see available options.") + Qt.quit() + } + } +} + +//(c)C: Copyright © 2020, Michał Policht . All rights reserved. +//(c)C: This file is a part of CuteHMI. +//(c)C: CuteHMI is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +//(c)C: CuteHMI 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 Lesser General Public License for more details. +//(c)C: You should have received a copy of the GNU Lesser General Public License along with CuteHMI. If not, see . diff --git a/tools/cutehmi.daemon.3/resources.qrc b/tools/cutehmi.daemon.3/resources.qrc new file mode 100644 index 00000000..338e6a40 --- /dev/null +++ b/tools/cutehmi.daemon.3/resources.qrc @@ -0,0 +1,5 @@ + + + qml/ExtensionLoader.qml + + diff --git a/tools/cutehmi.daemon.3/src/cutehmi/daemon/CoreData.hpp b/tools/cutehmi.daemon.3/src/cutehmi/daemon/CoreData.hpp new file mode 100644 index 00000000..c40d034a --- /dev/null +++ b/tools/cutehmi.daemon.3/src/cutehmi/daemon/CoreData.hpp @@ -0,0 +1,45 @@ +#ifndef H_TOOLS_CUTEHMI_DAEMON_2_SRC_CUTEHMI_DAEMON_COREDATA_HPP +#define H_TOOLS_CUTEHMI_DAEMON_2_SRC_CUTEHMI_DAEMON_COREDATA_HPP + +#include +#include + +namespace cutehmi { +namespace daemon { + +struct CoreData +{ + QCoreApplication * app; + QCommandLineParser * cmd; + QTranslator * qtTranslator; + QTranslator * daemonTranslator; + QString language; + QStringList failedTranslationsFiles; + QString translationsDir; + QString daemonTranslationFilePrefix; + QString daemonTranslationFile; + + struct Options + { + QStringList extension; + QStringList component; + QCommandLineOption app; + QCommandLineOption basedir; + QCommandLineOption init; + QCommandLineOption minor; + QCommandLineOption lang; + QCommandLineOption pidfile; + QCommandLineOption forks; + } * opt; +}; + +} +} + +#endif + +//(c)C: Copyright © 2020, Michał Policht . All rights reserved. +//(c)C: This file is a part of CuteHMI. +//(c)C: CuteHMI is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +//(c)C: CuteHMI 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 Lesser General Public License for more details. +//(c)C: You should have received a copy of the GNU Lesser General Public License along with CuteHMI. If not, see . diff --git a/tools/cutehmi.daemon.3/src/cutehmi/daemon/Daemon.cpp b/tools/cutehmi.daemon.3/src/cutehmi/daemon/Daemon.cpp new file mode 100644 index 00000000..e9d06cef --- /dev/null +++ b/tools/cutehmi.daemon.3/src/cutehmi/daemon/Daemon.cpp @@ -0,0 +1,52 @@ +#include "Daemon.hpp" + +namespace cutehmi { +namespace daemon { + +Daemon::Daemon(CoreData * data, std::function & core): + m_data(data), + m_core(core), + m_exitCode(EXIT_FAILURE) +{ + // + _init(); + // +} + +Daemon::~Daemon() +{ + _destroy(); +} + +int Daemon::exitCode() const +{ + return m_exitCode; +} + +void Daemon::setExitCode(int exitCode) +{ + m_exitCode = exitCode; +} + +int Daemon::exec() +{ + do { + _exec(); + m_exitCode = m_core(*m_data); + } while (m_exitCode == EXIT_AGAIN); + return m_exitCode; +} + +CoreData * Daemon::data() const +{ + return m_data; +} + +} +} + +//(c)C: Copyright © 2020, Michał Policht . All rights reserved. +//(c)C: This file is a part of CuteHMI. +//(c)C: CuteHMI is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +//(c)C: CuteHMI 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 Lesser General Public License for more details. +//(c)C: You should have received a copy of the GNU Lesser General Public License along with CuteHMI. If not, see . diff --git a/tools/cutehmi.daemon.3/src/cutehmi/daemon/Daemon.hpp b/tools/cutehmi.daemon.3/src/cutehmi/daemon/Daemon.hpp new file mode 100644 index 00000000..6427236d --- /dev/null +++ b/tools/cutehmi.daemon.3/src/cutehmi/daemon/Daemon.hpp @@ -0,0 +1,53 @@ +#ifndef H_TOOLS_CUTEHMI_DAEMON_2_SRC_CUTEHMI_DAEMON_DAEMON_HPP +#define H_TOOLS_CUTEHMI_DAEMON_2_SRC_CUTEHMI_DAEMON_DAEMON_HPP + +#include "CoreData.hpp" + +#include + +namespace cutehmi { +namespace daemon { + +class _Daemon; + +class Daemon final +{ + public: + static constexpr int EXIT_AGAIN = 129; // 128 + SIGHUP (1). + + Daemon(CoreData * data, std::function & core); + + ~Daemon(); // Non-virtual destructor, but class is final. + + int exitCode() const; + + protected: + void setExitCode(int exitCode); + + int exec(); + + CoreData * data() const; + + private: + void _init(); + + void _exec(); + + void _destroy(); + + CoreData * m_data; + std::function & m_core; + int m_exitCode; + _Daemon * _daemon; +}; + +} +} + +#endif + +//(c)C: Copyright © 2020, Michał Policht . All rights reserved. +//(c)C: This file is a part of CuteHMI. +//(c)C: CuteHMI is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +//(c)C: CuteHMI 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 Lesser General Public License for more details. +//(c)C: You should have received a copy of the GNU Lesser General Public License along with CuteHMI. If not, see . diff --git a/tools/cutehmi.daemon.3/src/cutehmi/daemon/Daemon_unix.cpp b/tools/cutehmi.daemon.3/src/cutehmi/daemon/Daemon_unix.cpp new file mode 100644 index 00000000..194a40d4 --- /dev/null +++ b/tools/cutehmi.daemon.3/src/cutehmi/daemon/Daemon_unix.cpp @@ -0,0 +1,338 @@ +#include "Daemon.hpp" +#include "Daemon_unix.hpp" +#include "logging.hpp" +#include "../../../cutehmi.metadata.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void syslogMessageHandler(QtMsgType type, const QMessageLogContext & context, const QString & msg) +{ + QByteArray localMsg = msg.toLocal8Bit(); + switch (type) { +#ifndef CUTEHMI_NDEBUG + case QtDebugMsg: + syslog(LOG_DEBUG, "%s: %s [%s:%u, %s]", context.category, localMsg.constData(), context.file, context.line, context.function); + break; + case QtInfoMsg: + syslog(LOG_INFO, "%s: %s [%s:%u, %s]", context.category, localMsg.constData(), context.file, context.line, context.function); + break; + case QtWarningMsg: + syslog(LOG_WARNING, "%s: %s [%s:%u, %s]", context.category, localMsg.constData(), context.file, context.line, context.function); + break; + case QtCriticalMsg: + syslog(LOG_CRIT, "%s: %s [%s:%u, %s]", context.category, localMsg.constData(), context.file, context.line, context.function); + break; + case QtFatalMsg: + syslog(LOG_ALERT, "%s: %s [%s:%u, %s]", context.category, localMsg.constData(), context.file, context.line, context.function); + break; +#else + case QtDebugMsg: + syslog(LOG_DEBUG, "%s: %s", context.category, localMsg.constData()); + break; + case QtInfoMsg: + syslog(LOG_INFO, "%s: %s", context.category, localMsg.constData()); + break; + case QtWarningMsg: + syslog(LOG_WARNING, "%s: %s", context.category, localMsg.constData()); + break; + case QtCriticalMsg: + syslog(LOG_CRIT, "%s: %s", context.category, localMsg.constData()); + break; + case QtFatalMsg: + syslog(LOG_ALERT, "%s: %s", context.category, localMsg.constData()); + break; +#endif + } +} + +} + +namespace cutehmi { +namespace daemon { + +void sigHandler(int signal) +{ + // Reference implementation: http://doc.qt.io/qt-5/unix-signals.html. + + ::write(_Daemon::signalFd[0], & signal, sizeof(signal)); +} + +int _Daemon::signalFd[2]; + +_Daemon::_Daemon(const QString & pidFile): + m_pidFile(pidFile), + m_pidFd(-1) +{ +} + +void _Daemon::initializeSignalHandling() +{ + // Reference implementation: http://doc.qt.io/qt-5/unix-signals.html. + + if (::socketpair(AF_UNIX, SOCK_STREAM, 0, signalFd)) + CUTEHMI_DIE("Could not create socket pair for signal handling."); + m_signalSocketNotifier.reset(new QSocketNotifier(signalFd[1], QSocketNotifier::Read)); + connect(m_signalSocketNotifier.get(), & QSocketNotifier::activated, this, & _Daemon::handleSignal); + +} + +void _Daemon::initializePidFile() +{ + if (!m_pidFile.isEmpty()) { + CUTEHMI_DEBUG("PID file path: '" << m_pidFile << "'."); + m_pidFd = createPidFile(); + lockPidFile(); + writePidFile(); + } else + CUTEHMI_WARNING("PID file disabled."); +} + +void _Daemon::destroyPidFile() +{ + if (!m_pidFile.isEmpty()) + removePidFile(); +} + +void _Daemon::handleSignal() +{ + // Reference implementation: http://doc.qt.io/qt-5/unix-signals.html. + + m_signalSocketNotifier->setEnabled(false); + + int signal; + QByteArray signalArr(sizeof(signal), 0); + std::size_t nRead = 0; + do { + ssize_t n = ::read(signalFd[1], signalArr.data() + nRead, sizeof(signal) - nRead); + if (n == -1) { + CUTEHMI_CRITICAL("Fatal error occurred while reading from local socket: " << strerror(errno)); + break; + } + if (n == 0) { + CUTEHMI_CRITICAL("Unexpected EOF occurred while reading from local socket."); + break; + } + nRead += n; + } while (nRead < sizeof(signal)); + + if (nRead == sizeof(signal)) { + signal = *reinterpret_cast(signalArr.data()); + switch (signal) { + case SIGTERM: + CUTEHMI_INFO("Termination requested by SIGTERM (" << signal << ") signal."); + emit exitRequested(EXIT_SUCCESS); + break; + case SIGINT: + CUTEHMI_INFO("Interrupt requested by SIGINT (" << signal << ") signal."); + emit exitRequested(128 + signal); + break; + case SIGHUP: + CUTEHMI_INFO("Restarting due to SIGHUP (" << signal << ") signal."); + emit exitRequested(Daemon::EXIT_AGAIN); + break; + case SIGQUIT: + CUTEHMI_DIE("Aborting due to SIGQUIT (%d) signal.", signal); + default: + CUTEHMI_WARNING("Captured unhandled signal (" << signal << ")."); + } + } else + CUTEHMI_CRITICAL("Unable to encode signal due to internal errors."); + + m_signalSocketNotifier->setEnabled(true); +} + +int _Daemon::createPidFile() +{ + int fd = open(m_pidFile.toLocal8Bit().constData(), O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); + if (fd == -1) + CUTEHMI_DIE("Could not open PID file '%s'; %s.", m_pidFile.toLocal8Bit().constData(), strerror(errno)); + + // Set close-on-exec flag. + int flags = fcntl(fd, F_GETFD); + if (flags == -1) + CUTEHMI_DIE("Could not access flags of PID file '%s'; %s.", m_pidFile.toLocal8Bit().constData(), strerror(errno)); + flags |= FD_CLOEXEC; + if (fcntl(fd, F_SETFD, flags) == -1) + CUTEHMI_DIE("Could not update flags of PID file '%s'; %s.", m_pidFile.toLocal8Bit().constData(), strerror(errno)); + + return fd; +} + +void _Daemon::lockPidFile() +{ + struct flock fl; + + fl.l_type = F_WRLCK; + fl.l_whence = SEEK_SET; + fl.l_start = 0; + fl.l_len = 0; + + if (fcntl(m_pidFd, F_SETLK, & fl) == -1) { + if (errno == EAGAIN || errno == EACCES) + CUTEHMI_DIE("PID file '%s' is already locked by another process. Ensure that another instance of '" CUTEHMI_DAEMON_NAME "' with the same 'pidfile' option is not already running.", m_pidFile.toLocal8Bit().constData()); + else + CUTEHMI_DIE("Unable to lock PID file '%s'; %s.", m_pidFile.toLocal8Bit().constData(), strerror(errno)); + } +} + +void _Daemon::writePidFile() +{ + if (ftruncate(m_pidFd, 0) == -1) + CUTEHMI_DIE("Could not truncate PID file '%s'.", m_pidFile.toLocal8Bit().constData()); + + QByteArray buff = QByteArray::number(getpid()); + if (static_cast(write(m_pidFd, buff.constData(), static_cast(buff.size()))) != buff.size()) + CUTEHMI_DIE("Could not store PID in the PID file '%s'.", m_pidFile.toLocal8Bit().constData()); +} + +void _Daemon::removePidFile() +{ + if (close(m_pidFd) == -1) + CUTEHMI_DIE("Could not close descriptor of the PID file '%s'; %s.", m_pidFile.toLocal8Bit().constData(), strerror(errno)); + + if (unlink(m_pidFile.toLocal8Bit().constData()) == -1) + CUTEHMI_WARNING("Could not remove PID file '" << m_pidFile << "'; " << strerror(errno) << "."); +} + + +void Daemon::_init() +{ + // + + // Configure logging. + openlog(CUTEHMI_DAEMON_NAME, LOG_PID | LOG_NDELAY, LOG_USER); + qInstallMessageHandler(::syslogMessageHandler); + + // As logging has been configured daemon can use logging macros. Standard file descriptors still have to be closed. + CUTEHMI_INFO("Starting " << CUTEHMI_DAEMON_NAME << "..."); + + int forks; + { + bool ok; + forks = data()->cmd->value(data()->opt->forks).toInt(& ok); + if (!ok) + CUTEHMI_DIE("Provided value for parameter '%s' is not a number.", data()->opt->forks.names()[0].toLocal8Bit().constData()); + else { + if (forks < 0 || forks > 2) + CUTEHMI_DIE("Value of parameter '%s' is out of range [0, 2].", data()->opt->forks.names()[0].toLocal8Bit().constData()); + CUTEHMI_DEBUG("Daemon set up to fork '" << forks << "' time(s)."); + } + } + + if (forks > 0) { + pid_t pid, sid; + + // Detach from controlling tty (tty is given per session). + // Note: when using std::exit() stack unwinding is not performed, which means destructors won't be called. + + // Fork and exit parent process to become a session leader. + pid = fork(); + if (pid < 0) { + CUTEHMI_WARNING("Daemon could not fork."); + std::exit(EXIT_FAILURE); + } else if (pid > 0) + std::exit(EXIT_SUCCESS); // tty session leader quits. + + // "... the child inherits the process group ID of the parent but gets a new process ID, so we’re guaranteed that the child + // is not a process group leader. This is a prerequisite for the call to setsid that is done next." + // -- Stephen A. Rago, W. Richard Stevens, "Advanced Programming in the UNIX® Environment: Second Edition" [Chapter 13. Daemon Processes]. + // Now process may become a session leader and detach from parent session. + sid = setsid(); + if (sid < 0) + CUTEHMI_DIE("Failed to initialize daemon (could not create session)."); + + if (forks > 1) { + // Second fork to become orphan and ensure that process will not acquire tty (see daemon(3)). + // Additionaly process will not receive modem hang-up signals (see Stephen A. Rago, W. Richard Stevens, "Advanced Programming in the UNIX® Environment: Second Edition" [Chapter 9. Process Relationships - Controlling Terminal]). + pid = fork(); + if (pid < 0) { + CUTEHMI_WARNING("Daemon could not fork."); + std::exit(EXIT_FAILURE); + } else if (pid > 0) + std::exit(EXIT_SUCCESS); // Session leader quits. + + // Orphaned daemon process continues... + } + } + + umask(0); + + // This is mainly to avoid locking former working directory. + if ((chdir("/")) < 0) + CUTEHMI_DIE("Failed to change working directory to '/'."); + + // Close file descriptors. + close(STDIN_FILENO); + close(STDOUT_FILENO); + close(STDERR_FILENO); + + // + + // Create helper class. + QString pidFilePath = data()->cmd->value(data()->opt->pidfile); + if (!pidFilePath.isEmpty()) { + if (QDir::isRelativePath(data()->cmd->value(data()->opt->pidfile))) + pidFilePath.prepend(QDir(data()->cmd->value(data()->opt->basedir)).absolutePath() + "/"); + } + _daemon = new _Daemon(pidFilePath); + + // Create exit point through 'terminateRequested' signal. + QObject::connect(_daemon, & _Daemon::exitRequested, [](int exitCode) { + QCoreApplication::exit(exitCode); + }); + + // Initialize signal handling. + _daemon->initializeSignalHandling(); + + // Initialize PID file. + _daemon->initializePidFile(); + + // Install signal handler. + struct sigaction sigAct; + sigAct.sa_handler = sigHandler; + sigemptyset(& sigAct.sa_mask); + sigAct.sa_flags = 0; + sigaction(SIGTERM, & sigAct, nullptr); + sigaction(SIGINT, & sigAct, nullptr); + sigaction(SIGQUIT, & sigAct, nullptr); + sigaction(SIGHUP, & sigAct, nullptr); + + // Execute core. + exec(); +} + +void Daemon::_exec() +{ +} + +void Daemon::_destroy() +{ + CUTEHMI_INFO("Daemon finished execution with exit code '" << exitCode() << "'."); + + _daemon->destroyPidFile(); + _daemon->deleteLater(); + + closelog(); // "The use of closelog() is optional." -- SYSLOG(3). +} + +} +} + +//(c)C: Copyright © 2020, Michał Policht . All rights reserved. +//(c)C: This file is a part of CuteHMI. +//(c)C: CuteHMI is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +//(c)C: CuteHMI 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 Lesser General Public License for more details. +//(c)C: You should have received a copy of the GNU Lesser General Public License along with CuteHMI. If not, see . diff --git a/tools/cutehmi.daemon.3/src/cutehmi/daemon/Daemon_unix.hpp b/tools/cutehmi.daemon.3/src/cutehmi/daemon/Daemon_unix.hpp new file mode 100644 index 00000000..de6fa271 --- /dev/null +++ b/tools/cutehmi.daemon.3/src/cutehmi/daemon/Daemon_unix.hpp @@ -0,0 +1,65 @@ +#ifndef H_TOOLS_CUTEHMI_DAEMON_2_SRC_CUTEHMI_DAEMON_DAEMON_u_UNIX_HPP +#define H_TOOLS_CUTEHMI_DAEMON_2_SRC_CUTEHMI_DAEMON_DAEMON_u_UNIX_HPP + +#include +#include + +#include + +namespace cutehmi { +namespace daemon { + +class Daemon; + +/** + * Unix-specific daemon helper. + */ +class _Daemon: + public QObject +{ + Q_OBJECT + + public: + static int signalFd[2]; + + _Daemon(const QString & pidFile); + + void initializeSignalHandling(); + + void initializePidFile(); + + void destroyPidFile(); + + signals: + void exitRequested(int exitCode); + + private slots: + void handleSignal(); + + private: + int createPidFile(); + + void lockPidFile(); + + void writePidFile(); + + void removePidFile(); + + private: + QString m_pidFile; + int m_pidFd; + std::unique_ptr m_signalSocketNotifier; + std::unique_ptr m_unhandledSignalsSocketNotifier; +}; + + +} +} + +#endif + +//(c)C: Copyright © 2020, Michał Policht . All rights reserved. +//(c)C: This file is a part of CuteHMI. +//(c)C: CuteHMI is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +//(c)C: CuteHMI 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 Lesser General Public License for more details. +//(c)C: You should have received a copy of the GNU Lesser General Public License along with CuteHMI. If not, see . diff --git a/tools/cutehmi.daemon.3/src/cutehmi/daemon/Daemon_win.cpp b/tools/cutehmi.daemon.3/src/cutehmi/daemon/Daemon_win.cpp new file mode 100644 index 00000000..4f452f18 --- /dev/null +++ b/tools/cutehmi.daemon.3/src/cutehmi/daemon/Daemon_win.cpp @@ -0,0 +1,29 @@ +#include "Daemon.hpp" +#include "logging.hpp" + +namespace cutehmi { +namespace daemon { + +void Daemon::_init() +{ + CUTEHMI_DIE("Windows daemon is not implemented."); +} + +void Daemon::_exec() +{ + CUTEHMI_DIE("Windows daemon is not implemented."); +} + +void Daemon::_destroy() +{ + CUTEHMI_DIE("Windows daemon is not implemented."); +} + +} +} + +//(c)C: Copyright © 2020, Michał Policht . All rights reserved. +//(c)C: This file is a part of CuteHMI. +//(c)C: CuteHMI is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +//(c)C: CuteHMI 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 Lesser General Public License for more details. +//(c)C: You should have received a copy of the GNU Lesser General Public License along with CuteHMI. If not, see . diff --git a/tools/cutehmi.daemon.3/src/cutehmi/daemon/Exception.cpp b/tools/cutehmi.daemon.3/src/cutehmi/daemon/Exception.cpp new file mode 100644 index 00000000..71831904 --- /dev/null +++ b/tools/cutehmi.daemon.3/src/cutehmi/daemon/Exception.cpp @@ -0,0 +1,18 @@ +#include "Exception.hpp" + +namespace cutehmi { +namespace daemon { + +Exception::Exception(const QString & what): + Parent(what) +{ +} + +} +} + +//(c)C: Copyright © 2020, Michał Policht . All rights reserved. +//(c)C: This file is a part of CuteHMI. +//(c)C: CuteHMI is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +//(c)C: CuteHMI 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 Lesser General Public License for more details. +//(c)C: You should have received a copy of the GNU Lesser General Public License along with CuteHMI. If not, see . diff --git a/tools/cutehmi.daemon.3/src/cutehmi/daemon/Exception.hpp b/tools/cutehmi.daemon.3/src/cutehmi/daemon/Exception.hpp new file mode 100644 index 00000000..f3419e1e --- /dev/null +++ b/tools/cutehmi.daemon.3/src/cutehmi/daemon/Exception.hpp @@ -0,0 +1,27 @@ +#ifndef H_TOOLS_CUTEHMI_DAEMON_2_SRC_CUTEHMI_DAEMON_EXCEPTION_HPP +#define H_TOOLS_CUTEHMI_DAEMON_2_SRC_CUTEHMI_DAEMON_EXCEPTION_HPP + +#include + +namespace cutehmi { +namespace daemon { + +class Exception: + public ExceptionMixin +{ + typedef ExceptionMixin Parent; + + public: + Exception(const QString & what); +}; + +} +} + +#endif + +//(c)C: Copyright © 2020, Michał Policht . All rights reserved. +//(c)C: This file is a part of CuteHMI. +//(c)C: CuteHMI is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +//(c)C: CuteHMI 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 Lesser General Public License for more details. +//(c)C: You should have received a copy of the GNU Lesser General Public License along with CuteHMI. If not, see . diff --git a/tools/cutehmi.daemon.3/src/cutehmi/daemon/logging.cpp b/tools/cutehmi.daemon.3/src/cutehmi/daemon/logging.cpp new file mode 100644 index 00000000..490e0ea0 --- /dev/null +++ b/tools/cutehmi.daemon.3/src/cutehmi/daemon/logging.cpp @@ -0,0 +1,10 @@ +#include "logging.hpp" +#include "../../../cutehmi.metadata.hpp" + +Q_LOGGING_CATEGORY(cutehmi_daemon_loggingCategory, CUTEHMI_DAEMON_NAME) + +//(c)C: Copyright © 2020, Michał Policht . All rights reserved. +//(c)C: This file is a part of CuteHMI. +//(c)C: CuteHMI is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +//(c)C: CuteHMI 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 Lesser General Public License for more details. +//(c)C: You should have received a copy of the GNU Lesser General Public License along with CuteHMI. If not, see . diff --git a/tools/cutehmi.daemon.3/src/cutehmi/daemon/logging.hpp b/tools/cutehmi.daemon.3/src/cutehmi/daemon/logging.hpp new file mode 100644 index 00000000..c16f7f1e --- /dev/null +++ b/tools/cutehmi.daemon.3/src/cutehmi/daemon/logging.hpp @@ -0,0 +1,26 @@ +#ifndef H_TOOLS_CUTEHMI_DAEMON_2_SRC_CUTEHMI_DAEMON_LOGGING_HPP +#define H_TOOLS_CUTEHMI_DAEMON_2_SRC_CUTEHMI_DAEMON_LOGGING_HPP + +#include + +Q_DECLARE_LOGGING_CATEGORY(cutehmi_daemon_loggingCategory) + +namespace cutehmi { +namespace daemon { + +inline +const QLoggingCategory & loggingCategory() +{ + return cutehmi_daemon_loggingCategory(); +} + +} +} + +#endif + +//(c)C: Copyright © 2020, Michał Policht . All rights reserved. +//(c)C: This file is a part of CuteHMI. +//(c)C: CuteHMI is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +//(c)C: CuteHMI 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 Lesser General Public License for more details. +//(c)C: You should have received a copy of the GNU Lesser General Public License along with CuteHMI. If not, see . diff --git a/tools/cutehmi.daemon.3/src/main.cpp b/tools/cutehmi.daemon.3/src/main.cpp new file mode 100644 index 00000000..fc314754 --- /dev/null +++ b/tools/cutehmi.daemon.3/src/main.cpp @@ -0,0 +1,329 @@ +#include "../cutehmi.metadata.hpp" +#include "../cutehmi.dirs.hpp" +#include "cutehmi/daemon/logging.hpp" +#include "cutehmi/daemon/Daemon.hpp" +#include "cutehmi/daemon/CoreData.hpp" +#include "cutehmi/daemon/Exception.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace cutehmi::daemon; + +/** + * Main function. + * @param argc number of arguments passed to the program. + * @param argv list of arguments passed to the program. + * @return return code. + */ +int main(int argc, char * argv[]) +{ +#ifdef CUTEHMI_DAEMON_DEFAULT_INIT + static constexpr const char * DEFAULT_INIT = CUTEHMI_DAEMON_DEFAULT_INIT; +#else + static constexpr const char * DEFAULT_INIT = "qrc:/qml/ExtensionLoader.qml"; +#endif + +#ifdef CUTEHMI_DAEMON_DEFAULT_EXTENSION + static constexpr const char * DEFAULT_EXTENSION = CUTEHMI_DAEMON_DEFAULT_EXTENSION; +#else + static constexpr const char * DEFAULT_EXTENSION = ""; +#endif + +#ifdef CUTEHMI_DAEMON_DEFAULT_COMPONENT + static constexpr const char * DEFAULT_COMPONENT = CUTEHMI_DAEMON_DEFAULT_COMPONENT; +#else + static constexpr const char * DEFAULT_COMPONENT = "Daemon"; +#endif + +#ifdef CUTEHMI_DAEMON_DEFAULT_MINOR + static constexpr const char * DEFAULT_MINOR = CUTEHMI_DAEMON_DEFAULT_MINOR; +#else + static constexpr const char * DEFAULT_MINOR = "0"; +#endif + + static constexpr const char * DEFAULT_FORKS = "2"; + + // + // Output shall remain silent until daemon logging is set up. + + // + // "In general, creating QObjects before the QApplication is not supported and can lead to weird crashes on exit, depending on the + // platform. This means static instances of QObject are also not supported. A properly structured single or multi-threaded application + // should make the QApplication be the first created, and last destroyed QObject." + + // Set up application. + + QCoreApplication::setOrganizationName(CUTEHMI_DAEMON_VENDOR); + QCoreApplication::setOrganizationDomain(CUTEHMI_DAEMON_DOMAIN); + QCoreApplication::setApplicationName(CUTEHMI_DAEMON_FRIENDLY_NAME); + QCoreApplication::setApplicationVersion(QString("%1.%2.%3").arg(CUTEHMI_DAEMON_MAJOR).arg(CUTEHMI_DAEMON_MINOR).arg(CUTEHMI_DAEMON_MICRO)); + + QCoreApplication app(argc, argv); + + + // Initial language setup. + + QString language = QLocale::system().name(); + QStringList failedTranslationsFiles; +#ifdef CUTEHMI_VIEW_DEFAULT_LANGUAGE + language = CUTEHMI_VIEW_DEFAULT_LANGUAGE; +#endif + if (!qgetenv("CUTEHMI_LANGUAGE").isEmpty()) + language = qgetenv("CUTEHMI_LANGUAGE"); + + QTranslator qtTranslator; + if (!qtTranslator.load("qt_" + language, QLibraryInfo::location(QLibraryInfo::TranslationsPath))) + failedTranslationsFiles.append("qt_" + language); + app.installTranslator(& qtTranslator); + + QTranslator daemonTranslator; + QString translationsDir = QDir("/" CUTEHMI_DIRS_TOOLS_INSTALL_SUBDIR).relativeFilePath("/" CUTEHMI_DIRS_TRANSLATIONS_INSTALL_SUBDIR); + QString daemonTranslationFilePrefix = QString(CUTEHMI_DAEMON_NAME).replace('.', '-') + "_"; + QString daemonTranslationFile = daemonTranslationFilePrefix + language; + if (!daemonTranslator.load(daemonTranslationFile, translationsDir)) + failedTranslationsFiles.append(daemonTranslationFile); + app.installTranslator(& daemonTranslator); + + + // Configure command line parser and process arguments. + + QCommandLineParser cmd; + cmd.setApplicationDescription(CUTEHMI_DAEMON_TRANSLATED_FRIENDLY_NAME + "\n" + CUTEHMI_DAEMON_TRANSLATED_DESCRIPTION); + cmd.addHelpOption(); + cmd.addVersionOption(); + CoreData::Options opt { + QStringList({"extension", QCoreApplication::translate("main", "Extension to import."), "[extension]"}), + QStringList({"component", QCoreApplication::translate("main", "Component to create. Defaults to '%1'.").arg(DEFAULT_COMPONENT), "[component]"}), + QCommandLineOption("app", QCoreApplication::translate("main", "Run project in application mode.")), + QCommandLineOption("basedir", QCoreApplication::translate("main", "Set base directory to ."), QCoreApplication::translate("main", "dir")), + QCommandLineOption("init", QCoreApplication::translate("main", "Override loader by specifying initial QML to load."), QCoreApplication::translate("main", "file")), + QCommandLineOption({"m", "minor"}, QCoreApplication::translate("main", "Use for extension minor version to import."), QCoreApplication::translate("main", "version")), + QCommandLineOption("lang", QCoreApplication::translate("main", "Choose application ."), QCoreApplication::translate("main", "language")), + QCommandLineOption("pidfile", QCoreApplication::translate("main", "PID file (Unix-specific)."), QCoreApplication::translate("main", "path")), + QCommandLineOption("forks", QCoreApplication::translate("main", "Denotes of forks the daemon should perform (Unix-specific)."), QCoreApplication::translate("main", "number")), + }; + opt.init.setDefaultValue(DEFAULT_INIT); + opt.minor.setDefaultValue(DEFAULT_MINOR); + opt.pidfile.setDefaultValue(QString("/var/run/") + CUTEHMI_DAEMON_NAME ".pid"); + opt.pidfile.setDescription(opt.pidfile.description() + "\nDefault value: '" + opt.pidfile.defaultValues().at(0) + "'."); + opt.basedir.setDefaultValue(QDir(QCoreApplication::applicationDirPath() + "/..").canonicalPath()); + opt.basedir.setDescription(opt.basedir.description() + "\nDefault value: '" + opt.basedir.defaultValues().at(0) + "'."); + opt.forks.setDefaultValue(DEFAULT_FORKS); + opt.forks.setDescription(opt.forks.description() + "\nDefault value: '" + opt.forks.defaultValues().at(0) + "'."); + cmd.addOption(opt.app); + cmd.addOption(opt.basedir); + cmd.addOption(opt.init); + cmd.addOption(opt.minor); + cmd.addOption(opt.lang); + cmd.addOption(opt.pidfile); + cmd.addOption(opt.forks); +#ifdef CUTEHMI_DAEMON_FORCE_DEFAULT_OPTIONS + opt.init.setFlags(QCommandLineOption::HiddenFromHelp); + opt.minor.setFlags(QCommandLineOption::HiddenFromHelp); +#else + cmd.addPositionalArgument(opt.extension.at(0), opt.extension.at(1), opt.extension.at(2)); + cmd.addPositionalArgument(opt.component.at(0), opt.component.at(1), opt.component.at(2)); +#endif + cmd.process(app); + + + // Prepare program core. + + CoreData coreData; + coreData.app = & app; + coreData.cmd = & cmd; + coreData.opt = & opt; + coreData.qtTranslator = & qtTranslator; + coreData.daemonTranslator = & daemonTranslator; + coreData.language = language; + coreData.translationsDir = translationsDir; + coreData.daemonTranslationFilePrefix = daemonTranslationFilePrefix; + coreData.daemonTranslationFile = daemonTranslationFile; + coreData.failedTranslationsFiles = failedTranslationsFiles; + + std::function core = [](CoreData & data) { + try { + // Finish language setup. + + if (!qgetenv("CUTEHMI_LANGUAGE").isEmpty()) + CUTEHMI_DEBUG("Language set by 'CUTEHMI_LANGUAGE' environmental variable: " << qgetenv("CUTEHMI_LANGUAGE")); + + if (!data.failedTranslationsFiles.isEmpty()) + for (auto translationsFile : data.failedTranslationsFiles) + CUTEHMI_WARNING("Could not load translations file '" << translationsFile << "'."); + + if (data.cmd->isSet(data.opt->lang)) + data.language = data.cmd->value(data.opt->lang); + + CUTEHMI_DEBUG("Default locale: " << QLocale()); + CUTEHMI_DEBUG("Language: " << data.language); + + if (!data.qtTranslator->load("qt_" + data.language, QLibraryInfo::location(QLibraryInfo::TranslationsPath))) + CUTEHMI_WARNING("Could not load translations file '" << "qt_" + data.language << "'."); + + data.daemonTranslationFilePrefix = QString(CUTEHMI_DAEMON_NAME).replace('.', '-') + "_"; + data.daemonTranslationFile = data.daemonTranslationFilePrefix + data.language; + if (!data.daemonTranslator->load(data.daemonTranslationFile, data.translationsDir)) + CUTEHMI_WARNING("Could not load translations file '" << data.daemonTranslationFile << "'."); + + + QDir baseDir = data.cmd->value(data.opt->basedir); + QString baseDirPath = baseDir.absolutePath() + "/"; + CUTEHMI_DEBUG("Base directory: " << baseDirPath); + + CUTEHMI_DEBUG("Library paths: " << QCoreApplication::libraryPaths()); + + QQmlApplicationEngine engine; + engine.addImportPath(baseDirPath + CUTEHMI_DIRS_EXTENSIONS_INSTALL_SUBDIR); + CUTEHMI_DEBUG("QML import paths: " << engine.importPathList()); + + QStringList positionalArguments = data.cmd->positionalArguments(); +#ifndef CUTEHMI_DAEMON_FORCE_DEFAULT_OPTIONS + QString extension; + if (positionalArguments.length() > 0) + extension = positionalArguments.at(0); + else + extension = DEFAULT_EXTENSION; + + QString extensionMinor = data.cmd->value(data.opt->minor); + QString init = data.cmd->value(data.opt->init); + QString component; + if (positionalArguments.length() > 1) + component = positionalArguments.at(1); + else + component = DEFAULT_COMPONENT; +#else + QString extension = DEFAULT_EXTENSION; + QString component = DEFAULT_COMPONENT; + QString extensionMinor = data.opt->minor.defaultValues().first(); + QString init = data.opt->init.defaultValues().first(); + + if (positionalArguments.length() > 1) + throw Exception(QCoreApplication::translate("main", "You can not use 'component' option, because 'forceDefaultOptions' option has been set during compilation time.").toLocal8Bit().constData()); + if (positionalArguments.length() > 0) + throw Exception(QCoreApplication::translate("main", "You can not use 'extension' option, because 'forceDefaultOptions' option has been set during compilation time.").toLocal8Bit().constData()); + if (data.cmd->value(data.opt->minor) != extensionMinor) + throw Exception(QCoreApplication::translate("main", "You can not use '%1' option, because 'forceDefaultOptions' option has been set during compilation time.").arg(data.opt->minor.names().join(", ")).toLocal8Bit().constData()); + if (data.cmd->value(data.opt->init) != init) + throw Exception(QCoreApplication::translate("main", "You can not use '%1' option, because 'forceDefaultOptions' option has been set during compilation time.").arg(data.opt->init.names().join(", ")).toLocal8Bit().constData()); +#endif + if (!extensionMinor.isEmpty()) { + bool ok; + extensionMinor.toUInt(& ok); + if (!ok) + throw Exception(QCoreApplication::translate("main", "Command line argument error: value of '%1' option must be a number.").arg(data.opt->minor.names().last())); + } + QString extensionBaseName = extension.left(extension.lastIndexOf('.')); + QString extensionMajor = extension.right(extension.length() - extension.lastIndexOf('.') - 1); + { + bool ok; + extensionMajor.toUInt(& ok); + if (!ok) + throw Exception(QCoreApplication::translate("main", "Command line argument error: please specify extension with major version number after the last dot.")); + } + engine.rootContext()->setContextProperty("cutehmi_daemon_extensionBaseName", extensionBaseName); + engine.rootContext()->setContextProperty("cutehmi_daemon_extensionMajor", extensionMajor); + engine.rootContext()->setContextProperty("cutehmi_daemon_extensionMinor", extensionMinor); + engine.rootContext()->setContextProperty("cutehmi_daemon_extensionComponent", component); + + // + // Class QQmlApplicationEngine connects Qt.quit() signal to QCoreApplication::quit() and QQmlApplicationEngine::exit() + // signal to QCoreApplication::exit(), but it does so with AutoConnection. This causes in some circumstances problems, + // which are described in Qt documentation (see QCoreApplication::exit()). Workaround is to disconnect signals and + // connect them again with QueuedConnection. + engine.disconnect(& engine, nullptr, data.app, nullptr); + engine.connect(& engine, SIGNAL(quit()), data.app, SLOT(quit()), Qt::QueuedConnection); + engine.connect(& engine, & QQmlApplicationEngine::exit, data.app, & QCoreApplication::exit, Qt::QueuedConnection); + // + + if (!init.isNull()) { + CUTEHMI_DEBUG("Init: '" << init << "'"); + CUTEHMI_DEBUG("Extension: '" << extension << "'"); + CUTEHMI_DEBUG("Minor: '" << extensionMinor << "'"); + CUTEHMI_DEBUG("Component: '" << component << "'"); + QUrl initUrl(init); + if (initUrl.isValid()) { + // Assure that URL is not mixing relative path with explicitly specified scheme, which is forbidden. QUrl::isValid() doesn't check this out. + if (!initUrl.scheme().isEmpty() && QDir::isRelativePath(initUrl.path())) + throw Exception(QCoreApplication::translate("main", "URL '%1' contains relative path along with URL scheme, which is forbidden.").arg(initUrl.url())); + else { + // If source URL is relative (does not contain scheme), then make absolute URL: file:///baseDirPath/sourceUrl. + if (initUrl.isRelative()) + initUrl = QUrl::fromLocalFile(baseDirPath).resolved(initUrl); + // Check if file exists and eventually set context property. + if (initUrl.isLocalFile() && !QFile::exists(initUrl.toLocalFile())) + throw Exception(QCoreApplication::translate("main", "QML file '%1' does not exist.").arg(initUrl.url())); + else { + engine.load(initUrl.url()); + int result = data.app->exec(); + engine.collectGarbage(); + return result; + } + } + } else + throw Exception(QCoreApplication::translate("main", "Invalid format of QML file URL '%1'.").arg(init)); + } else + throw Exception(QCoreApplication::translate("main", "No initial loader has been specified.")); + + return EXIT_SUCCESS; + + } catch (const Exception & e) { + CUTEHMI_CRITICAL(e.what()); + } catch (const cutehmi::Messenger::NoAdvertiserException & e) { + CUTEHMI_CRITICAL("Dialog message: " << e.message()->text()); + if (!e.message()->informativeText().isEmpty()) + CUTEHMI_CRITICAL("Informative text: " << e.message()->informativeText()); + if (!e.message()->detailedText().isEmpty()) + CUTEHMI_CRITICAL("Detailed text: " << e.message()->detailedText()); + CUTEHMI_CRITICAL("Available buttons: " << e.message()->buttons()); + } catch (const std::exception & e) { + CUTEHMI_CRITICAL(e.what()); + } catch (...) { + CUTEHMI_CRITICAL("Caught unrecognized exception."); + throw; + } + + return EXIT_FAILURE; + }; + + + // Run program core in daemon or application mode. + + int exitCode; + + if (!cmd.isSet(opt.app)) { + Daemon daemon(& coreData, core); + + // At this point logging should be configured and printing facilities silenced. Not much to say anyways... + // + + exitCode = daemon.exitCode(); + } else + exitCode = core(coreData); + + // Destroy singleton instances before QCoreApplication. Ignoring the recommendation to connect clean-up code to the + // aboutToQuit() signal, because for daemon it is always violent termination if QCoreApplication::exec() does not exit. + cutehmi::destroySingletonInstances(); + + return exitCode; + + // +} + +//(c)C: Copyright © 2020, Michał Policht . All rights reserved. +//(c)C: This file is a part of CuteHMI. +//(c)C: CuteHMI is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +//(c)C: CuteHMI 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 Lesser General Public License for more details. +//(c)C: You should have received a copy of the GNU Lesser General Public License along with CuteHMI. If not, see . diff --git a/tools/cutehmi.daemon.3/tests/Test.qbs b/tools/cutehmi.daemon.3/tests/Test.qbs new file mode 100644 index 00000000..e7f9db64 --- /dev/null +++ b/tools/cutehmi.daemon.3/tests/Test.qbs @@ -0,0 +1,18 @@ +import qbs + +import cutehmi + +cutehmi.Test +{ + testNamePrefix: parent.parent.name + + Depends { name: "cutehmi.daemon.3" } + Depends { name: "CuteHMI.2" } + Depends { name: "CuteHMI.Test.0" } +} + +//(c)C: Copyright © 2020, Michał Policht . All rights reserved. +//(c)C: This file is a part of CuteHMI. +//(c)C: CuteHMI is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +//(c)C: CuteHMI 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 Lesser General Public License for more details. +//(c)C: You should have received a copy of the GNU Lesser General Public License along with CuteHMI. If not, see . diff --git a/tools/cutehmi.daemon.3/tests/test_cutehmi_daemon.cpp b/tools/cutehmi.daemon.3/tests/test_cutehmi_daemon.cpp new file mode 100644 index 00000000..fe393038 --- /dev/null +++ b/tools/cutehmi.daemon.3/tests/test_cutehmi_daemon.cpp @@ -0,0 +1,106 @@ +#include +#include + +#include "../cutehmi.dirs.hpp" + +namespace cutehmi { +namespace daemon { + +class test_cutehmi_daemon: + public QObject +{ + Q_OBJECT + + private slots: + void initTestCase(); + + void helpOption(); + + void versionOption(); + + void countDaemonExample(); + + private: + QString m_installDir; + QString m_programPath; +}; + +void test_cutehmi_daemon::initTestCase() +{ + QString m_installDir = qEnvironmentVariable("CUTEHMI_INSTALL_DIR"); + QVERIFY(!m_installDir.isEmpty()); + + QString toolsInstallSubdir = CUTEHMI_DIRS_TOOLS_INSTALL_SUBDIR; + if (!toolsInstallSubdir.isEmpty()) + m_installDir += "/" + toolsInstallSubdir; + + m_programPath = m_installDir + "/cutehmi.daemon.3"; +#ifndef CUTEHMI_NDEBUG + m_programPath += ".debug"; +#endif +} + +void test_cutehmi_daemon::helpOption() +{ + QList argumentsList; + argumentsList << QStringList({"--help"}) + << QStringList({"--h"}); + + for (auto arguments : argumentsList) { + QProcess process; + process.start(m_programPath, arguments); + process.waitForFinished(1000); + QCOMPARE(process.error(), QProcess::UnknownError); + QVERIFY(!process.readAllStandardError().contains("Unknown option")); + QVERIFY(!process.readAllStandardOutput().isEmpty()); + QCOMPARE(process.exitStatus(), QProcess::NormalExit); + QCOMPARE(process.exitCode(), EXIT_SUCCESS); + } +} + +void test_cutehmi_daemon::versionOption() +{ + QList argumentsList; + argumentsList << QStringList({"--version"}) + << QStringList({"--v"}); + + for (auto arguments : argumentsList) { + QProcess process; + process.start(m_programPath, arguments); + process.waitForFinished(1000); + QCOMPARE(process.error(), QProcess::UnknownError); + QVERIFY(!process.readAllStandardError().contains("Unknown option")); + QVERIFY(!process.readAllStandardOutput().isEmpty()); + QCOMPARE(process.exitStatus(), QProcess::NormalExit); + QCOMPARE(process.exitCode(), EXIT_SUCCESS); + } +} + +void test_cutehmi_daemon::countDaemonExample() +{ + QProcess process; + QStringList arguments { {"--app"}, {"--extension=CuteHMI.Examples.CountDaemon.2"} }; + process.start(m_programPath, arguments); + QVERIFY(process.waitForFinished()); + + QString stdOut = QString::fromLocal8Bit(process.readAllStandardOutput()); + QString stdErr = QString::fromLocal8Bit(process.readAllStandardError()); + + QVERIFY(stdErr.contains("I can count")); + + QCOMPARE(process.error(), QProcess::UnknownError); + QCOMPARE(process.exitStatus(), QProcess::NormalExit); + QCOMPARE(process.exitCode(), EXIT_SUCCESS); +} + +} +} + +QTEST_MAIN(cutehmi::daemon::test_cutehmi_daemon) +#include "test_cutehmi_daemon.moc" + +//(c)C: Copyright © 2020, Michał Policht . All rights reserved. +//(c)C: This file is a part of CuteHMI. +//(c)C: CuteHMI is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +//(c)C: CuteHMI 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 Lesser General Public License for more details. +//(c)C: You should have received a copy of the GNU Lesser General Public License along with CuteHMI. If not, see . diff --git a/tools/cutehmi.daemon.3/tests/tests.qbs b/tools/cutehmi.daemon.3/tests/tests.qbs new file mode 100644 index 00000000..ae0a975d --- /dev/null +++ b/tools/cutehmi.daemon.3/tests/tests.qbs @@ -0,0 +1,23 @@ +import qbs + +import "Test.qbs" as Test + +Project { + condition: !qbs.targetOS.contains("windows") + + Test { + testName: "test_cutehmi_daemon" + + files: [ + "test_cutehmi_daemon.cpp", + ] + + Depends { name: "CuteHMI.Examples.CountDaemon.2" } + } +} + +//(c)C: Copyright © 2020, Michał Policht . All rights reserved. +//(c)C: This file is a part of CuteHMI. +//(c)C: CuteHMI is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +//(c)C: CuteHMI 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 Lesser General Public License for more details. +//(c)C: You should have received a copy of the GNU Lesser General Public License along with CuteHMI. If not, see .