Mercurial > hg > orthanc-python
annotate Sources/PythonLock.cpp @ 36:fd58eb5749ed
CMake simplification using DownloadOrthancFramework.cmake
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Mon, 06 Jul 2020 17:37:30 +0200 |
parents | b2bbb516056e |
children | ee76cced46a5 |
rev | line source |
---|---|
0 | 1 /** |
2 * Python plugin for Orthanc | |
3 * Copyright (C) 2017-2020 Osimis S.A., Belgium | |
4 * | |
5 * This program is free software: you can redistribute it and/or | |
6 * modify it under the terms of the GNU Affero General Public License | |
7 * as published by the Free Software Foundation, either version 3 of | |
8 * the License, or (at your option) any later version. | |
9 * | |
10 * This program is distributed in the hope that it will be useful, but | |
11 * WITHOUT ANY WARRANTY; without even the implied warranty of | |
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
13 * Affero General Public License for more details. | |
14 * | |
15 * You should have received a copy of the GNU Affero General Public License | |
16 * along with this program. If not, see <http://www.gnu.org/licenses/>. | |
17 **/ | |
18 | |
19 | |
20 #include "PythonLock.h" | |
21 | |
22 #include "PythonFunction.h" | |
23 #include "PythonModule.h" | |
24 | |
36
fd58eb5749ed
CMake simplification using DownloadOrthancFramework.cmake
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
28
diff
changeset
|
25 #include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h" |
fd58eb5749ed
CMake simplification using DownloadOrthancFramework.cmake
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
28
diff
changeset
|
26 |
fd58eb5749ed
CMake simplification using DownloadOrthancFramework.cmake
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
28
diff
changeset
|
27 #include <Compatibility.h> // For std::unique_ptr<> |
0 | 28 |
29 #include <boost/thread/mutex.hpp> | |
30 | |
31 static boost::mutex mutex_; | |
32 static PyThreadState* interpreterState_ = NULL; | |
33 static PythonLock::ModuleFunctionsInstaller moduleFunctions_ = NULL; | |
34 static PythonLock::ModuleClassesInstaller moduleClasses_ = NULL; | |
35 static std::string moduleName_; | |
36 static std::string exceptionName_; | |
28
b2bbb516056e
The "Calling Python..." info logs are disabled if "PythonVerbose" is "false"
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
13
diff
changeset
|
37 static bool verbose_ = false; |
0 | 38 |
39 | |
40 struct module_state | |
41 { | |
42 PyObject *exceptionClass_ = NULL; | |
43 }; | |
44 | |
45 #if PY_MAJOR_VERSION >= 3 | |
46 # define GETSTATE(module) ((struct module_state*) PyModule_GetState(module)) | |
47 #else | |
48 # define GETSTATE(module) (&_state) | |
49 static struct module_state _state; | |
50 #endif | |
51 | |
52 | |
13 | 53 PythonLock::PythonLock() : |
54 gstate_(PyGILState_Ensure()) | |
0 | 55 { |
56 //OrthancPlugins::LogInfo("Python lock (GIL) acquired"); | |
57 } | |
58 | |
59 | |
60 PythonLock::~PythonLock() | |
61 { | |
62 PyGILState_Release(gstate_); | |
63 //OrthancPlugins::LogInfo("Python lock (GIL) released"); | |
64 } | |
65 | |
66 | |
67 void PythonLock::ExecuteCommand(const std::string& s) | |
68 { | |
69 if (PyRun_SimpleString(s.c_str()) != 0) | |
70 { | |
71 OrthancPlugins::LogError("Error while executing a Python command"); | |
72 ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin); | |
73 } | |
74 } | |
75 | |
76 | |
77 void PythonLock::ExecuteFile(const std::string& path) | |
78 { | |
79 OrthancPlugins::MemoryBuffer buffer; | |
80 buffer.ReadFile(path); | |
81 | |
82 std::string script; | |
83 buffer.ToString(script); | |
84 | |
85 ExecuteCommand(script); | |
86 } | |
87 | |
88 | |
89 bool PythonLock::HasErrorOccurred(std::string& target) | |
90 { | |
91 if (PyErr_Occurred()) | |
92 { | |
93 PyObject *exceptionType = NULL; | |
94 PyObject *exceptionValue = NULL; | |
95 PyObject *traceback = NULL; | |
96 PyErr_Fetch(&exceptionType, &exceptionValue, &traceback); | |
97 | |
98 if (exceptionType == NULL) | |
99 { | |
100 return false; | |
101 } | |
102 | |
103 PyErr_NormalizeException(&exceptionType, &exceptionValue, &traceback); | |
104 | |
105 #if PY_MAJOR_VERSION >= 3 | |
106 if (traceback != NULL) | |
107 { | |
108 PyException_SetTraceback(exceptionValue, traceback); | |
109 } | |
110 #endif | |
111 | |
112 if (exceptionType != NULL) | |
113 { | |
114 PythonObject temp(*this, PyObject_Str(exceptionType)); | |
115 std::string s; | |
116 if (temp.ToUtf8String(s)) | |
117 { | |
118 target += s + "\n"; | |
119 } | |
120 } | |
121 | |
122 if (exceptionValue != NULL) | |
123 { | |
124 PythonObject temp(*this, PyObject_Str(exceptionValue)); | |
125 std::string s; | |
126 if (temp.ToUtf8String(s)) | |
127 { | |
128 target += s + "\n"; | |
129 } | |
130 } | |
131 | |
132 { | |
133 PythonModule module(*this, "traceback"); | |
134 PythonFunction f(*this, module, "format_tb"); | |
135 | |
136 if (traceback != NULL && | |
137 f.IsValid()) | |
138 { | |
139 PythonObject args(*this, PyTuple_New(1)); | |
140 PyTuple_SetItem(args.GetPyObject(), 0, traceback); | |
141 | |
36
fd58eb5749ed
CMake simplification using DownloadOrthancFramework.cmake
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
28
diff
changeset
|
142 std::unique_ptr<PythonObject> value(f.CallUnchecked(args.GetPyObject())); |
0 | 143 |
144 if (value->IsValid()) | |
145 { | |
146 Py_ssize_t len = PyList_Size(value->GetPyObject()); | |
147 for (Py_ssize_t i = 0; i < len; i++) | |
148 { | |
149 PythonObject item(*this, PyList_GetItem(value->GetPyObject(), i), true /* borrowed */); | |
150 std::string line; | |
151 if (item.ToUtf8String(line)) | |
152 { | |
153 target += "\n" + line; | |
154 } | |
155 } | |
156 } | |
157 } | |
158 } | |
159 | |
160 | |
161 /** | |
162 * "This call takes away a reference to each object: you must own | |
163 * a reference to each object before the call and after the call | |
164 * you no longer own these references. (If you don't understand | |
165 * this, don't use this function. I warned you.)" | |
166 * => I don't use PyErr_Restore() | |
167 **/ | |
168 | |
169 //PyErr_Restore(exceptionType, exceptionValue, traceback); | |
170 //PyErr_Clear(); | |
171 | |
172 return true; | |
173 } | |
174 else | |
175 { | |
176 return false; | |
177 } | |
178 } | |
179 | |
180 | |
181 static void RegisterException(PyObject* module, | |
182 const std::string& name) | |
183 { | |
184 struct module_state *state = GETSTATE(module); | |
185 | |
186 state->exceptionClass_ = PyErr_NewException(const_cast<char*>(name.c_str()), NULL, NULL); | |
187 if (state->exceptionClass_ == NULL) | |
188 { | |
189 Py_DECREF(module); | |
190 OrthancPlugins::LogError("Cannot create the Python exception class"); | |
191 ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); | |
192 } | |
193 | |
194 #if 1 | |
195 Py_XINCREF(state->exceptionClass_); | |
196 if (PyModule_AddObject(module, "Exception", state->exceptionClass_) < 0) | |
197 { | |
198 Py_XDECREF(state->exceptionClass_); | |
199 Py_CLEAR(state->exceptionClass_); | |
200 OrthancPlugins::LogError("Cannot create the Python exception class"); | |
201 ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); | |
202 } | |
203 #else | |
204 // Install the exception class | |
205 PyObject* dict = PyModule_GetDict(module); | |
206 PyDict_SetItemString(dict, "Exception", state->exceptionClass); | |
207 #endif | |
208 } | |
209 | |
210 | |
211 | |
212 #if PY_MAJOR_VERSION >= 3 | |
213 | |
214 static int sdk_traverse(PyObject *module, visitproc visit, void *arg) | |
215 { | |
216 Py_VISIT(GETSTATE(module)->exceptionClass_); | |
217 return 0; | |
218 } | |
219 | |
220 static int sdk_clear(PyObject *module) | |
221 { | |
222 Py_CLEAR(GETSTATE(module)->exceptionClass_); | |
223 return 0; | |
224 } | |
225 | |
226 static struct PyModuleDef moduledef = | |
227 { | |
228 PyModuleDef_HEAD_INIT, | |
229 NULL, /* m_name => TO BE FILLED */ | |
230 NULL, | |
231 sizeof(struct module_state), | |
232 NULL, /* m_methods => TO BE FILLED */ | |
233 NULL, | |
234 sdk_traverse, | |
235 sdk_clear, | |
236 NULL | |
237 }; | |
238 | |
239 | |
240 PyMODINIT_FUNC InitializeModule() | |
241 { | |
242 if (moduleFunctions_ == NULL || | |
243 moduleClasses_ == NULL || | |
244 moduleName_.empty() || | |
245 exceptionName_.empty()) | |
246 { | |
247 ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); | |
248 } | |
249 | |
250 moduledef.m_name = moduleName_.c_str(); | |
251 moduledef.m_methods = moduleFunctions_(); | |
252 | |
253 PyObject *module = PyModule_Create(&moduledef); | |
254 if (module == NULL) | |
255 { | |
256 OrthancPlugins::LogError("Cannot create a Python module"); | |
257 ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); | |
258 } | |
259 | |
260 RegisterException(module, moduleName_ + "." + exceptionName_); | |
261 moduleClasses_(module); | |
262 | |
263 return module; | |
264 } | |
265 | |
266 #else | |
267 | |
268 void InitializeModule() | |
269 { | |
270 if (moduleFunctions_ == NULL || | |
271 moduleClasses_ == NULL || | |
272 moduleName_.empty() || | |
273 exceptionName_.empty()) | |
274 { | |
275 ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); | |
276 } | |
277 | |
278 PyObject *module = Py_InitModule(moduleName_.c_str(), moduleFunctions_()); | |
279 if (module == NULL) | |
280 { | |
281 OrthancPlugins::LogError("Cannot create a Python module"); | |
282 ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); | |
283 } | |
284 | |
285 RegisterException(module, moduleName_ + "." + exceptionName_); | |
286 moduleClasses_(module); | |
287 } | |
288 | |
289 #endif | |
290 | |
291 | |
292 | |
293 void PythonLock::GlobalInitialize(const std::string& moduleName, | |
294 const std::string& exceptionName, | |
295 ModuleFunctionsInstaller moduleFunctions, | |
296 ModuleClassesInstaller moduleClasses, | |
297 bool verbose) | |
298 { | |
299 boost::mutex::scoped_lock lock(mutex_); | |
300 | |
301 if (interpreterState_ != NULL) | |
302 { | |
303 OrthancPlugins::LogError("Cannot initialize twice the Python interpreter"); | |
304 ORTHANC_PLUGINS_THROW_EXCEPTION(BadSequenceOfCalls); | |
305 } | |
306 | |
307 if (moduleClasses == NULL || | |
308 moduleFunctions == NULL) | |
309 { | |
310 ORTHANC_PLUGINS_THROW_EXCEPTION(NullPointer); | |
311 } | |
312 | |
313 if (moduleName.empty() || | |
314 exceptionName.empty()) | |
315 { | |
316 ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange); | |
317 } | |
318 | |
319 if (exceptionName.find('.') != std::string::npos) | |
320 { | |
321 OrthancPlugins::LogError("The name of the exception cannot contain \".\", found: " + | |
322 exceptionName); | |
323 ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange); | |
324 } | |
325 | |
326 moduleClasses_ = moduleClasses; | |
327 moduleFunctions_ = moduleFunctions; | |
328 moduleName_ = moduleName; | |
329 exceptionName_ = exceptionName; | |
330 | |
331 std::string executable; | |
332 | |
333 { | |
334 OrthancPlugins::OrthancString str; | |
335 str.Assign(OrthancPluginGetOrthancPath(OrthancPlugins::GetGlobalContext())); | |
336 str.ToString(executable); | |
337 } | |
338 | |
339 OrthancPlugins::LogWarning("Program name: " + executable); | |
340 | |
341 #if PY_MAJOR_VERSION == 2 | |
342 Py_SetProgramName(&executable[0]); /* optional but recommended */ | |
343 #else | |
344 std::wstring wide(executable.begin(), executable.end()); | |
345 Py_SetProgramName(&wide[0]); /* optional but recommended */ | |
346 Py_UnbufferedStdioFlag = 1; // Write immediately to stdout after "sys.stdout.flush()" (only in Python 3.x) | |
347 #endif | |
348 | |
349 Py_InspectFlag = 1; // Don't exit the Orthanc process on Python error | |
350 | |
28
b2bbb516056e
The "Calling Python..." info logs are disabled if "PythonVerbose" is "false"
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
13
diff
changeset
|
351 verbose_ = verbose; |
0 | 352 if (verbose) |
353 { | |
354 Py_VerboseFlag = 1; | |
355 } | |
356 | |
357 #if PY_MAJOR_VERSION >= 3 | |
358 PyImport_AppendInittab(moduleName_.c_str(), InitializeModule); | |
359 #endif | |
360 | |
361 #if PY_VERSION_HEX < 0x03020000 /* 3.2.0 */ | |
362 /** | |
363 * "Changed in version 3.2: This function cannot be called before | |
364 * Py_Initialize() anymore." | |
365 **/ | |
366 if (!PyEval_ThreadsInitialized()) | |
367 { | |
368 PyEval_InitThreads(); | |
369 } | |
370 #endif | |
371 | |
372 Py_InitializeEx(0 /* Python is embedded, skip signal handlers */); | |
373 | |
12 | 374 #if 0 |
0 | 375 #if PY_MAJOR_VERSION == 2 |
376 std::cout << Py_GetPath() << std::endl; | |
377 std::cout << Py_GetProgramName() << std::endl; | |
378 std::cout << Py_GetProgramFullPath() << std::endl; | |
379 #else | |
380 std::wcout << Py_GetPath() << std::endl; | |
381 std::wcout << Py_GetProgramName() << std::endl; | |
382 std::wcout << Py_GetProgramFullPath() << std::endl; | |
383 #endif | |
12 | 384 #endif |
0 | 385 |
386 #if (PY_VERSION_HEX >= 0x03020000 /* 3.2.0 */ && \ | |
387 PY_VERSION_HEX < 0x03070000 /* 3.7.0 */) | |
388 /** | |
389 * Changed in version 3.7: This function is now called by | |
390 * Py_Initialize(), so you don't have to call it yourself anymore. | |
391 **/ | |
392 if (!PyEval_ThreadsInitialized()) | |
393 { | |
394 PyEval_InitThreads(); | |
395 } | |
396 #endif | |
397 | |
398 | |
399 #if PY_MAJOR_VERSION == 2 | |
400 InitializeModule(); | |
401 #endif | |
402 | |
403 // https://fr.slideshare.net/YiLungTsai/embed-python => slide 26 | |
404 interpreterState_ = PyEval_SaveThread(); | |
405 } | |
406 | |
407 | |
408 void PythonLock::AddSysPath(const std::string& path) | |
409 { | |
410 /** | |
411 * "It is recommended that applications embedding the Python | |
412 * interpreter for purposes other than executing a single script | |
413 * pass 0 as updatepath, and update sys.path themselves if | |
414 * desired. See CVE-2008-5983." | |
415 * => Set "sys.path.append()" to the directory of the configuration file by default | |
416 **/ | |
417 | |
418 PythonLock lock; | |
419 | |
420 /** | |
421 * Initialization of "sys.path.append()" must be done before loading | |
422 * any module. | |
423 **/ | |
424 | |
425 PyObject *sysPath = PySys_GetObject(const_cast<char*>("path")); | |
426 if (sysPath == NULL) | |
427 { | |
428 OrthancPlugins::LogError("Cannot find sys.path"); | |
429 ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); | |
430 } | |
431 | |
432 PyObject* pyPath = PyUnicode_FromString(path.c_str()); | |
433 int result = PyList_Insert(sysPath, 0, pyPath); | |
434 Py_DECREF(pyPath); | |
435 | |
436 if (result != 0) | |
437 { | |
438 OrthancPlugins::LogError("Cannot run sys.path.append()"); | |
439 ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); | |
440 } | |
441 } | |
442 | |
443 | |
444 void PythonLock::GlobalFinalize() | |
445 { | |
446 boost::mutex::scoped_lock lock(mutex_); | |
447 | |
448 if (interpreterState_ != NULL) | |
449 { | |
450 PyEval_RestoreThread(interpreterState_); | |
451 interpreterState_ = NULL; | |
452 } | |
453 | |
454 Py_Finalize(); | |
455 } | |
456 | |
457 | |
458 void PythonLock::RaiseException(PyObject* module, | |
459 OrthancPluginErrorCode code) | |
460 { | |
461 if (code != OrthancPluginErrorCode_Success) | |
462 { | |
463 const char* message = OrthancPluginGetErrorDescription(OrthancPlugins::GetGlobalContext(), code); | |
464 | |
465 struct module_state *state = GETSTATE(module); | |
466 if (state->exceptionClass_ == NULL) | |
467 { | |
468 OrthancPlugins::LogError("No Python exception has been registered"); | |
469 } | |
470 else | |
471 { | |
472 PyErr_SetString(state->exceptionClass_, message); | |
473 } | |
474 } | |
475 } | |
28
b2bbb516056e
The "Calling Python..." info logs are disabled if "PythonVerbose" is "false"
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
13
diff
changeset
|
476 |
b2bbb516056e
The "Calling Python..." info logs are disabled if "PythonVerbose" is "false"
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
13
diff
changeset
|
477 |
b2bbb516056e
The "Calling Python..." info logs are disabled if "PythonVerbose" is "false"
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
13
diff
changeset
|
478 void PythonLock::LogCall(const std::string& message) |
b2bbb516056e
The "Calling Python..." info logs are disabled if "PythonVerbose" is "false"
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
13
diff
changeset
|
479 { |
b2bbb516056e
The "Calling Python..." info logs are disabled if "PythonVerbose" is "false"
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
13
diff
changeset
|
480 /** |
b2bbb516056e
The "Calling Python..." info logs are disabled if "PythonVerbose" is "false"
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
13
diff
changeset
|
481 * For purity, this function should lock the global "mutex_", but |
b2bbb516056e
The "Calling Python..." info logs are disabled if "PythonVerbose" is "false"
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
13
diff
changeset
|
482 * "verbose_" cannot change after the initial call to |
b2bbb516056e
The "Calling Python..." info logs are disabled if "PythonVerbose" is "false"
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
13
diff
changeset
|
483 * "GlobalInitialize()". |
b2bbb516056e
The "Calling Python..." info logs are disabled if "PythonVerbose" is "false"
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
13
diff
changeset
|
484 **/ |
b2bbb516056e
The "Calling Python..." info logs are disabled if "PythonVerbose" is "false"
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
13
diff
changeset
|
485 |
b2bbb516056e
The "Calling Python..." info logs are disabled if "PythonVerbose" is "false"
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
13
diff
changeset
|
486 if (verbose_) |
b2bbb516056e
The "Calling Python..." info logs are disabled if "PythonVerbose" is "false"
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
13
diff
changeset
|
487 { |
b2bbb516056e
The "Calling Python..." info logs are disabled if "PythonVerbose" is "false"
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
13
diff
changeset
|
488 OrthancPlugins::LogInfo(message); |
b2bbb516056e
The "Calling Python..." info logs are disabled if "PythonVerbose" is "false"
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
13
diff
changeset
|
489 } |
b2bbb516056e
The "Calling Python..." info logs are disabled if "PythonVerbose" is "false"
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
13
diff
changeset
|
490 } |