Mercurial > hg > orthanc
comparison Plugins/Engine/OrthancPlugins.cpp @ 2000:39329372b667
Speedup in plugins by removing unnecessary locks
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Tue, 31 May 2016 12:19:53 +0200 |
parents | 364cc624eb65 |
children | 6301bbcbcaed |
comparison
equal
deleted
inserted
replaced
1999:364cc624eb65 | 2000:39329372b667 |
---|---|
248 } | 248 } |
249 }; | 249 }; |
250 } | 250 } |
251 | 251 |
252 | 252 |
253 struct OrthancPlugins::PImpl | 253 class OrthancPlugins::PImpl |
254 { | 254 { |
255 private: | |
256 boost::mutex contextMutex_; | |
257 ServerContext* context_; | |
258 | |
259 | |
260 public: | |
255 class RestCallback : public boost::noncopyable | 261 class RestCallback : public boost::noncopyable |
256 { | 262 { |
257 private: | 263 private: |
258 boost::regex regex_; | 264 boost::regex regex_; |
259 OrthancPluginRestCallback callback_; | 265 OrthancPluginRestCallback callback_; |
299 } | 305 } |
300 } | 306 } |
301 }; | 307 }; |
302 | 308 |
303 | 309 |
310 class ServerContextLock | |
311 { | |
312 private: | |
313 boost::mutex::scoped_lock lock_; | |
314 ServerContext* context_; | |
315 | |
316 public: | |
317 ServerContextLock(PImpl& that) : | |
318 lock_(that.contextMutex_), | |
319 context_(that.context_) | |
320 { | |
321 if (context_ == NULL) | |
322 { | |
323 throw OrthancException(ErrorCode_DatabaseNotInitialized); | |
324 } | |
325 } | |
326 | |
327 ServerContext& GetContext() | |
328 { | |
329 assert(context_ != NULL); | |
330 return *context_; | |
331 } | |
332 }; | |
333 | |
334 | |
335 void SetServerContext(ServerContext* context) | |
336 { | |
337 boost::mutex::scoped_lock(contextMutex_); | |
338 context_ = context; | |
339 } | |
340 | |
341 | |
304 typedef std::pair<std::string, _OrthancPluginProperty> Property; | 342 typedef std::pair<std::string, _OrthancPluginProperty> Property; |
305 typedef std::list<RestCallback*> RestCallbacks; | 343 typedef std::list<RestCallback*> RestCallbacks; |
306 typedef std::list<OrthancPluginOnStoredInstanceCallback> OnStoredCallbacks; | 344 typedef std::list<OrthancPluginOnStoredInstanceCallback> OnStoredCallbacks; |
307 typedef std::list<OrthancPluginOnChangeCallback> OnChangeCallbacks; | 345 typedef std::list<OrthancPluginOnChangeCallback> OnChangeCallbacks; |
308 typedef std::list<OrthancPluginIncomingHttpRequestFilter> IncomingHttpRequestFilters; | 346 typedef std::list<OrthancPluginIncomingHttpRequestFilter> IncomingHttpRequestFilters; |
309 typedef std::map<Property, std::string> Properties; | 347 typedef std::map<Property, std::string> Properties; |
310 | 348 |
311 PluginsManager manager_; | 349 PluginsManager manager_; |
312 ServerContext* context_; | 350 |
313 RestCallbacks restCallbacks_; | 351 RestCallbacks restCallbacks_; |
314 OnStoredCallbacks onStoredCallbacks_; | 352 OnStoredCallbacks onStoredCallbacks_; |
315 OnChangeCallbacks onChangeCallbacks_; | 353 OnChangeCallbacks onChangeCallbacks_; |
316 OrthancPluginFindCallback findCallback_; | 354 OrthancPluginFindCallback findCallback_; |
317 OrthancPluginWorklistCallback worklistCallback_; | 355 OrthancPluginWorklistCallback worklistCallback_; |
318 OrthancPluginDecodeImageCallback decodeImageCallback_; | 356 OrthancPluginDecodeImageCallback decodeImageCallback_; |
319 _OrthancPluginMoveCallback moveCallbacks_; | 357 _OrthancPluginMoveCallback moveCallbacks_; |
320 IncomingHttpRequestFilters incomingHttpRequestFilters_; | 358 IncomingHttpRequestFilters incomingHttpRequestFilters_; |
321 std::auto_ptr<StorageAreaFactory> storageArea_; | 359 std::auto_ptr<StorageAreaFactory> storageArea_; |
360 | |
322 boost::recursive_mutex restCallbackMutex_; | 361 boost::recursive_mutex restCallbackMutex_; |
323 boost::recursive_mutex storedCallbackMutex_; | 362 boost::recursive_mutex storedCallbackMutex_; |
324 boost::recursive_mutex changeCallbackMutex_; | 363 boost::recursive_mutex changeCallbackMutex_; |
325 boost::mutex findCallbackMutex_; | 364 boost::mutex findCallbackMutex_; |
326 boost::mutex moveCallbackMutex_; | |
327 boost::mutex worklistCallbackMutex_; | 365 boost::mutex worklistCallbackMutex_; |
328 boost::mutex decodeImageCallbackMutex_; | 366 boost::mutex decodeImageCallbackMutex_; |
329 boost::recursive_mutex invokeServiceMutex_; | 367 boost::recursive_mutex invokeServiceMutex_; |
368 | |
330 Properties properties_; | 369 Properties properties_; |
331 int argc_; | 370 int argc_; |
332 char** argv_; | 371 char** argv_; |
333 std::auto_ptr<OrthancPluginDatabase> database_; | 372 std::auto_ptr<OrthancPluginDatabase> database_; |
334 PluginsErrorDictionary dictionary_; | 373 PluginsErrorDictionary dictionary_; |
632 | 671 |
633 | 672 |
634 public: | 673 public: |
635 MoveHandler(OrthancPlugins& that) | 674 MoveHandler(OrthancPlugins& that) |
636 { | 675 { |
637 boost::mutex::scoped_lock lock(that.pimpl_->moveCallbackMutex_); | 676 boost::recursive_mutex::scoped_lock lock(that.pimpl_->invokeServiceMutex_); |
638 params_ = that.pimpl_->moveCallbacks_; | 677 params_ = that.pimpl_->moveCallbacks_; |
639 | 678 |
640 if (params_.callback == NULL || | 679 if (params_.callback == NULL || |
641 params_.getMoveSize == NULL || | 680 params_.getMoveSize == NULL || |
642 params_.applyMove == NULL || | 681 params_.applyMove == NULL || |
730 } | 769 } |
731 | 770 |
732 | 771 |
733 void OrthancPlugins::SetServerContext(ServerContext& context) | 772 void OrthancPlugins::SetServerContext(ServerContext& context) |
734 { | 773 { |
735 pimpl_->context_ = &context; | 774 pimpl_->SetServerContext(&context); |
736 } | 775 } |
737 | 776 |
777 | |
778 void OrthancPlugins::ResetServerContext() | |
779 { | |
780 pimpl_->SetServerContext(NULL); | |
781 } | |
738 | 782 |
739 | 783 |
740 OrthancPlugins::~OrthancPlugins() | 784 OrthancPlugins::~OrthancPlugins() |
741 { | 785 { |
742 for (PImpl::RestCallbacks::iterator it = pimpl_->restCallbacks_.begin(); | 786 for (PImpl::RestCallbacks::iterator it = pimpl_->restCallbacks_.begin(); |
1033 } | 1077 } |
1034 | 1078 |
1035 | 1079 |
1036 void OrthancPlugins::RegisterMoveCallback(const void* parameters) | 1080 void OrthancPlugins::RegisterMoveCallback(const void* parameters) |
1037 { | 1081 { |
1082 // invokeServiceMutex_ is assumed to be locked | |
1083 | |
1038 const _OrthancPluginMoveCallback& p = | 1084 const _OrthancPluginMoveCallback& p = |
1039 *reinterpret_cast<const _OrthancPluginMoveCallback*>(parameters); | 1085 *reinterpret_cast<const _OrthancPluginMoveCallback*>(parameters); |
1040 | |
1041 boost::mutex::scoped_lock lock(pimpl_->moveCallbackMutex_); | |
1042 | 1086 |
1043 if (pimpl_->moveCallbacks_.callback != NULL) | 1087 if (pimpl_->moveCallbacks_.callback != NULL) |
1044 { | 1088 { |
1045 LOG(ERROR) << "Can only register one plugin to handle C-MOVE requests"; | 1089 LOG(ERROR) << "Can only register one plugin to handle C-MOVE requests"; |
1046 throw OrthancException(ErrorCode_Plugin); | 1090 throw OrthancException(ErrorCode_Plugin); |
1230 | 1274 |
1231 translatedOutput->Answer(compressed); | 1275 translatedOutput->Answer(compressed); |
1232 } | 1276 } |
1233 | 1277 |
1234 | 1278 |
1235 void OrthancPlugins::CheckContextAvailable() | |
1236 { | |
1237 if (!pimpl_->context_) | |
1238 { | |
1239 throw OrthancException(ErrorCode_DatabaseNotInitialized); | |
1240 } | |
1241 } | |
1242 | |
1243 | |
1244 void OrthancPlugins::GetDicomForInstance(const void* parameters) | 1279 void OrthancPlugins::GetDicomForInstance(const void* parameters) |
1245 { | 1280 { |
1246 const _OrthancPluginGetDicomForInstance& p = | 1281 const _OrthancPluginGetDicomForInstance& p = |
1247 *reinterpret_cast<const _OrthancPluginGetDicomForInstance*>(parameters); | 1282 *reinterpret_cast<const _OrthancPluginGetDicomForInstance*>(parameters); |
1248 | 1283 |
1249 std::string dicom; | 1284 std::string dicom; |
1250 | 1285 |
1251 CheckContextAvailable(); | 1286 { |
1252 pimpl_->context_->ReadFile(dicom, p.instanceId, FileContentType_Dicom); | 1287 PImpl::ServerContextLock lock(*pimpl_); |
1288 lock.GetContext().ReadFile(dicom, p.instanceId, FileContentType_Dicom); | |
1289 } | |
1253 | 1290 |
1254 CopyToMemoryBuffer(*p.target, dicom); | 1291 CopyToMemoryBuffer(*p.target, dicom); |
1255 } | 1292 } |
1256 | 1293 |
1257 | 1294 |
1262 *reinterpret_cast<const _OrthancPluginRestApiGet*>(parameters); | 1299 *reinterpret_cast<const _OrthancPluginRestApiGet*>(parameters); |
1263 | 1300 |
1264 LOG(INFO) << "Plugin making REST GET call on URI " << p.uri | 1301 LOG(INFO) << "Plugin making REST GET call on URI " << p.uri |
1265 << (afterPlugins ? " (after plugins)" : " (built-in API)"); | 1302 << (afterPlugins ? " (after plugins)" : " (built-in API)"); |
1266 | 1303 |
1267 CheckContextAvailable(); | 1304 IHttpHandler* handler; |
1268 IHttpHandler& handler = pimpl_->context_->GetHttpHandler().RestrictToOrthancRestApi(!afterPlugins); | 1305 |
1306 { | |
1307 PImpl::ServerContextLock lock(*pimpl_); | |
1308 handler = &lock.GetContext().GetHttpHandler().RestrictToOrthancRestApi(!afterPlugins); | |
1309 } | |
1269 | 1310 |
1270 std::string result; | 1311 std::string result; |
1271 if (HttpToolbox::SimpleGet(result, handler, RequestOrigin_Plugins, p.uri)) | 1312 if (HttpToolbox::SimpleGet(result, *handler, RequestOrigin_Plugins, p.uri)) |
1272 { | 1313 { |
1273 CopyToMemoryBuffer(*p.target, result); | 1314 CopyToMemoryBuffer(*p.target, result); |
1274 } | 1315 } |
1275 else | 1316 else |
1276 { | 1317 { |
1294 std::string name(p.headersKeys[i]); | 1335 std::string name(p.headersKeys[i]); |
1295 std::transform(name.begin(), name.end(), name.begin(), ::tolower); | 1336 std::transform(name.begin(), name.end(), name.begin(), ::tolower); |
1296 headers[name] = p.headersValues[i]; | 1337 headers[name] = p.headersValues[i]; |
1297 } | 1338 } |
1298 | 1339 |
1299 CheckContextAvailable(); | 1340 IHttpHandler* handler; |
1300 IHttpHandler& handler = pimpl_->context_->GetHttpHandler().RestrictToOrthancRestApi(!p.afterPlugins); | 1341 |
1301 | 1342 { |
1343 PImpl::ServerContextLock lock(*pimpl_); | |
1344 handler = &lock.GetContext().GetHttpHandler().RestrictToOrthancRestApi(!p.afterPlugins); | |
1345 } | |
1346 | |
1302 std::string result; | 1347 std::string result; |
1303 if (HttpToolbox::SimpleGet(result, handler, RequestOrigin_Plugins, p.uri, headers)) | 1348 if (HttpToolbox::SimpleGet(result, *handler, RequestOrigin_Plugins, p.uri, headers)) |
1304 { | 1349 { |
1305 CopyToMemoryBuffer(*p.target, result); | 1350 CopyToMemoryBuffer(*p.target, result); |
1306 } | 1351 } |
1307 else | 1352 else |
1308 { | 1353 { |
1319 *reinterpret_cast<const _OrthancPluginRestApiPostPut*>(parameters); | 1364 *reinterpret_cast<const _OrthancPluginRestApiPostPut*>(parameters); |
1320 | 1365 |
1321 LOG(INFO) << "Plugin making REST " << EnumerationToString(isPost ? HttpMethod_Post : HttpMethod_Put) | 1366 LOG(INFO) << "Plugin making REST " << EnumerationToString(isPost ? HttpMethod_Post : HttpMethod_Put) |
1322 << " call on URI " << p.uri << (afterPlugins ? " (after plugins)" : " (built-in API)"); | 1367 << " call on URI " << p.uri << (afterPlugins ? " (after plugins)" : " (built-in API)"); |
1323 | 1368 |
1324 CheckContextAvailable(); | 1369 IHttpHandler* handler; |
1325 IHttpHandler& handler = pimpl_->context_->GetHttpHandler().RestrictToOrthancRestApi(!afterPlugins); | 1370 |
1326 | 1371 { |
1372 PImpl::ServerContextLock lock(*pimpl_); | |
1373 handler = &lock.GetContext().GetHttpHandler().RestrictToOrthancRestApi(!afterPlugins); | |
1374 } | |
1375 | |
1327 std::string result; | 1376 std::string result; |
1328 if (isPost ? | 1377 if (isPost ? |
1329 HttpToolbox::SimplePost(result, handler, RequestOrigin_Plugins, p.uri, p.body, p.bodySize) : | 1378 HttpToolbox::SimplePost(result, *handler, RequestOrigin_Plugins, p.uri, p.body, p.bodySize) : |
1330 HttpToolbox::SimplePut (result, handler, RequestOrigin_Plugins, p.uri, p.body, p.bodySize)) | 1379 HttpToolbox::SimplePut (result, *handler, RequestOrigin_Plugins, p.uri, p.body, p.bodySize)) |
1331 { | 1380 { |
1332 CopyToMemoryBuffer(*p.target, result); | 1381 CopyToMemoryBuffer(*p.target, result); |
1333 } | 1382 } |
1334 else | 1383 else |
1335 { | 1384 { |
1343 { | 1392 { |
1344 const char* uri = reinterpret_cast<const char*>(parameters); | 1393 const char* uri = reinterpret_cast<const char*>(parameters); |
1345 LOG(INFO) << "Plugin making REST DELETE call on URI " << uri | 1394 LOG(INFO) << "Plugin making REST DELETE call on URI " << uri |
1346 << (afterPlugins ? " (after plugins)" : " (built-in API)"); | 1395 << (afterPlugins ? " (after plugins)" : " (built-in API)"); |
1347 | 1396 |
1348 CheckContextAvailable(); | 1397 IHttpHandler* handler; |
1349 IHttpHandler& handler = pimpl_->context_->GetHttpHandler().RestrictToOrthancRestApi(!afterPlugins); | 1398 |
1350 | 1399 { |
1351 if (!HttpToolbox::SimpleDelete(handler, RequestOrigin_Plugins, uri)) | 1400 PImpl::ServerContextLock lock(*pimpl_); |
1401 handler = &lock.GetContext().GetHttpHandler().RestrictToOrthancRestApi(!afterPlugins); | |
1402 } | |
1403 | |
1404 if (!HttpToolbox::SimpleDelete(*handler, RequestOrigin_Plugins, uri)) | |
1352 { | 1405 { |
1353 throw OrthancException(ErrorCode_UnknownResource); | 1406 throw OrthancException(ErrorCode_UnknownResource); |
1354 } | 1407 } |
1355 } | 1408 } |
1356 | 1409 |
1399 | 1452 |
1400 default: | 1453 default: |
1401 throw OrthancException(ErrorCode_InternalError); | 1454 throw OrthancException(ErrorCode_InternalError); |
1402 } | 1455 } |
1403 | 1456 |
1404 CheckContextAvailable(); | |
1405 | |
1406 std::list<std::string> result; | 1457 std::list<std::string> result; |
1407 pimpl_->context_->GetIndex().LookupIdentifierExact(result, level, tag, p.argument); | 1458 |
1459 { | |
1460 PImpl::ServerContextLock lock(*pimpl_); | |
1461 lock.GetContext().GetIndex().LookupIdentifierExact(result, level, tag, p.argument); | |
1462 } | |
1408 | 1463 |
1409 if (result.size() == 1) | 1464 if (result.size() == 1) |
1410 { | 1465 { |
1411 *p.result = CopyString(result.front()); | 1466 *p.result = CopyString(result.front()); |
1412 } | 1467 } |
1860 { | 1915 { |
1861 throw OrthancException(ErrorCode_ParameterOutOfRange); | 1916 throw OrthancException(ErrorCode_ParameterOutOfRange); |
1862 } | 1917 } |
1863 | 1918 |
1864 std::string content; | 1919 std::string content; |
1865 pimpl_->context_->ReadFile(content, p.instanceId, FileContentType_Dicom); | 1920 |
1921 { | |
1922 PImpl::ServerContextLock lock(*pimpl_); | |
1923 lock.GetContext().ReadFile(content, p.instanceId, FileContentType_Dicom); | |
1924 } | |
1925 | |
1866 dicom.reset(new ParsedDicomFile(content)); | 1926 dicom.reset(new ParsedDicomFile(content)); |
1867 } | 1927 } |
1868 | 1928 |
1869 Json::Value json; | 1929 Json::Value json; |
1870 dicom->ToJson(json, Plugins::Convert(p.format), | 1930 dicom->ToJson(json, Plugins::Convert(p.format), |
2072 } | 2132 } |
2073 } | 2133 } |
2074 | 2134 |
2075 | 2135 |
2076 | 2136 |
2077 bool OrthancPlugins::InvokeService(SharedLibrary& plugin, | 2137 bool OrthancPlugins::InvokeSafeService(SharedLibrary& plugin, |
2078 _OrthancPluginService service, | 2138 _OrthancPluginService service, |
2079 const void* parameters) | 2139 const void* parameters) |
2080 { | 2140 { |
2081 VLOG(1) << "Calling service " << service << " from plugin " << plugin.GetPath(); | 2141 // Services that can be run without mutual exclusion |
2082 | |
2083 if (service == _OrthancPluginService_DatabaseAnswer) | |
2084 { | |
2085 // This case solves a deadlock at (*) reported by James Webster | |
2086 // on 2015-10-27 that was present in versions of Orthanc <= | |
2087 // 0.9.4 and related to database plugins implementing a custom | |
2088 // index. The problem was that locking the database is already | |
2089 // ensured by the "ServerIndex" class if the invoked service is | |
2090 // "DatabaseAnswer". | |
2091 DatabaseAnswer(parameters); | |
2092 return true; | |
2093 } | |
2094 | |
2095 boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_); // (*) | |
2096 | 2142 |
2097 switch (service) | 2143 switch (service) |
2098 { | 2144 { |
2099 case _OrthancPluginService_GetOrthancPath: | 2145 case _OrthancPluginService_GetOrthancPath: |
2100 { | 2146 { |
2126 return true; | 2172 return true; |
2127 } | 2173 } |
2128 | 2174 |
2129 case _OrthancPluginService_BufferCompression: | 2175 case _OrthancPluginService_BufferCompression: |
2130 BufferCompression(parameters); | 2176 BufferCompression(parameters); |
2131 return true; | |
2132 | |
2133 case _OrthancPluginService_RegisterRestCallback: | |
2134 RegisterRestCallback(parameters, true); | |
2135 return true; | |
2136 | |
2137 case _OrthancPluginService_RegisterRestCallbackNoLock: | |
2138 RegisterRestCallback(parameters, false); | |
2139 return true; | |
2140 | |
2141 case _OrthancPluginService_RegisterOnStoredInstanceCallback: | |
2142 RegisterOnStoredInstanceCallback(parameters); | |
2143 return true; | |
2144 | |
2145 case _OrthancPluginService_RegisterOnChangeCallback: | |
2146 RegisterOnChangeCallback(parameters); | |
2147 return true; | |
2148 | |
2149 case _OrthancPluginService_RegisterWorklistCallback: | |
2150 RegisterWorklistCallback(parameters); | |
2151 return true; | |
2152 | |
2153 case _OrthancPluginService_RegisterFindCallback: | |
2154 RegisterFindCallback(parameters); | |
2155 return true; | |
2156 | |
2157 case _OrthancPluginService_RegisterMoveCallback: | |
2158 RegisterMoveCallback(parameters); | |
2159 return true; | |
2160 | |
2161 case _OrthancPluginService_RegisterDecodeImageCallback: | |
2162 RegisterDecodeImageCallback(parameters); | |
2163 return true; | |
2164 | |
2165 case _OrthancPluginService_RegisterIncomingHttpRequestFilter: | |
2166 RegisterIncomingHttpRequestFilter(parameters); | |
2167 return true; | 2177 return true; |
2168 | 2178 |
2169 case _OrthancPluginService_AnswerBuffer: | 2179 case _OrthancPluginService_AnswerBuffer: |
2170 AnswerBuffer(parameters); | 2180 AnswerBuffer(parameters); |
2171 return true; | 2181 return true; |
2263 case _OrthancPluginService_GetInstanceMetadata: | 2273 case _OrthancPluginService_GetInstanceMetadata: |
2264 case _OrthancPluginService_GetInstanceOrigin: | 2274 case _OrthancPluginService_GetInstanceOrigin: |
2265 AccessDicomInstance(service, parameters); | 2275 AccessDicomInstance(service, parameters); |
2266 return true; | 2276 return true; |
2267 | 2277 |
2268 case _OrthancPluginService_RegisterStorageArea: | |
2269 { | |
2270 LOG(INFO) << "Plugin has registered a custom storage area"; | |
2271 const _OrthancPluginRegisterStorageArea& p = | |
2272 *reinterpret_cast<const _OrthancPluginRegisterStorageArea*>(parameters); | |
2273 | |
2274 if (pimpl_->storageArea_.get() == NULL) | |
2275 { | |
2276 pimpl_->storageArea_.reset(new StorageAreaFactory(plugin, p, GetErrorDictionary())); | |
2277 } | |
2278 else | |
2279 { | |
2280 throw OrthancException(ErrorCode_StorageAreaAlreadyRegistered); | |
2281 } | |
2282 | |
2283 return true; | |
2284 } | |
2285 | |
2286 case _OrthancPluginService_SetPluginProperty: | |
2287 { | |
2288 const _OrthancPluginSetPluginProperty& p = | |
2289 *reinterpret_cast<const _OrthancPluginSetPluginProperty*>(parameters); | |
2290 pimpl_->properties_[std::make_pair(p.plugin, p.property)] = p.value; | |
2291 return true; | |
2292 } | |
2293 | |
2294 case _OrthancPluginService_SetGlobalProperty: | 2278 case _OrthancPluginService_SetGlobalProperty: |
2295 { | 2279 { |
2296 const _OrthancPluginGlobalProperty& p = | 2280 const _OrthancPluginGlobalProperty& p = |
2297 *reinterpret_cast<const _OrthancPluginGlobalProperty*>(parameters); | 2281 *reinterpret_cast<const _OrthancPluginGlobalProperty*>(parameters); |
2298 if (p.property < 1024) | 2282 if (p.property < 1024) |
2299 { | 2283 { |
2300 return false; | 2284 return false; |
2301 } | 2285 } |
2302 else | 2286 else |
2303 { | 2287 { |
2304 CheckContextAvailable(); | 2288 PImpl::ServerContextLock lock(*pimpl_); |
2305 pimpl_->context_->GetIndex().SetGlobalProperty(static_cast<GlobalProperty>(p.property), p.value); | 2289 lock.GetContext().GetIndex().SetGlobalProperty(static_cast<GlobalProperty>(p.property), p.value); |
2306 return true; | 2290 return true; |
2307 } | 2291 } |
2308 } | 2292 } |
2309 | 2293 |
2310 case _OrthancPluginService_GetGlobalProperty: | 2294 case _OrthancPluginService_GetGlobalProperty: |
2311 { | 2295 { |
2312 CheckContextAvailable(); | |
2313 | |
2314 const _OrthancPluginGlobalProperty& p = | 2296 const _OrthancPluginGlobalProperty& p = |
2315 *reinterpret_cast<const _OrthancPluginGlobalProperty*>(parameters); | 2297 *reinterpret_cast<const _OrthancPluginGlobalProperty*>(parameters); |
2316 std::string result = pimpl_->context_->GetIndex().GetGlobalProperty(static_cast<GlobalProperty>(p.property), p.value); | 2298 |
2299 std::string result; | |
2300 | |
2301 { | |
2302 PImpl::ServerContextLock lock(*pimpl_); | |
2303 result = lock.GetContext().GetIndex().GetGlobalProperty(static_cast<GlobalProperty>(p.property), p.value); | |
2304 } | |
2305 | |
2317 *(p.result) = CopyString(result); | 2306 *(p.result) = CopyString(result); |
2318 return true; | 2307 return true; |
2319 } | 2308 } |
2320 | |
2321 case _OrthancPluginService_GetCommandLineArgumentsCount: | |
2322 { | |
2323 const _OrthancPluginReturnSingleValue& p = | |
2324 *reinterpret_cast<const _OrthancPluginReturnSingleValue*>(parameters); | |
2325 *(p.resultUint32) = pimpl_->argc_ - 1; | |
2326 return true; | |
2327 } | |
2328 | |
2329 case _OrthancPluginService_GetCommandLineArgument: | |
2330 { | |
2331 const _OrthancPluginGlobalProperty& p = | |
2332 *reinterpret_cast<const _OrthancPluginGlobalProperty*>(parameters); | |
2333 | |
2334 if (p.property + 1 > pimpl_->argc_) | |
2335 { | |
2336 return false; | |
2337 } | |
2338 else | |
2339 { | |
2340 std::string arg = std::string(pimpl_->argv_[p.property + 1]); | |
2341 *(p.result) = CopyString(arg); | |
2342 return true; | |
2343 } | |
2344 } | |
2345 | |
2346 case _OrthancPluginService_RegisterDatabaseBackend: | |
2347 { | |
2348 LOG(INFO) << "Plugin has registered a custom database back-end"; | |
2349 | |
2350 const _OrthancPluginRegisterDatabaseBackend& p = | |
2351 *reinterpret_cast<const _OrthancPluginRegisterDatabaseBackend*>(parameters); | |
2352 | |
2353 if (pimpl_->database_.get() == NULL) | |
2354 { | |
2355 pimpl_->database_.reset(new OrthancPluginDatabase(plugin, GetErrorDictionary(), | |
2356 *p.backend, NULL, 0, p.payload)); | |
2357 } | |
2358 else | |
2359 { | |
2360 throw OrthancException(ErrorCode_DatabaseBackendAlreadyRegistered); | |
2361 } | |
2362 | |
2363 *(p.result) = reinterpret_cast<OrthancPluginDatabaseContext*>(pimpl_->database_.get()); | |
2364 | |
2365 return true; | |
2366 } | |
2367 | |
2368 case _OrthancPluginService_RegisterDatabaseBackendV2: | |
2369 { | |
2370 LOG(INFO) << "Plugin has registered a custom database back-end"; | |
2371 | |
2372 const _OrthancPluginRegisterDatabaseBackendV2& p = | |
2373 *reinterpret_cast<const _OrthancPluginRegisterDatabaseBackendV2*>(parameters); | |
2374 | |
2375 if (pimpl_->database_.get() == NULL) | |
2376 { | |
2377 pimpl_->database_.reset(new OrthancPluginDatabase(plugin, GetErrorDictionary(), | |
2378 *p.backend, p.extensions, | |
2379 p.extensionsSize, p.payload)); | |
2380 } | |
2381 else | |
2382 { | |
2383 throw OrthancException(ErrorCode_DatabaseBackendAlreadyRegistered); | |
2384 } | |
2385 | |
2386 *(p.result) = reinterpret_cast<OrthancPluginDatabaseContext*>(pimpl_->database_.get()); | |
2387 | |
2388 return true; | |
2389 } | |
2390 | |
2391 case _OrthancPluginService_DatabaseAnswer: | |
2392 throw OrthancException(ErrorCode_InternalError); // Implemented before locking (*) | |
2393 | 2309 |
2394 case _OrthancPluginService_GetExpectedDatabaseVersion: | 2310 case _OrthancPluginService_GetExpectedDatabaseVersion: |
2395 { | 2311 { |
2396 const _OrthancPluginReturnSingleValue& p = | 2312 const _OrthancPluginReturnSingleValue& p = |
2397 *reinterpret_cast<const _OrthancPluginReturnSingleValue*>(parameters); | 2313 *reinterpret_cast<const _OrthancPluginReturnSingleValue*>(parameters); |
2556 IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea); | 2472 IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea); |
2557 storage.Remove(p.uuid, Plugins::Convert(p.type)); | 2473 storage.Remove(p.uuid, Plugins::Convert(p.type)); |
2558 return true; | 2474 return true; |
2559 } | 2475 } |
2560 | 2476 |
2477 case _OrthancPluginService_DicomBufferToJson: | |
2478 case _OrthancPluginService_DicomInstanceToJson: | |
2479 ApplyDicomToJson(service, parameters); | |
2480 return true; | |
2481 | |
2482 case _OrthancPluginService_CreateDicom: | |
2483 ApplyCreateDicom(service, parameters); | |
2484 return true; | |
2485 | |
2486 case _OrthancPluginService_WorklistAddAnswer: | |
2487 { | |
2488 const _OrthancPluginWorklistAnswersOperation& p = | |
2489 *reinterpret_cast<const _OrthancPluginWorklistAnswersOperation*>(parameters); | |
2490 reinterpret_cast<const WorklistHandler*>(p.query)->AddAnswer(p.answers, p.dicom, p.size); | |
2491 return true; | |
2492 } | |
2493 | |
2494 case _OrthancPluginService_WorklistMarkIncomplete: | |
2495 { | |
2496 const _OrthancPluginWorklistAnswersOperation& p = | |
2497 *reinterpret_cast<const _OrthancPluginWorklistAnswersOperation*>(parameters); | |
2498 reinterpret_cast<DicomFindAnswers*>(p.answers)->SetComplete(false); | |
2499 return true; | |
2500 } | |
2501 | |
2502 case _OrthancPluginService_WorklistIsMatch: | |
2503 { | |
2504 const _OrthancPluginWorklistQueryOperation& p = | |
2505 *reinterpret_cast<const _OrthancPluginWorklistQueryOperation*>(parameters); | |
2506 *p.isMatch = reinterpret_cast<const WorklistHandler*>(p.query)->IsMatch(p.dicom, p.size); | |
2507 return true; | |
2508 } | |
2509 | |
2510 case _OrthancPluginService_WorklistGetDicomQuery: | |
2511 { | |
2512 const _OrthancPluginWorklistQueryOperation& p = | |
2513 *reinterpret_cast<const _OrthancPluginWorklistQueryOperation*>(parameters); | |
2514 reinterpret_cast<const WorklistHandler*>(p.query)->GetDicomQuery(*p.target); | |
2515 return true; | |
2516 } | |
2517 | |
2518 case _OrthancPluginService_FindAddAnswer: | |
2519 { | |
2520 const _OrthancPluginFindOperation& p = | |
2521 *reinterpret_cast<const _OrthancPluginFindOperation*>(parameters); | |
2522 reinterpret_cast<DicomFindAnswers*>(p.answers)->Add(p.dicom, p.size); | |
2523 return true; | |
2524 } | |
2525 | |
2526 case _OrthancPluginService_FindMarkIncomplete: | |
2527 { | |
2528 const _OrthancPluginFindOperation& p = | |
2529 *reinterpret_cast<const _OrthancPluginFindOperation*>(parameters); | |
2530 reinterpret_cast<DicomFindAnswers*>(p.answers)->SetComplete(false); | |
2531 return true; | |
2532 } | |
2533 | |
2534 case _OrthancPluginService_GetFindQuerySize: | |
2535 case _OrthancPluginService_GetFindQueryTag: | |
2536 case _OrthancPluginService_GetFindQueryTagName: | |
2537 case _OrthancPluginService_GetFindQueryValue: | |
2538 { | |
2539 const _OrthancPluginFindOperation& p = | |
2540 *reinterpret_cast<const _OrthancPluginFindOperation*>(parameters); | |
2541 reinterpret_cast<const FindHandler*>(p.query)->Invoke(service, p); | |
2542 return true; | |
2543 } | |
2544 | |
2545 case _OrthancPluginService_CreateImage: | |
2546 case _OrthancPluginService_CreateImageAccessor: | |
2547 case _OrthancPluginService_DecodeDicomImage: | |
2548 ApplyCreateImage(service, parameters); | |
2549 return true; | |
2550 | |
2551 case _OrthancPluginService_ComputeMd5: | |
2552 case _OrthancPluginService_ComputeSha1: | |
2553 ComputeHash(service, parameters); | |
2554 return true; | |
2555 | |
2556 case _OrthancPluginService_LookupDictionary: | |
2557 ApplyLookupDictionary(parameters); | |
2558 return true; | |
2559 | |
2560 case _OrthancPluginService_GenerateUuid: | |
2561 { | |
2562 *reinterpret_cast<const _OrthancPluginRetrieveDynamicString*>(parameters)->result = | |
2563 CopyString(Toolbox::GenerateUuid()); | |
2564 return true; | |
2565 } | |
2566 | |
2567 default: | |
2568 return false; | |
2569 } | |
2570 } | |
2571 | |
2572 | |
2573 | |
2574 bool OrthancPlugins::InvokeProtectedService(SharedLibrary& plugin, | |
2575 _OrthancPluginService service, | |
2576 const void* parameters) | |
2577 { | |
2578 // Services that must be run in mutual exclusion. Guideline: | |
2579 // Whenever "pimpl_" is directly accessed by the service, it | |
2580 // should be listed here. | |
2581 | |
2582 switch (service) | |
2583 { | |
2584 case _OrthancPluginService_RegisterRestCallback: | |
2585 RegisterRestCallback(parameters, true); | |
2586 return true; | |
2587 | |
2588 case _OrthancPluginService_RegisterRestCallbackNoLock: | |
2589 RegisterRestCallback(parameters, false); | |
2590 return true; | |
2591 | |
2592 case _OrthancPluginService_RegisterOnStoredInstanceCallback: | |
2593 RegisterOnStoredInstanceCallback(parameters); | |
2594 return true; | |
2595 | |
2596 case _OrthancPluginService_RegisterOnChangeCallback: | |
2597 RegisterOnChangeCallback(parameters); | |
2598 return true; | |
2599 | |
2600 case _OrthancPluginService_RegisterWorklistCallback: | |
2601 RegisterWorklistCallback(parameters); | |
2602 return true; | |
2603 | |
2604 case _OrthancPluginService_RegisterFindCallback: | |
2605 RegisterFindCallback(parameters); | |
2606 return true; | |
2607 | |
2608 case _OrthancPluginService_RegisterMoveCallback: | |
2609 RegisterMoveCallback(parameters); | |
2610 return true; | |
2611 | |
2612 case _OrthancPluginService_RegisterDecodeImageCallback: | |
2613 RegisterDecodeImageCallback(parameters); | |
2614 return true; | |
2615 | |
2616 case _OrthancPluginService_RegisterIncomingHttpRequestFilter: | |
2617 RegisterIncomingHttpRequestFilter(parameters); | |
2618 return true; | |
2619 | |
2620 case _OrthancPluginService_RegisterStorageArea: | |
2621 { | |
2622 LOG(INFO) << "Plugin has registered a custom storage area"; | |
2623 const _OrthancPluginRegisterStorageArea& p = | |
2624 *reinterpret_cast<const _OrthancPluginRegisterStorageArea*>(parameters); | |
2625 | |
2626 if (pimpl_->storageArea_.get() == NULL) | |
2627 { | |
2628 pimpl_->storageArea_.reset(new StorageAreaFactory(plugin, p, GetErrorDictionary())); | |
2629 } | |
2630 else | |
2631 { | |
2632 throw OrthancException(ErrorCode_StorageAreaAlreadyRegistered); | |
2633 } | |
2634 | |
2635 return true; | |
2636 } | |
2637 | |
2638 case _OrthancPluginService_SetPluginProperty: | |
2639 { | |
2640 const _OrthancPluginSetPluginProperty& p = | |
2641 *reinterpret_cast<const _OrthancPluginSetPluginProperty*>(parameters); | |
2642 pimpl_->properties_[std::make_pair(p.plugin, p.property)] = p.value; | |
2643 return true; | |
2644 } | |
2645 | |
2646 case _OrthancPluginService_GetCommandLineArgumentsCount: | |
2647 { | |
2648 const _OrthancPluginReturnSingleValue& p = | |
2649 *reinterpret_cast<const _OrthancPluginReturnSingleValue*>(parameters); | |
2650 *(p.resultUint32) = pimpl_->argc_ - 1; | |
2651 return true; | |
2652 } | |
2653 | |
2654 case _OrthancPluginService_GetCommandLineArgument: | |
2655 { | |
2656 const _OrthancPluginGlobalProperty& p = | |
2657 *reinterpret_cast<const _OrthancPluginGlobalProperty*>(parameters); | |
2658 | |
2659 if (p.property + 1 > pimpl_->argc_) | |
2660 { | |
2661 return false; | |
2662 } | |
2663 else | |
2664 { | |
2665 std::string arg = std::string(pimpl_->argv_[p.property + 1]); | |
2666 *(p.result) = CopyString(arg); | |
2667 return true; | |
2668 } | |
2669 } | |
2670 | |
2671 case _OrthancPluginService_RegisterDatabaseBackend: | |
2672 { | |
2673 LOG(INFO) << "Plugin has registered a custom database back-end"; | |
2674 | |
2675 const _OrthancPluginRegisterDatabaseBackend& p = | |
2676 *reinterpret_cast<const _OrthancPluginRegisterDatabaseBackend*>(parameters); | |
2677 | |
2678 if (pimpl_->database_.get() == NULL) | |
2679 { | |
2680 pimpl_->database_.reset(new OrthancPluginDatabase(plugin, GetErrorDictionary(), | |
2681 *p.backend, NULL, 0, p.payload)); | |
2682 } | |
2683 else | |
2684 { | |
2685 throw OrthancException(ErrorCode_DatabaseBackendAlreadyRegistered); | |
2686 } | |
2687 | |
2688 *(p.result) = reinterpret_cast<OrthancPluginDatabaseContext*>(pimpl_->database_.get()); | |
2689 | |
2690 return true; | |
2691 } | |
2692 | |
2693 case _OrthancPluginService_RegisterDatabaseBackendV2: | |
2694 { | |
2695 LOG(INFO) << "Plugin has registered a custom database back-end"; | |
2696 | |
2697 const _OrthancPluginRegisterDatabaseBackendV2& p = | |
2698 *reinterpret_cast<const _OrthancPluginRegisterDatabaseBackendV2*>(parameters); | |
2699 | |
2700 if (pimpl_->database_.get() == NULL) | |
2701 { | |
2702 pimpl_->database_.reset(new OrthancPluginDatabase(plugin, GetErrorDictionary(), | |
2703 *p.backend, p.extensions, | |
2704 p.extensionsSize, p.payload)); | |
2705 } | |
2706 else | |
2707 { | |
2708 throw OrthancException(ErrorCode_DatabaseBackendAlreadyRegistered); | |
2709 } | |
2710 | |
2711 *(p.result) = reinterpret_cast<OrthancPluginDatabaseContext*>(pimpl_->database_.get()); | |
2712 | |
2713 return true; | |
2714 } | |
2715 | |
2716 case _OrthancPluginService_DatabaseAnswer: | |
2717 throw OrthancException(ErrorCode_InternalError); // Implemented before locking (*) | |
2718 | |
2561 case _OrthancPluginService_RegisterErrorCode: | 2719 case _OrthancPluginService_RegisterErrorCode: |
2562 { | 2720 { |
2563 const _OrthancPluginRegisterErrorCode& p = | 2721 const _OrthancPluginRegisterErrorCode& p = |
2564 *reinterpret_cast<const _OrthancPluginRegisterErrorCode*>(parameters); | 2722 *reinterpret_cast<const _OrthancPluginRegisterErrorCode*>(parameters); |
2565 *(p.target) = pimpl_->dictionary_.Register(plugin, p.code, p.httpStatus, p.message); | 2723 *(p.target) = pimpl_->dictionary_.Register(plugin, p.code, p.httpStatus, p.message); |
2591 Toolbox::ReconstructMainDicomTags(*pimpl_->database_, storage, Plugins::Convert(p.level)); | 2749 Toolbox::ReconstructMainDicomTags(*pimpl_->database_, storage, Plugins::Convert(p.level)); |
2592 | 2750 |
2593 return true; | 2751 return true; |
2594 } | 2752 } |
2595 | 2753 |
2596 case _OrthancPluginService_DicomBufferToJson: | |
2597 case _OrthancPluginService_DicomInstanceToJson: | |
2598 ApplyDicomToJson(service, parameters); | |
2599 return true; | |
2600 | |
2601 case _OrthancPluginService_CreateDicom: | |
2602 ApplyCreateDicom(service, parameters); | |
2603 return true; | |
2604 | |
2605 case _OrthancPluginService_WorklistAddAnswer: | |
2606 { | |
2607 const _OrthancPluginWorklistAnswersOperation& p = | |
2608 *reinterpret_cast<const _OrthancPluginWorklistAnswersOperation*>(parameters); | |
2609 reinterpret_cast<const WorklistHandler*>(p.query)->AddAnswer(p.answers, p.dicom, p.size); | |
2610 return true; | |
2611 } | |
2612 | |
2613 case _OrthancPluginService_WorklistMarkIncomplete: | |
2614 { | |
2615 const _OrthancPluginWorklistAnswersOperation& p = | |
2616 *reinterpret_cast<const _OrthancPluginWorklistAnswersOperation*>(parameters); | |
2617 reinterpret_cast<DicomFindAnswers*>(p.answers)->SetComplete(false); | |
2618 return true; | |
2619 } | |
2620 | |
2621 case _OrthancPluginService_WorklistIsMatch: | |
2622 { | |
2623 const _OrthancPluginWorklistQueryOperation& p = | |
2624 *reinterpret_cast<const _OrthancPluginWorklistQueryOperation*>(parameters); | |
2625 *p.isMatch = reinterpret_cast<const WorklistHandler*>(p.query)->IsMatch(p.dicom, p.size); | |
2626 return true; | |
2627 } | |
2628 | |
2629 case _OrthancPluginService_WorklistGetDicomQuery: | |
2630 { | |
2631 const _OrthancPluginWorklistQueryOperation& p = | |
2632 *reinterpret_cast<const _OrthancPluginWorklistQueryOperation*>(parameters); | |
2633 reinterpret_cast<const WorklistHandler*>(p.query)->GetDicomQuery(*p.target); | |
2634 return true; | |
2635 } | |
2636 | |
2637 case _OrthancPluginService_FindAddAnswer: | |
2638 { | |
2639 const _OrthancPluginFindOperation& p = | |
2640 *reinterpret_cast<const _OrthancPluginFindOperation*>(parameters); | |
2641 reinterpret_cast<DicomFindAnswers*>(p.answers)->Add(p.dicom, p.size); | |
2642 return true; | |
2643 } | |
2644 | |
2645 case _OrthancPluginService_FindMarkIncomplete: | |
2646 { | |
2647 const _OrthancPluginFindOperation& p = | |
2648 *reinterpret_cast<const _OrthancPluginFindOperation*>(parameters); | |
2649 reinterpret_cast<DicomFindAnswers*>(p.answers)->SetComplete(false); | |
2650 return true; | |
2651 } | |
2652 | |
2653 case _OrthancPluginService_GetFindQuerySize: | |
2654 case _OrthancPluginService_GetFindQueryTag: | |
2655 case _OrthancPluginService_GetFindQueryTagName: | |
2656 case _OrthancPluginService_GetFindQueryValue: | |
2657 { | |
2658 const _OrthancPluginFindOperation& p = | |
2659 *reinterpret_cast<const _OrthancPluginFindOperation*>(parameters); | |
2660 reinterpret_cast<const FindHandler*>(p.query)->Invoke(service, p); | |
2661 return true; | |
2662 } | |
2663 | |
2664 case _OrthancPluginService_CreateImage: | |
2665 case _OrthancPluginService_CreateImageAccessor: | |
2666 case _OrthancPluginService_DecodeDicomImage: | |
2667 ApplyCreateImage(service, parameters); | |
2668 return true; | |
2669 | |
2670 case _OrthancPluginService_ComputeMd5: | |
2671 case _OrthancPluginService_ComputeSha1: | |
2672 ComputeHash(service, parameters); | |
2673 return true; | |
2674 | |
2675 case _OrthancPluginService_LookupDictionary: | |
2676 ApplyLookupDictionary(parameters); | |
2677 return true; | |
2678 | |
2679 case _OrthancPluginService_GenerateUuid: | |
2680 { | |
2681 *reinterpret_cast<const _OrthancPluginRetrieveDynamicString*>(parameters)->result = | |
2682 CopyString(Toolbox::GenerateUuid()); | |
2683 return true; | |
2684 } | |
2685 | |
2686 default: | 2754 default: |
2687 { | 2755 { |
2688 // This service is unknown to the Orthanc plugin engine | 2756 // This service is unknown to the Orthanc plugin engine |
2689 return false; | 2757 return false; |
2690 } | 2758 } |
2691 } | 2759 } |
2692 } | 2760 } |
2693 | 2761 |
2694 | 2762 |
2763 | |
2764 bool OrthancPlugins::InvokeService(SharedLibrary& plugin, | |
2765 _OrthancPluginService service, | |
2766 const void* parameters) | |
2767 { | |
2768 VLOG(1) << "Calling service " << service << " from plugin " << plugin.GetPath(); | |
2769 | |
2770 if (service == _OrthancPluginService_DatabaseAnswer) | |
2771 { | |
2772 // This case solves a deadlock at (*) reported by James Webster | |
2773 // on 2015-10-27 that was present in versions of Orthanc <= | |
2774 // 0.9.4 and related to database plugins implementing a custom | |
2775 // index. The problem was that locking the database is already | |
2776 // ensured by the "ServerIndex" class if the invoked service is | |
2777 // "DatabaseAnswer". | |
2778 DatabaseAnswer(parameters); | |
2779 return true; | |
2780 } | |
2781 | |
2782 if (InvokeSafeService(plugin, service, parameters)) | |
2783 { | |
2784 // The invoked service does not require locking | |
2785 return true; | |
2786 } | |
2787 else | |
2788 { | |
2789 // The invoked service requires locking | |
2790 boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_); // (*) | |
2791 return InvokeProtectedService(plugin, service, parameters); | |
2792 } | |
2793 } | |
2794 | |
2795 | |
2695 bool OrthancPlugins::HasStorageArea() const | 2796 bool OrthancPlugins::HasStorageArea() const |
2696 { | 2797 { |
2798 boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_); | |
2697 return pimpl_->storageArea_.get() != NULL; | 2799 return pimpl_->storageArea_.get() != NULL; |
2698 } | 2800 } |
2699 | 2801 |
2700 bool OrthancPlugins::HasDatabaseBackend() const | 2802 bool OrthancPlugins::HasDatabaseBackend() const |
2701 { | 2803 { |
2804 boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_); | |
2702 return pimpl_->database_.get() != NULL; | 2805 return pimpl_->database_.get() != NULL; |
2703 } | 2806 } |
2704 | 2807 |
2705 | 2808 |
2706 IStorageArea* OrthancPlugins::CreateStorageArea() | 2809 IStorageArea* OrthancPlugins::CreateStorageArea() |
2855 } | 2958 } |
2856 | 2959 |
2857 | 2960 |
2858 bool OrthancPlugins::HasMoveHandler() | 2961 bool OrthancPlugins::HasMoveHandler() |
2859 { | 2962 { |
2860 boost::mutex::scoped_lock lock(pimpl_->moveCallbackMutex_); | 2963 boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_); |
2861 return pimpl_->moveCallbacks_.callback != NULL; | 2964 return pimpl_->moveCallbacks_.callback != NULL; |
2862 } | 2965 } |
2863 | 2966 |
2864 | 2967 |
2865 bool OrthancPlugins::HasCustomImageDecoder() | 2968 bool OrthancPlugins::HasCustomImageDecoder() |