You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

626 lines
22 KiB

2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
  1. /*
  2. * This program source code file is part of KiCad, a free EDA CAD application.
  3. *
  4. * Copyright (C) 2024 Jon Evans <jon@craftyjon.com>
  5. * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
  6. *
  7. * This program is free software: you can redistribute it and/or modify it
  8. * under the terms of the GNU General Public License as published by the
  9. * Free Software Foundation, either version 3 of the License, or (at your
  10. * option) any later version.
  11. *
  12. * This program is distributed in the hope that it will be useful, but
  13. * WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  15. * General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU General Public License along
  18. * with this program. If not, see <http://www.gnu.org/licenses/>.
  19. */
  20. #include <fstream>
  21. #include <env_vars.h>
  22. #include <fmt/format.h>
  23. #include <wx/dir.h>
  24. #include <wx/log.h>
  25. #include <wx/utils.h>
  26. #include <api/api_plugin_manager.h>
  27. #include <api/api_server.h>
  28. #include <api/api_utils.h>
  29. #include <gestfich.h>
  30. #include <paths.h>
  31. #include <pgm_base.h>
  32. #include <python_manager.h>
  33. #include <settings/settings_manager.h>
  34. #include <settings/common_settings.h>
  35. wxDEFINE_EVENT( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxCommandEvent );
  36. wxDEFINE_EVENT( EDA_EVT_PLUGIN_AVAILABILITY_CHANGED, wxCommandEvent );
  37. API_PLUGIN_MANAGER::API_PLUGIN_MANAGER( wxEvtHandler* aEvtHandler ) :
  38. wxEvtHandler(),
  39. m_parent( aEvtHandler )
  40. {
  41. // Read and store pcm schema
  42. wxFileName schemaFile( PATHS::GetStockDataPath( true ), wxS( "api.v1.schema.json" ) );
  43. schemaFile.Normalize( FN_NORMALIZE_FLAGS | wxPATH_NORM_ENV_VARS );
  44. schemaFile.AppendDir( wxS( "schemas" ) );
  45. m_schema_validator = std::make_unique<JSON_SCHEMA_VALIDATOR>( schemaFile );
  46. Bind( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, &API_PLUGIN_MANAGER::processNextJob, this );
  47. }
  48. class PLUGIN_TRAVERSER : public wxDirTraverser
  49. {
  50. private:
  51. std::function<void( const wxFileName& )> m_action;
  52. public:
  53. explicit PLUGIN_TRAVERSER( std::function<void( const wxFileName& )> aAction )
  54. : m_action( std::move( aAction ) )
  55. {
  56. }
  57. wxDirTraverseResult OnFile( const wxString& aFilePath ) override
  58. {
  59. wxFileName file( aFilePath );
  60. if( file.GetFullName() == wxS( "plugin.json" ) )
  61. m_action( file );
  62. return wxDIR_CONTINUE;
  63. }
  64. wxDirTraverseResult OnDir( const wxString& dirPath ) override
  65. {
  66. return wxDIR_CONTINUE;
  67. }
  68. };
  69. void API_PLUGIN_MANAGER::ReloadPlugins()
  70. {
  71. m_plugins.clear();
  72. m_pluginsCache.clear();
  73. m_actionsCache.clear();
  74. m_environmentCache.clear();
  75. m_buttonBindings.clear();
  76. m_menuBindings.clear();
  77. m_readyPlugins.clear();
  78. PLUGIN_TRAVERSER loader(
  79. [&]( const wxFileName& aFile )
  80. {
  81. wxLogTrace( traceApi, wxString::Format( "Manager: loading plugin from %s",
  82. aFile.GetFullPath() ) );
  83. auto plugin = std::make_unique<API_PLUGIN>( aFile, *m_schema_validator );
  84. if( plugin->IsOk() )
  85. {
  86. if( m_pluginsCache.count( plugin->Identifier() ) )
  87. {
  88. wxLogTrace( traceApi,
  89. wxString::Format( "Manager: identifier %s already present!",
  90. plugin->Identifier() ) );
  91. return;
  92. }
  93. else
  94. {
  95. m_pluginsCache[plugin->Identifier()] = plugin.get();
  96. }
  97. for( const PLUGIN_ACTION& action : plugin->Actions() )
  98. m_actionsCache[action.identifier] = &action;
  99. m_plugins.insert( std::move( plugin ) );
  100. }
  101. else
  102. {
  103. wxLogTrace( traceApi, "Manager: loading failed" );
  104. }
  105. } );
  106. wxDir systemPluginsDir( PATHS::GetStockPluginsPath() );
  107. if( systemPluginsDir.IsOpened() )
  108. {
  109. wxLogTrace( traceApi, wxString::Format( "Manager: scanning system path (%s) for plugins...",
  110. systemPluginsDir.GetName() ) );
  111. systemPluginsDir.Traverse( loader );
  112. }
  113. wxString thirdPartyPath;
  114. const ENV_VAR_MAP& env = Pgm().GetLocalEnvVariables();
  115. if( std::optional<wxString> v = ENV_VAR::GetVersionedEnvVarValue( env, wxT( "3RD_PARTY" ) ) )
  116. thirdPartyPath = *v;
  117. else
  118. thirdPartyPath = PATHS::GetDefault3rdPartyPath();
  119. wxDir thirdParty( thirdPartyPath );
  120. if( thirdParty.IsOpened() )
  121. {
  122. wxLogTrace( traceApi, wxString::Format( "Manager: scanning PCM path (%s) for plugins...",
  123. thirdParty.GetName() ) );
  124. thirdParty.Traverse( loader );
  125. }
  126. wxDir userPluginsDir( PATHS::GetUserPluginsPath() );
  127. if( userPluginsDir.IsOpened() )
  128. {
  129. wxLogTrace( traceApi, wxString::Format( "Manager: scanning user path (%s) for plugins...",
  130. userPluginsDir.GetName() ) );
  131. userPluginsDir.Traverse( loader );
  132. }
  133. processPluginDependencies();
  134. wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_AVAILABILITY_CHANGED, wxID_ANY );
  135. m_parent->QueueEvent( evt );
  136. }
  137. void API_PLUGIN_MANAGER::RecreatePluginEnvironment( const wxString& aIdentifier )
  138. {
  139. if( !m_pluginsCache.contains( aIdentifier ) )
  140. return;
  141. const API_PLUGIN* plugin = m_pluginsCache.at( aIdentifier );
  142. wxCHECK( plugin, /* void */ );
  143. std::optional<wxString> env = PYTHON_MANAGER::GetPythonEnvironment( plugin->Identifier() );
  144. wxCHECK( env.has_value(), /* void */ );
  145. wxFileName envConfigPath( *env, wxS( "pyvenv.cfg" ) );
  146. envConfigPath.MakeAbsolute();
  147. if( envConfigPath.DirExists() && envConfigPath.Rmdir( wxPATH_RMDIR_RECURSIVE ) )
  148. {
  149. wxLogTrace( traceApi,
  150. wxString::Format( "Manager: Removed existing Python environment at %s for %s",
  151. envConfigPath.GetPath(), plugin->Identifier() ) );
  152. JOB job;
  153. job.type = JOB_TYPE::CREATE_ENV;
  154. job.identifier = plugin->Identifier();
  155. job.plugin_path = plugin->BasePath();
  156. job.env_path = envConfigPath.GetPath();
  157. m_jobs.emplace_back( job );
  158. wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
  159. QueueEvent( evt );
  160. }
  161. }
  162. std::optional<const PLUGIN_ACTION*> API_PLUGIN_MANAGER::GetAction( const wxString& aIdentifier )
  163. {
  164. if( !m_actionsCache.contains( aIdentifier ) )
  165. return std::nullopt;
  166. return m_actionsCache.at( aIdentifier );
  167. }
  168. void API_PLUGIN_MANAGER::InvokeAction( const wxString& aIdentifier )
  169. {
  170. if( !m_actionsCache.contains( aIdentifier ) )
  171. return;
  172. const PLUGIN_ACTION* action = m_actionsCache.at( aIdentifier );
  173. const API_PLUGIN& plugin = action->plugin;
  174. if( !m_readyPlugins.count( plugin.Identifier() ) )
  175. {
  176. wxLogTrace( traceApi, wxString::Format( "Manager: Plugin %s is not ready",
  177. plugin.Identifier() ) );
  178. return;
  179. }
  180. wxFileName pluginFile( plugin.BasePath(), action->entrypoint );
  181. pluginFile.Normalize( wxPATH_NORM_ABSOLUTE | wxPATH_NORM_SHORTCUT | wxPATH_NORM_DOTS
  182. | wxPATH_NORM_TILDE, plugin.BasePath() );
  183. wxString pluginPath = pluginFile.GetFullPath();
  184. std::vector<const wchar_t*> args;
  185. std::optional<wxString> py;
  186. switch( plugin.Runtime().type )
  187. {
  188. case PLUGIN_RUNTIME_TYPE::PYTHON:
  189. {
  190. py = PYTHON_MANAGER::GetVirtualPython( plugin.Identifier() );
  191. if( !py )
  192. {
  193. wxLogTrace( traceApi, wxString::Format( "Manager: Python interpreter for %s not found",
  194. plugin.Identifier() ) );
  195. return;
  196. }
  197. if( !pluginFile.IsFileReadable() )
  198. {
  199. wxLogTrace( traceApi, wxString::Format( "Manager: Python entrypoint %s is not readable",
  200. pluginFile.GetFullPath() ) );
  201. return;
  202. }
  203. std::optional<wxString> pythonHome =
  204. PYTHON_MANAGER::GetPythonEnvironment( plugin.Identifier() );
  205. PYTHON_MANAGER manager( *py );
  206. wxExecuteEnv env;
  207. wxGetEnvMap( &env.env );
  208. env.env[wxS( "KICAD_API_SOCKET" )] = Pgm().GetApiServer().SocketPath();
  209. env.env[wxS( "KICAD_API_TOKEN" )] = Pgm().GetApiServer().Token();
  210. env.cwd = pluginFile.GetPath();
  211. #ifdef _WIN32
  212. wxString systemRoot;
  213. wxGetEnv( wxS( "SYSTEMROOT" ), &systemRoot );
  214. env.env[wxS( "SYSTEMROOT" )] = systemRoot;
  215. if( Pgm().GetCommonSettings()->m_Api.python_interpreter == FindKicadFile( "pythonw.exe" )
  216. || wxGetEnv( wxT( "KICAD_RUN_FROM_BUILD_DIR" ), nullptr ) )
  217. {
  218. wxLogTrace( traceApi, "Configured Python is the KiCad one; erasing path overrides..." );
  219. env.env.erase( "PYTHONHOME" );
  220. env.env.erase( "PYTHONPATH" );
  221. }
  222. #endif
  223. if( pythonHome )
  224. env.env[wxS( "VIRTUAL_ENV" )] = *pythonHome;
  225. manager.Execute( pluginFile.GetFullPath(),
  226. []( int aRetVal, const wxString& aOutput, const wxString& aError )
  227. {
  228. wxLogTrace( traceApi,
  229. wxString::Format( "Manager: action exited with code %d", aRetVal ) );
  230. if( !aError.IsEmpty() )
  231. wxLogTrace( traceApi, wxString::Format( "Manager: action stderr: %s", aError ) );
  232. },
  233. &env, true );
  234. break;
  235. }
  236. case PLUGIN_RUNTIME_TYPE::EXEC:
  237. {
  238. if( !pluginFile.IsFileExecutable() )
  239. {
  240. wxLogTrace( traceApi, wxString::Format( "Manager: Exec entrypoint %s is not executable",
  241. pluginFile.GetFullPath() ) );
  242. return;
  243. }
  244. args.emplace_back( pluginPath.wc_str() );
  245. for( const wxString& arg : action->args )
  246. args.emplace_back( arg.wc_str() );
  247. args.emplace_back( nullptr );
  248. wxExecuteEnv env;
  249. wxGetEnvMap( &env.env );
  250. env.env[wxS( "KICAD_API_SOCKET" )] = Pgm().GetApiServer().SocketPath();
  251. env.env[wxS( "KICAD_API_TOKEN" )] = Pgm().GetApiServer().Token();
  252. env.cwd = pluginFile.GetPath();
  253. long p = wxExecute( const_cast<wchar_t**>( args.data() ),
  254. wxEXEC_ASYNC | wxEXEC_HIDE_CONSOLE, nullptr, &env );
  255. if( !p )
  256. {
  257. wxLogTrace( traceApi, wxString::Format( "Manager: launching action %s failed",
  258. action->identifier ) );
  259. }
  260. else
  261. {
  262. wxLogTrace( traceApi, wxString::Format( "Manager: launching action %s -> pid %ld",
  263. action->identifier, p ) );
  264. }
  265. break;
  266. }
  267. default:
  268. wxLogTrace( traceApi, wxString::Format( "Manager: unhandled runtime for action %s",
  269. action->identifier ) );
  270. return;
  271. }
  272. }
  273. std::vector<const PLUGIN_ACTION*> API_PLUGIN_MANAGER::GetActionsForScope( PLUGIN_ACTION_SCOPE aScope )
  274. {
  275. std::vector<const PLUGIN_ACTION*> actions;
  276. for( auto& [identifier, action] : m_actionsCache )
  277. {
  278. if( !m_readyPlugins.count( action->plugin.Identifier() ) )
  279. continue;
  280. if( action->scopes.count( aScope ) )
  281. actions.emplace_back( action );
  282. }
  283. return actions;
  284. }
  285. void API_PLUGIN_MANAGER::processPluginDependencies()
  286. {
  287. bool addedAnyJobs = false;
  288. for( const std::unique_ptr<API_PLUGIN>& plugin : m_plugins )
  289. {
  290. if( m_busyPlugins.contains( plugin->Identifier() ) )
  291. continue;
  292. wxLogTrace( traceApi, wxString::Format( "Manager: processing dependencies for %s",
  293. plugin->Identifier() ) );
  294. m_environmentCache[plugin->Identifier()] = wxEmptyString;
  295. if( plugin->Runtime().type != PLUGIN_RUNTIME_TYPE::PYTHON )
  296. {
  297. wxLogTrace( traceApi, wxString::Format( "Manager: %s is not a Python plugin, all set",
  298. plugin->Identifier() ) );
  299. m_readyPlugins.insert( plugin->Identifier() );
  300. continue;
  301. }
  302. std::optional<wxString> env = PYTHON_MANAGER::GetPythonEnvironment( plugin->Identifier() );
  303. if( !env )
  304. {
  305. wxLogTrace( traceApi, wxString::Format( "Manager: could not create env for %s",
  306. plugin->Identifier() ) );
  307. continue;
  308. }
  309. m_busyPlugins.insert( plugin->Identifier() );
  310. wxFileName envConfigPath( *env, wxS( "pyvenv.cfg" ) );
  311. envConfigPath.MakeAbsolute();
  312. if( envConfigPath.IsFileReadable() )
  313. {
  314. wxLogTrace( traceApi, wxString::Format( "Manager: Python env for %s exists at %s",
  315. plugin->Identifier(),
  316. envConfigPath.GetPath() ) );
  317. JOB job;
  318. job.type = JOB_TYPE::INSTALL_REQUIREMENTS;
  319. job.identifier = plugin->Identifier();
  320. job.plugin_path = plugin->BasePath();
  321. job.env_path = envConfigPath.GetPath();
  322. m_jobs.emplace_back( job );
  323. addedAnyJobs = true;
  324. continue;
  325. }
  326. wxLogTrace( traceApi, wxString::Format( "Manager: will create Python env for %s at %s",
  327. plugin->Identifier(), envConfigPath.GetPath() ) );
  328. JOB job;
  329. job.type = JOB_TYPE::CREATE_ENV;
  330. job.identifier = plugin->Identifier();
  331. job.plugin_path = plugin->BasePath();
  332. job.env_path = envConfigPath.GetPath();
  333. m_jobs.emplace_back( job );
  334. addedAnyJobs = true;
  335. }
  336. if( addedAnyJobs )
  337. {
  338. wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
  339. QueueEvent( evt );
  340. }
  341. }
  342. void API_PLUGIN_MANAGER::processNextJob( wxCommandEvent& aEvent )
  343. {
  344. if( m_jobs.empty() )
  345. {
  346. wxLogTrace( traceApi, "Manager: no more jobs to process" );
  347. return;
  348. }
  349. wxLogTrace( traceApi, wxString::Format( "Manager: begin processing; %zu jobs left in queue",
  350. m_jobs.size() ) );
  351. JOB& job = m_jobs.front();
  352. if( job.type == JOB_TYPE::CREATE_ENV )
  353. {
  354. wxLogTrace( traceApi, "Manager: Using Python interpreter at %s",
  355. Pgm().GetCommonSettings()->m_Api.python_interpreter );
  356. wxLogTrace( traceApi, wxString::Format( "Manager: creating Python env at %s",
  357. job.env_path ) );
  358. PYTHON_MANAGER manager( Pgm().GetCommonSettings()->m_Api.python_interpreter );
  359. wxExecuteEnv env;
  360. #ifdef _WIN32
  361. wxString systemRoot;
  362. wxGetEnv( wxS( "SYSTEMROOT" ), &systemRoot );
  363. env.env[wxS( "SYSTEMROOT" )] = systemRoot;
  364. if( Pgm().GetCommonSettings()->m_Api.python_interpreter == FindKicadFile( "pythonw.exe" )
  365. || wxGetEnv( wxT( "KICAD_RUN_FROM_BUILD_DIR" ), nullptr ) )
  366. {
  367. wxLogTrace( traceApi, "Configured Python is the KiCad one; erasing path overrides..." );
  368. env.env.erase( "PYTHONHOME" );
  369. env.env.erase( "PYTHONPATH" );
  370. }
  371. #endif
  372. manager.Execute(
  373. wxString::Format( wxS( "-m venv --system-site-packages \"%s\"" ),
  374. job.env_path ),
  375. [this]( int aRetVal, const wxString& aOutput, const wxString& aError )
  376. {
  377. wxLogTrace( traceApi,
  378. wxString::Format( "Manager: created venv (python returned %d)", aRetVal ) );
  379. if( !aError.IsEmpty() )
  380. wxLogTrace( traceApi, wxString::Format( "Manager: venv err: %s", aError ) );
  381. wxCommandEvent* evt =
  382. new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
  383. QueueEvent( evt );
  384. }, &env );
  385. JOB nextJob( job );
  386. nextJob.type = JOB_TYPE::SETUP_ENV;
  387. m_jobs.emplace_back( nextJob );
  388. }
  389. else if( job.type == JOB_TYPE::SETUP_ENV )
  390. {
  391. wxLogTrace( traceApi, wxString::Format( "Manager: setting up environment for %s",
  392. job.plugin_path ) );
  393. std::optional<wxString> pythonHome = PYTHON_MANAGER::GetPythonEnvironment( job.identifier );
  394. std::optional<wxString> python = PYTHON_MANAGER::GetVirtualPython( job.identifier );
  395. if( !python )
  396. {
  397. wxLogTrace( traceApi, wxString::Format( "Manager: error: python not found at %s",
  398. job.env_path ) );
  399. }
  400. else
  401. {
  402. PYTHON_MANAGER manager( *python );
  403. wxExecuteEnv env;
  404. if( pythonHome )
  405. env.env[wxS( "VIRTUAL_ENV" )] = *pythonHome;
  406. #ifdef _WIN32
  407. wxString systemRoot;
  408. wxGetEnv( wxS( "SYSTEMROOT" ), &systemRoot );
  409. env.env[wxS( "SYSTEMROOT" )] = systemRoot;
  410. if( Pgm().GetCommonSettings()->m_Api.python_interpreter
  411. == FindKicadFile( "pythonw.exe" )
  412. || wxGetEnv( wxT( "KICAD_RUN_FROM_BUILD_DIR" ), nullptr ) )
  413. {
  414. wxLogTrace( traceApi,
  415. "Configured Python is the KiCad one; erasing path overrides..." );
  416. env.env.erase( "PYTHONHOME" );
  417. env.env.erase( "PYTHONPATH" );
  418. }
  419. #endif
  420. wxString cmd = wxS( "-m pip install --upgrade pip" );
  421. wxLogTrace( traceApi, "Manager: calling python %s", cmd );
  422. manager.Execute( cmd,
  423. [this]( int aRetVal, const wxString& aOutput, const wxString& aError )
  424. {
  425. wxLogTrace( traceApi, wxString::Format( "Manager: upgrade pip returned %d",
  426. aRetVal ) );
  427. if( !aError.IsEmpty() )
  428. {
  429. wxLogTrace( traceApi,
  430. wxString::Format( "Manager: upgrade pip stderr: %s", aError ) );
  431. }
  432. wxCommandEvent* evt =
  433. new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
  434. QueueEvent( evt );
  435. }, &env );
  436. JOB nextJob( job );
  437. nextJob.type = JOB_TYPE::INSTALL_REQUIREMENTS;
  438. m_jobs.emplace_back( nextJob );
  439. }
  440. }
  441. else if( job.type == JOB_TYPE::INSTALL_REQUIREMENTS )
  442. {
  443. wxLogTrace( traceApi, wxString::Format( "Manager: installing dependencies for %s",
  444. job.plugin_path ) );
  445. std::optional<wxString> pythonHome = PYTHON_MANAGER::GetPythonEnvironment( job.identifier );
  446. std::optional<wxString> python = PYTHON_MANAGER::GetVirtualPython( job.identifier );
  447. wxFileName reqs = wxFileName( job.plugin_path, "requirements.txt" );
  448. if( !python )
  449. {
  450. wxLogTrace( traceApi, wxString::Format( "Manager: error: python not found at %s",
  451. job.env_path ) );
  452. }
  453. else if( !reqs.IsFileReadable() )
  454. {
  455. wxLogTrace( traceApi,
  456. wxString::Format( "Manager: error: requirements.txt not found at %s",
  457. job.plugin_path ) );
  458. }
  459. else
  460. {
  461. wxLogTrace( traceApi, "Manager: Python exe '%s'", *python );
  462. PYTHON_MANAGER manager( *python );
  463. wxExecuteEnv env;
  464. #ifdef _WIN32
  465. wxString systemRoot;
  466. wxGetEnv( wxS( "SYSTEMROOT" ), &systemRoot );
  467. env.env[wxS( "SYSTEMROOT" )] = systemRoot;
  468. // If we are using the KiCad-shipped Python interpreter we have to do hacks
  469. env.env.erase( "PYTHONHOME" );
  470. env.env.erase( "PYTHONPATH" );
  471. #endif
  472. if( pythonHome )
  473. env.env[wxS( "VIRTUAL_ENV" )] = *pythonHome;
  474. wxString cmd = wxString::Format(
  475. wxS( "-m pip install --no-input --isolated --only-binary :all: --require-virtualenv "
  476. "--exists-action i -r \"%s\"" ),
  477. reqs.GetFullPath() );
  478. wxLogTrace( traceApi, "Manager: calling python %s", cmd );
  479. manager.Execute( cmd,
  480. [this, job]( int aRetVal, const wxString& aOutput, const wxString& aError )
  481. {
  482. if( !aError.IsEmpty() )
  483. wxLogTrace( traceApi, wxString::Format( "Manager: pip stderr: %s", aError ) );
  484. if( aRetVal == 0 )
  485. {
  486. wxLogTrace( traceApi, wxString::Format( "Manager: marking %s as ready",
  487. job.identifier ) );
  488. m_readyPlugins.insert( job.identifier );
  489. m_busyPlugins.erase( job.identifier );
  490. wxCommandEvent* availabilityEvt =
  491. new wxCommandEvent( EDA_EVT_PLUGIN_AVAILABILITY_CHANGED, wxID_ANY );
  492. wxTheApp->QueueEvent( availabilityEvt );
  493. }
  494. wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED,
  495. wxID_ANY );
  496. QueueEvent( evt );
  497. }, &env );
  498. }
  499. wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
  500. QueueEvent( evt );
  501. }
  502. m_jobs.pop_front();
  503. wxLogTrace( traceApi, wxString::Format( "Manager: finished job; %zu left in queue",
  504. m_jobs.size() ) );
  505. }