33#include <QRegularExpression>
37#include <QElapsedTimer>
80 m_lineCb = std::move(cb);
87 m_progressCb = std::move(cb);
93 const QStringList& args)
96 if (m_config.unbuffered) {
97 fullArgs << QStringLiteral(
"-u");
99 fullArgs << scriptPath << args;
101 return execute(fullArgs);
107 const QStringList& args)
109 QStringList fullArgs;
110 if (m_config.unbuffered) {
111 fullArgs << QStringLiteral(
"-u");
113 fullArgs << QStringLiteral(
"-c") << code;
116 return execute(fullArgs);
124 proc.start(m_config.pythonExe, {QStringLiteral(
"--version")});
125 return proc.waitForFinished(5000) &&
126 proc.exitStatus() == QProcess::NormalExit &&
127 proc.exitCode() == 0;
135 proc.start(m_config.pythonExe, {QStringLiteral(
"--version")});
136 if (!proc.waitForFinished(5000) || proc.exitCode() != 0) {
140 QString out = QString::fromUtf8(proc.readAllStandardOutput()).trimmed();
141 if (out.startsWith(QStringLiteral(
"Python "))) {
152 static const QRegularExpression validPkg(QStringLiteral(
"^[A-Za-z_][A-Za-z0-9_.]*$"));
153 if (!validPkg.match(packageName).hasMatch()) {
154 qWarning() <<
"[PythonRunner] Invalid package name:" << packageName;
161 proc.start(m_config.pythonExe,
162 {QStringLiteral(
"-c"),
163 QStringLiteral(
"import importlib.util, sys; "
164 "sys.exit(0 if importlib.util.find_spec('%1') else 1)")
166 if (!proc.waitForFinished(10000)) {
168 proc.waitForFinished(1000);
171 return proc.exitStatus() == QProcess::NormalExit &&
172 proc.exitCode() == 0;
179 if (m_config.venvDir.isEmpty()) {
183 return QDir(m_config.venvDir).absoluteFilePath(QStringLiteral(
"Scripts/python.exe"));
185 return QDir(m_config.venvDir).absoluteFilePath(QStringLiteral(
"bin/python"));
195 if (m_config.venvDir.isEmpty()) {
201 bool venvExists = QFileInfo::exists(venvPython);
205 qDebug() <<
"[PythonRunner] Creating virtual environment at:" << m_config.venvDir;
208 venvProc.start(m_config.pythonExe,
209 {QStringLiteral(
"-m"), QStringLiteral(
"venv"), m_config.venvDir});
211 if (!venvProc.waitForStarted(10000)) {
212 result.
stdErr = QStringLiteral(
"Failed to start venv creation: ") + venvProc.errorString();
213 qWarning() <<
"[PythonRunner]" << result.
stdErr;
217 if (!venvProc.waitForFinished(120000)) {
218 result.
stdErr = QStringLiteral(
"Venv creation timed out.");
220 venvProc.waitForFinished(5000);
221 qWarning() <<
"[PythonRunner]" << result.
stdErr;
225 if (venvProc.exitCode() != 0) {
226 result.
exitCode = venvProc.exitCode();
227 result.
stdErr = QString::fromUtf8(venvProc.readAllStandardError());
228 qWarning() <<
"[PythonRunner] venv creation failed:" << result.
stdErr;
232 qDebug() <<
"[PythonRunner] Virtual environment created.";
234 qDebug() <<
"[PythonRunner] Virtual environment already exists at:" << m_config.venvDir;
239 bool needsInstall =
false;
243 QString pipExe = QDir(m_config.venvDir).absoluteFilePath(QStringLiteral(
"Scripts/pip.exe"));
245 QString pipExe = QDir(m_config.venvDir).absoluteFilePath(QStringLiteral(
"bin/pip"));
248 if (!m_config.packageDir.isEmpty()) {
250 QString tomlPath = QDir(m_config.packageDir).absoluteFilePath(QStringLiteral(
"pyproject.toml"));
251 if (QFileInfo::exists(tomlPath)) {
252 pipArgs << QStringLiteral(
"install") << m_config.packageDir;
254 qDebug() <<
"[PythonRunner] Installing from pyproject.toml in:" << m_config.packageDir;
256 qWarning() <<
"[PythonRunner] packageDir set but no pyproject.toml found at:" << tomlPath;
260 if (!needsInstall && !m_config.requirementsFile.isEmpty()) {
261 if (QFileInfo::exists(m_config.requirementsFile)) {
262 pipArgs << QStringLiteral(
"install")
263 << QStringLiteral(
"-r") << m_config.requirementsFile;
265 qDebug() <<
"[PythonRunner] Installing from requirements.txt:" << m_config.requirementsFile;
267 qWarning() <<
"[PythonRunner] requirementsFile not found:" << m_config.requirementsFile;
273 pipProc.start(pipExe, pipArgs);
275 if (!pipProc.waitForStarted(10000)) {
276 result.
stdErr = QStringLiteral(
"Failed to start pip: ") + pipProc.errorString();
277 qWarning() <<
"[PythonRunner]" << result.
stdErr;
282 if (!pipProc.waitForFinished(1800000)) {
283 result.
stdErr = QStringLiteral(
"pip install timed out.");
286 pipProc.waitForFinished(5000);
287 qWarning() <<
"[PythonRunner]" << result.
stdErr;
291 result.
stdOut = QString::fromUtf8(pipProc.readAllStandardOutput());
292 result.
stdErr = QString::fromUtf8(pipProc.readAllStandardError());
293 result.
exitCode = pipProc.exitCode();
295 if (pipProc.exitCode() != 0) {
296 qWarning() <<
"[PythonRunner] pip install failed with code" << pipProc.exitCode();
297 qWarning() <<
"[PythonRunner] stderr:" << result.
stdErr;
301 qDebug() <<
"[PythonRunner] Dependencies installed successfully.";
305 m_config.pythonExe = venvPython;
309 qDebug() <<
"[PythonRunner] Using venv Python:" << m_config.pythonExe;
316 const QStringList& args)
325 return run(scriptPath, args);
335 process.setProcessChannelMode(QProcess::SeparateChannels);
339 process.setWorkingDirectory(m_config.
workingDir);
344 QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
345 for (
const QString& kv : m_config.
extraEnv) {
346 int idx = kv.indexOf(QLatin1Char(
'='));
348 env.insert(kv.left(idx), kv.mid(idx + 1));
351 process.setProcessEnvironment(env);
354 qDebug() <<
"[PythonRunner] Launching:" << m_config.pythonExe << fullArgs.join(QLatin1Char(
' '));
356 process.start(m_config.pythonExe, fullArgs);
358 if (!process.waitForStarted(10000)) {
359 result.
stdErr = QStringLiteral(
"Failed to start Python process: ") + process.errorString();
360 qWarning() <<
"[PythonRunner]" << result.
stdErr;
365 QByteArray stdOutBuf, stdErrBuf;
366 QStringList stdOutLines, stdErrLines;
368 auto processLines = [&](QByteArray& buffer,
int channel) {
369 while (buffer.contains(
'\n')) {
370 int idx = buffer.indexOf(
'\n');
371 QString line = QString::fromUtf8(buffer.left(idx));
372 buffer.remove(0, idx + 1);
382 m_lineCb(channel, line);
389 if (parseProgressLine(line, pct, msg)) {
392 m_progressCb(pct, msg);
399 qDebug() <<
"[Python stdout]" << line;
401 qWarning() <<
"[Python stderr]" << line;
407 QElapsedTimer elapsed;
410 while (!process.waitForFinished(100)) {
411 stdOutBuf.append(process.readAllStandardOutput());
412 stdErrBuf.append(process.readAllStandardError());
413 processLines(stdOutBuf, 0);
414 processLines(stdErrBuf, 1);
417 if (m_config.timeoutMsec > 0 && elapsed.elapsed() >= m_config.timeoutMsec) {
419 stdOutBuf.append(process.readAllStandardOutput());
420 stdErrBuf.append(process.readAllStandardError());
421 processLines(stdOutBuf, 0);
422 processLines(stdErrBuf, 1);
425 result.
stdErr += QStringLiteral(
"\nProcess timed out after %1 ms.").arg(m_config.timeoutMsec);
427 process.waitForFinished(5000);
428 qWarning() <<
"[PythonRunner] Process timed out after" << m_config.timeoutMsec <<
"ms.";
434 stdOutBuf.append(process.readAllStandardOutput());
435 stdErrBuf.append(process.readAllStandardError());
437 processLines(stdOutBuf, 0);
438 processLines(stdErrBuf, 1);
440 if (!stdOutBuf.isEmpty()) {
441 QString line = QString::fromUtf8(stdOutBuf);
447 if (!stdErrBuf.isEmpty()) {
448 QString line = QString::fromUtf8(stdErrBuf);
456 result.
stdOut = stdOutLines.join(QLatin1Char(
'\n'));
457 result.
stdErr += stdErrLines.join(QLatin1Char(
'\n'));
459 result.
exitCode = process.exitCode();
461 process.exitStatus() == QProcess::NormalExit &&
465 qDebug() <<
"[PythonRunner] Script finished successfully.";
467 qWarning() <<
"[PythonRunner] Script exited with code" << result.
exitCode;
477bool PythonRunner::parseProgressLine(
const QString& line,
482 static const QRegularExpression re(
483 QStringLiteral(R
"(^\[progress\]\s+(\d+(?:\.\d+)?)\s*%\s*(.*)?$)"),
484 QRegularExpression::CaseInsensitiveOption);
486 QRegularExpressionMatch match = re.match(line.trimmed());
487 if (!match.hasMatch()) {
492 pct = match.captured(1).toFloat(&ok);
496 msg = match.captured(2).trimmed();
Embedding-free launcher that runs MNE-Python (or any user) scripts as a child process and streams the...
Shared utilities (I/O helpers, spectral analysis, layout management, warp algorithms).
std::function< void(float pct, const QString &msg)> PythonProgressCallback
std::function< void(int channel, const QString &line)> PythonLineCallback
Script execution result container.
Script execution configuration.
const PythonRunnerConfig & config() const
void setLineCallback(PythonLineCallback cb)
void setConfig(const PythonRunnerConfig &config)
QString pythonVersion() const
QString venvPythonPath() const
bool isPythonAvailable() const
void progressUpdated(float pct, const QString &msg)
bool isPackageAvailable(const QString &packageName) const
PythonRunnerResult runCode(const QString &code, const QStringList &args={})
PythonRunnerResult ensureVenv()
PythonRunnerResult run(const QString &scriptPath, const QStringList &args={})
void setProgressCallback(PythonProgressCallback cb)
void finished(const UTILSLIB::PythonRunnerResult &result)
PythonRunnerResult runInVenv(const QString &scriptPath, const QStringList &args={})
PythonRunner(QObject *pParent=nullptr)
void lineReceived(int channel, const QString &line)