Code_TYMPAN  4.4.0
Industrial site acoustic simulation
subprocess_util.cpp
Go to the documentation of this file.
1 /*
2  * Copyright (C) <2012-2024> <EDF-DTG> <FRANCE>
3  * This file is part of Code_TYMPAN (R).
4  * Code_TYMPAN (R) is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  * Code_TYMPAN (R) is distributed in the hope that it will be useful,
9  * but WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11  * See the GNU General Public License for more details.
12  * You should have received a copy of the GNU General Public License along
13  * with Code_TYMPAN (R). If not, see <https://www.gnu.org/licenses/>.
14  */
15 
22 #include <QFile>
23 #include <QCoreApplication>
24 #include <QProcess>
25 #include <QDir>
26 #include <QRegularExpression>
27 #include <QEventLoop>
28 #include <QTimer>
29 #include <QElapsedTimer>
30 #include <atomic>
31 
32 #include "Tympan/core/defines.h"
33 #include "Tympan/core/chrono.h"
34 #include "Tympan/core/logging.h"
35 #include "Tympan/core/exceptions.h"
36 #include "subprocess_util.h"
37 
38 #if defined _DEBUG
39  #define TYMPAN_REL_CYTHON_PATH "cython_d"
40 #else // undefined _DEBUG (release install)
41  #define TYMPAN_REL_CYTHON_PATH "cython"
42 #endif // defined _DEBUG
43 
44 #define COMPUTATION_TIMEOUT 10000 // In ms
45 
46 // flag global au TU (non exporté) + fonction publique pour le setter
47 static std::atomic_bool g_python_cancel_requested{false};
48 
50 {
51  g_python_cancel_requested.store(true, std::memory_order_relaxed);
52 }
53 
55 {
56  return g_python_cancel_requested.load(std::memory_order_relaxed);
57 }
58 
59 // Returns the base directory for locating Cython libs and runtime deps.
60 // Priority: env TYMPAN_APP_DIR -> QCoreApplication::applicationDirPath() if available -> current working dir.
61 static QString _tympan_app_dir()
62 {
63  const QProcessEnvironment pe = QProcessEnvironment::systemEnvironment();
64  if (pe.contains("TYMPAN_INSTALL_PATH"))
65  {
66  return QDir::cleanPath(pe.value("TYMPAN_INSTALL_PATH"));
67  }
68  if (QCoreApplication::instance())
69  {
70  return QCoreApplication::applicationDirPath();
71  }
72  // Headless fallback: current working directory
73  return QDir::currentPath();
74 }
75 
77 {
78  QStringList env(QProcess::systemEnvironment());
79  // Absolute path to Tympan install directory
80  QString cythonlibs_path(_tympan_app_dir());
81  // Relative path from Tympan install directory to cython modules
82  cythonlibs_path.append("/");
83  cythonlibs_path.append(TYMPAN_REL_CYTHON_PATH);
84  cythonlibs_path = QDir::toNativeSeparators(cythonlibs_path);
85  // Set new PYTHONPATH
86  QRegularExpression pythonpath_regexp("^PYTHONPATH=(.*)", QRegularExpression::CaseInsensitiveOption);
87  int pythonpath_index = env.indexOf(pythonpath_regexp);
88  QString pythonpath;
89  if (pythonpath_index > 0)
90  {
91  pythonpath = env[pythonpath_index];
92  // Check the presence of cython libs in the PYTHONPATH
93  if (pythonpath != "PYTHONPATH=")
94  {
95 #if TY_PLATFORM == TY_PLATFORM_WIN32 || TY_PLATFORM == TY_PLATFORM_WIN64
96  pythonpath.append(";");
97 #else
98  pythonpath.append(":");
99 #endif
100  }
101  pythonpath.append(cythonlibs_path);
102  env.removeAt(pythonpath_index);
103  }
104  else
105  {
106  pythonpath = "PYTHONPATH=";
107  pythonpath.append(cythonlibs_path);
108  }
109  env.append(pythonpath);
110 #if TY_PLATFORM == TY_PLATFORM_WIN32 || TY_PLATFORM == TY_PLATFORM_WIN64
111  // Add path to dynamic libraries needed by Tympan and thus needed by pytam (cython library)
112  QRegularExpression path_regexp("^Path=(.*)", QRegularExpression::CaseInsensitiveOption);
113  int path_index = env.indexOf(path_regexp);
114  QString path = env[path_index];
115  QRegularExpression equal_regexp(
116  "=",
117  QRegularExpression::CaseInsensitiveOption); // Position of the "=" symbol in the path QString
118  int equal_index = path.indexOf(equal_regexp) + 1;
119  QString application_path(_tympan_app_dir());
120  application_path = QDir::toNativeSeparators(application_path);
121  path.insert(equal_index, application_path + ";"); // Insert the application path right after "PATH =".
122  env.removeAt(path_index);
123  env.append(path);
124 #endif
125  return env;
126 }
127 
129 {
130  QStringList env(QProcess::systemEnvironment());
131  // TYMPAN_PYTHON_INTERP environment variable must be set to the path to
132  // python 3 interpreter (ex: "C:\Python3\python.exe")
133  int python_interp_idx = env.indexOf(QRegularExpression("^TYMPAN_PYTHON_INTERP=(.*)"));
134  if (python_interp_idx < 0)
135  {
136  throw tympan::invalid_data(
137  "Can't access python interpreter. TYMPAN_PYTHON_INTERP environment variable is not set.");
138  }
139  QString python_interp_path = env.at(python_interp_idx).split('=')[1].remove("\"");
140  QFile python_interp(python_interp_path);
141  if (!python_interp.exists())
142  {
143  throw tympan::invalid_data("Can't access python interpreter. TYMPAN_PYTHON_INTERP environment "
144  "variable is not correctly set.");
145  }
146  return python_interp_path;
147 }
148 
149 std::string _read_environment_variables(QStringList env)
150 {
151  std::string variables = "\nVariables d'environnement:\n";
152  int pythonpath_index =
153  env.indexOf(QRegularExpression("^PYTHONPATH=(.*)", QRegularExpression::CaseInsensitiveOption));
154  if (pythonpath_index >= 0)
155  {
156  variables += env[pythonpath_index].toStdString() + "\n";
157  }
158  else
159  {
160  variables += "PYTHONPATH absente\n";
161  }
162 #if TY_PLATFORM == TY_PLATFORM_WIN32 || TY_PLATFORM == TY_PLATFORM_WIN64
163  int path_index = env.indexOf(QRegularExpression("^Path=(.*)", QRegularExpression::CaseInsensitiveOption));
164  if (path_index >= 0)
165  {
166  variables += env[path_index].toStdString() + "\n";
167  }
168  else
169  {
170  variables += "Path absente\n";
171  }
172  int python_interp_index = env.indexOf(QRegularExpression("^TYMPAN_PYTHON_INTERP=(.*)"));
173  if (python_interp_index >= 0)
174  {
175  variables += env[python_interp_index].toStdString() + "\n";
176  }
177  else
178  {
179  variables += "TYMPAN_PYTHON_INTERP absente\n";
180  }
181 #endif
182  return variables;
183 }
184 
186 {
187  QStringList appli_env(QProcess::systemEnvironment());
188  int tympan_debug_idx = appli_env.indexOf(QRegularExpression("^TYMPAN_DEBUG=(.*)"));
189  if (tympan_debug_idx >= 0)
190  {
191  QString debug_option = appli_env[tympan_debug_idx].split('=')[1];
192  if (debug_option.contains("keep_tmp_files", Qt::CaseInsensitive))
193  {
194  return true;
195  }
196  }
197  return false;
198 }
199 
200 bool init_tmp_file(QTemporaryFile& tmp_file, bool keep_file)
201 {
202  if (!tmp_file.open())
203  {
204  return false;
205  }
206  tmp_file.close();
207  // Prevent from automatic file removal
208  if (keep_file)
209  tmp_file.setAutoRemove(false);
210  return true;
211 }
212 
213 bool python(QStringList args, std::string& error_msg)
214 {
215  g_python_cancel_requested.store(false, std::memory_order_relaxed);
217 
218  // Start stopwatch
219  OChronoTime startTime;
220  logger.debug("Lancement du script python: %s", args.join(" ").toStdString().c_str());
221 
222  QProcess python;
223  float comp_duration = 0.0f;
224 
225  // Set PYTHONPATH for the Python subprocess
226  QStringList env(_python_qprocess_environment());
227  python.setEnvironment(env);
228  python.setProcessChannelMode(QProcess::SeparateChannels); // also useful for stage B
229 
230  // Resolve Python interpreter
231  QString python_interp;
232  try
233  {
234  python_interp = _get_python_interp();
235  }
236  catch (const tympan::invalid_data&)
237  {
238  error_msg = "L'interpreteur python n'a pas pu etre trouve.\nVeuillez verifier que la variable "
239  "d'environnement TYMPAN_PYTHON_INTERP est correctement positionnee\n";
240  error_msg.append(_read_environment_variables(env));
241  return false;
242  }
243 
244  // Start process
245  python.start(python_interp, args);
246 
247  // Blocking monitoring loop: short polls + periodic logging + cooperative cancel
248  const int poll_ms = 100; // short poll for responsive cancel
249  bool terminate_sent = false;
250  qint64 kill_deadline_ms = -1;
251 
252  QElapsedTimer elapsed;
253  elapsed.start();
254  qint64 last_log_ms = 0;
255 
256  while (true)
257  {
258  if (python.waitForFinished(poll_ms))
259  {
260  break; // finished (normal or error) → exit loop
261  }
262 
263  // Periodic log (keeps previous behavior without relying on event loop)
264  const qint64 now = elapsed.elapsed();
265  if (now - last_log_ms >= static_cast<qint64>(COMPUTATION_TIMEOUT))
266  {
267  comp_duration = static_cast<float>(now) / 1000.0f;
268  logger.info("Le script python s'execute encore apres %.0f secondes", comp_duration);
269  last_log_ms = now;
270  }
271 
272  // Cancellation path
273  if (g_python_cancel_requested.load(std::memory_order_relaxed))
274  {
275  if (!terminate_sent)
276  {
277  logger.warning("Demande d'annulation reçue : envoi de terminate() au sous-processus Python.");
278  python.terminate();
279  terminate_sent = true;
280  kill_deadline_ms = now + 1500; // grace period
281  }
282  else if (kill_deadline_ms >= 0 && now >= kill_deadline_ms &&
283  python.state() != QProcess::NotRunning)
284  {
285  logger.warning("Le sous-processus Python ne s'est pas terminé : kill().");
286  python.kill();
287  // After kill(), either it will finish or be forcefully ended on next poll
288  }
289  }
290  }
291 
292  // --- Read outputs + status ---
293  const QString std_error = python.readAllStandardError();
294  const int exit_code = python.exitCode();
295 
296  if (python.exitStatus() != QProcess::NormalExit || exit_code != 0)
297  {
298  error_msg = "Le sous-process python s'est terminé avec le code d'erreur ";
299  error_msg += std::to_string(static_cast<long long>(exit_code));
300  error_msg += "\n";
301  error_msg += std_error.toStdString();
302  error_msg += _read_environment_variables(env);
303  error_msg += "Veuillez lire tympan.log pour plus d'information.\n";
304  logger.error("%s", error_msg.c_str());
305  return false;
306  }
307  else
308  {
309  logger.info("Le sous-processus Python s'est terminé correctement");
310  if (!std_error.isEmpty())
311  logger.warning(std_error.toStdString().c_str());
312  }
313 
314  // Compute time
315  OChronoTime endTime;
316  OChronoTime duration = endTime - startTime;
317  unsigned long second = duration.getTime() / 1000;
318  unsigned long millisecond = duration.getTime() - second * 1000;
319  logger.info("Temps de calcul : %02ld,%03ld sec. (%ld msec.)", second, millisecond, duration.getTime());
320  return true;
321 }
unsigned long getTime() const
Definition: chrono.h:42
virtual void debug(const char *message,...)
Definition: logging.cpp:151
virtual void warning(const char *message,...)
Definition: logging.cpp:119
virtual void error(const char *message,...)
Definition: logging.cpp:127
static OMessageManager * get()
Definition: logging.cpp:108
virtual void info(const char *message,...)
Definition: logging.cpp:143
Utilities to handle exceptions and to pretty-print value.
The base exception class for errors due to invalid data.
Definition: exceptions.h:75
bool python(QStringList args, std::string &error_msg)
Launch a Python subprocess and wait for it using a non-blocking UI loop.
bool must_keep_tmp_files()
Tell whether temporary files should be preserved (debug mode).
bool init_tmp_file(QTemporaryFile &tmp_file, bool keep_file)
Create and initialize a QTemporaryFile according to the current policy.
QString _get_python_interp()
std::string _read_environment_variables(QStringList env)
QStringList _python_qprocess_environment()
void python_request_cancel()
Request cancellation of the currently running Python subprocess (UI hook, stage A).
#define TYMPAN_REL_CYTHON_PATH
bool python_cancel_was_requested()
Query whether a cancellation was requested since the last call to python().
#define COMPUTATION_TIMEOUT
Utilities to interact with Python subprocesses from the Tympan application.