mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-28 21:34:12 +02:00
8334a9a27f
A script reaches the whole project and, through the export calls, the
filesystem. That is a capability most people installing an electrical CAD
program never asked for, and leaving it on by default hands it to them
anyway. So QET_HAS_SCRIPTING builds now ship with it switched off.
QetSettings::scriptingEnabled() is the single answer, read by all three
places that need it, with QET_ENABLE_SCRIPTING=1 overriding the stored
value. The override is not decoration: a CI job or a batch run has no
dialog to tick, and a machine whose HOME is created fresh for each run has
nowhere to keep the setting either. It beats a stored "false" on purpose,
so a box unticked once cannot lock a build server out of --run for good.
Only the exact value "1" counts.
--run refuses with exit 3 and a message naming both ways in.
Projet > Exécuter un script... asks once, and turns the setting on if
the answer is yes. Asking beats grey: a disabled menu
entry says something exists and nothing about how to have
it, and this is the pattern people already know from
macro security in office software.
Configurer QElectroTech > Général > Projets has the checkbox, for
turning it back off. While the environment forces
scripting on, the box is disabled and says why, and
applyConf() then leaves the stored value alone rather
than quietly overwriting it.
runOnProject() checks as well, after both callers have. It is the one
function that actually evaluates JavaScript, so it is the one place a
future caller cannot forget to ask; the callers check first only to give a
better answer than it can.
Verified on the built binary, all four states, with an isolated HOME:
stored env result
absent - refused, exit 3
true - script runs, exit 0
false - refused, exit 3
false 1 script runs, exit 0
tst_scriptingsetting covers the same matrix hermetically, in its own
QSettings scope, and was mutation-checked: flipping the default to true
turns defaultsToOff() red.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
215 lines
6.8 KiB
C++
215 lines
6.8 KiB
C++
/*
|
|
Copyright 2006-2026 The QElectroTech Team
|
|
This file is part of QElectroTech.
|
|
|
|
QElectroTech is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 2 of the License, or
|
|
(at your option) any later version.
|
|
|
|
QElectroTech 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 General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
#include "qetscripting.h"
|
|
|
|
#include "qetscriptapi.h"
|
|
#include "../qetmessagebox.h"
|
|
#include "../qetproject.h"
|
|
#include "../utils/qetsettings.h"
|
|
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
#include <QObject>
|
|
#include <QTextStream>
|
|
|
|
#ifdef QET_HAS_SCRIPTING
|
|
#include <QJSEngine>
|
|
#include <chrono>
|
|
#include <condition_variable>
|
|
#include <mutex>
|
|
#include <thread>
|
|
#endif
|
|
|
|
namespace {
|
|
QTextStream out(stdout);
|
|
QTextStream err(stderr);
|
|
}
|
|
|
|
namespace QetScripting {
|
|
|
|
bool isRunRequest(const QStringList &args)
|
|
{
|
|
return args.contains(QStringLiteral("--run"));
|
|
}
|
|
|
|
#ifdef QET_HAS_SCRIPTING
|
|
|
|
namespace {
|
|
/**
|
|
@brief refusalMessage
|
|
What to tell somebody whose script was not run, and how to change
|
|
that. Written once because the command line and the graphical
|
|
editor both need to say it, and an explanation that names only one
|
|
of the two ways out sends half the people down the wrong path.
|
|
*/
|
|
QString refusalMessage()
|
|
{
|
|
return QObject::tr(
|
|
"Les scripts sont désactivés.\n\n"
|
|
"Un script a accès à l'ensemble du projet et peut écrire des "
|
|
"fichiers, aussi cette fonction est-elle désactivée par défaut.\n\n"
|
|
"Pour l'activer : Configurer QElectroTech > Général > Projets, "
|
|
"ou définir la variable d'environnement QET_ENABLE_SCRIPTING=1 "
|
|
"pour une exécution sans interface (CI, traitement par lot).");
|
|
}
|
|
}
|
|
|
|
int run(const QStringList &args)
|
|
{
|
|
if (!QetSettings::scriptingEnabled()) {
|
|
err << refusalMessage() << "\n";
|
|
return 3;
|
|
}
|
|
|
|
const int idx = args.indexOf(QStringLiteral("--run"));
|
|
const QString script_path = args.value(idx + 1);
|
|
const QString project_path = args.value(idx + 2);
|
|
if (script_path.isEmpty() || project_path.isEmpty()) {
|
|
err << "Usage: qelectrotech --run <script.js> <project.qet>\n";
|
|
return 2;
|
|
}
|
|
if (!QFileInfo::exists(script_path)) {
|
|
err << "Script not found: " << script_path << "\n";
|
|
return 2;
|
|
}
|
|
if (!QFileInfo::exists(project_path)) {
|
|
err << "Project not found: " << project_path << "\n";
|
|
return 2;
|
|
}
|
|
|
|
QETProject project(project_path);
|
|
if (project.state() != QETProject::Ok) {
|
|
err << "Failed to open project: " << project_path
|
|
<< " (state " << project.state() << ")\n";
|
|
return 1;
|
|
}
|
|
|
|
return runOnProject(script_path, &project, nullptr) ? 0 : 1;
|
|
}
|
|
|
|
namespace {
|
|
// A runaway script (an infinite loop, or just a very slow one) would
|
|
// otherwise freeze the GUI forever, or hang a CI job running --run with
|
|
// no way out. QJSEngine::setInterrupted() is documented callable from
|
|
// another thread; the engine polls it during evaluation and returns an
|
|
// error QJSValue, which the normal error-reporting path below already
|
|
// handles.
|
|
constexpr int kScriptTimeoutMs = 30000;
|
|
}
|
|
|
|
bool runOnProject(const QString &scriptPath, QETProject *project, DiagramView *view)
|
|
{
|
|
// Checked here as well as at each caller, deliberately: this is the
|
|
// one function that actually evaluates JavaScript, so it is the one
|
|
// place a future caller cannot forget to ask. The callers check first
|
|
// only to give a better answer than this one can -- a usable exit code
|
|
// on the command line, an offer to switch the setting on in the editor.
|
|
if (!QetSettings::scriptingEnabled()) {
|
|
err << refusalMessage() << "\n";
|
|
if (view) {
|
|
QET::QetMessageBox::warning(nullptr, QObject::tr("Script"),
|
|
refusalMessage());
|
|
}
|
|
return false;
|
|
}
|
|
|
|
QFile file(scriptPath);
|
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
|
err << "Cannot open script: " << scriptPath << "\n";
|
|
return false;
|
|
}
|
|
const QString source = QString::fromUtf8(file.readAll());
|
|
file.close();
|
|
|
|
QJSEngine engine;
|
|
auto *api = new QetScriptApi(project, view, &engine);
|
|
QJSValue qet_value = engine.newQObject(api);
|
|
// newQObject() takes ownership by default (QJSEngine::JavaScriptOwnership),
|
|
// which would delete api as soon as the engine's GC decides to -- api's
|
|
// real owner is the engine itself via the parent-child relationship set
|
|
// above, so keep the engine, not the GC, in charge of its lifetime.
|
|
engine.setObjectOwnership(api, QJSEngine::CppOwnership);
|
|
engine.globalObject().setProperty(QStringLiteral("qet"), qet_value);
|
|
|
|
// wait_for(), not sleep_for(): a script that finishes well inside the
|
|
// timeout must let the watchdog thread wake immediately, not force
|
|
// every run -- including a fast, successful one -- to block on join()
|
|
// for the full budget. Caught by testing this against a one-line
|
|
// script: it took the full 30 seconds to exit before this fix.
|
|
bool finished = false;
|
|
std::mutex mtx;
|
|
std::condition_variable cv;
|
|
std::thread watchdog([&]() {
|
|
std::unique_lock<std::mutex> lock(mtx);
|
|
cv.wait_for(lock, std::chrono::milliseconds(kScriptTimeoutMs), [&finished]{ return finished; });
|
|
if (!finished) {
|
|
engine.setInterrupted(true);
|
|
}
|
|
});
|
|
|
|
QJSValue result = engine.evaluate(source, scriptPath);
|
|
{
|
|
std::lock_guard<std::mutex> lock(mtx);
|
|
finished = true;
|
|
}
|
|
cv.notify_one();
|
|
watchdog.join();
|
|
|
|
if (result.isError()) {
|
|
const QString message = QStringLiteral("Script error: %1:%2: %3")
|
|
.arg(scriptPath)
|
|
.arg(result.property(QStringLiteral("lineNumber")).toInt())
|
|
.arg(result.toString());
|
|
err << message << "\n";
|
|
// Interactive "Run Script...": stderr is invisible to a user who
|
|
// launched the GUI normally (nowhere on Windows, easy to miss
|
|
// everywhere else). Headless --run has no GUI to show this in, and
|
|
// no session for it to block.
|
|
if (view) {
|
|
QET::QetMessageBox::critical(nullptr, QObject::tr("Script"), message);
|
|
}
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
#else // !QET_HAS_SCRIPTING
|
|
|
|
// Qt::Qml was not found at configure time (see the QET_HAS_SCRIPTING probe
|
|
// in the top-level CMakeLists.txt). Compile to a clear, non-silent failure
|
|
// rather than omitting these symbols -- the same "always compiled, the
|
|
// disabled path says why" shape as the QtPdf feature guard.
|
|
|
|
int run(const QStringList &)
|
|
{
|
|
err << "This build of QElectroTech was compiled without the Qt Qml "
|
|
"module, so JavaScript scripting (--run) is not available.\n";
|
|
return 1;
|
|
}
|
|
|
|
bool runOnProject(const QString &, QETProject *, DiagramView *)
|
|
{
|
|
err << "This build of QElectroTech was compiled without the Qt Qml "
|
|
"module, so JavaScript scripting is not available.\n";
|
|
return false;
|
|
}
|
|
|
|
#endif // QET_HAS_SCRIPTING
|
|
|
|
}
|