KiCad PCB EDA Suite
Loading...
Searching...
No Matches
api_plugin_manager.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright (C) 2024 Jon Evans <[email protected]>
5 * Copyright (C) 2024 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
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
28#include <api/api_server.h>
29#include <api/api_utils.h>
30#include <paths.h>
31#include <pgm_base.h>
32#include <python_manager.h>
35
36
37wxDEFINE_EVENT( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxCommandEvent );
39
40
41API_PLUGIN_MANAGER::API_PLUGIN_MANAGER( wxEvtHandler* aEvtHandler ) :
42 wxEvtHandler(),
43 m_parent( aEvtHandler )
44{
45 Bind( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, &API_PLUGIN_MANAGER::processNextJob, this );
46}
47
48
49class PLUGIN_TRAVERSER : public wxDirTraverser
50{
51private:
52 std::function<void( const wxFileName& )> m_action;
53
54public:
55 explicit PLUGIN_TRAVERSER( std::function<void( const wxFileName& )> aAction )
56 : m_action( std::move( aAction ) )
57 {
58 }
59
60 wxDirTraverseResult OnFile( const wxString& aFilePath ) override
61 {
62 wxFileName file( aFilePath );
63
64 if( file.GetFullName() == wxS( "plugin.json" ) )
65 m_action( file );
66
67 return wxDIR_CONTINUE;
68 }
69
70 wxDirTraverseResult OnDir( const wxString& dirPath ) override
71 {
72 return wxDIR_CONTINUE;
73 }
74};
75
76
78{
79 m_plugins.clear();
80 m_pluginsCache.clear();
81 m_actionsCache.clear();
82 m_environmentCache.clear();
83 m_buttonBindings.clear();
84 m_menuBindings.clear();
85 m_readyPlugins.clear();
86
87 PLUGIN_TRAVERSER loader(
88 [&]( const wxFileName& aFile )
89 {
90 wxLogTrace( traceApi, wxString::Format( "Manager: loading plugin from %s",
91 aFile.GetFullPath() ) );
92
93 auto plugin = std::make_unique<API_PLUGIN>( aFile );
94
95 if( plugin->IsOk() )
96 {
97 if( m_pluginsCache.count( plugin->Identifier() ) )
98 {
99 wxLogTrace( traceApi,
100 wxString::Format( "Manager: identifier %s already present!",
101 plugin->Identifier() ) );
102 return;
103 }
104 else
105 {
106 m_pluginsCache[plugin->Identifier()] = plugin.get();
107 }
108
109 for( const PLUGIN_ACTION& action : plugin->Actions() )
110 m_actionsCache[action.identifier] = &action;
111
112 m_plugins.insert( std::move( plugin ) );
113 }
114 else
115 {
116 wxLogTrace( traceApi, "Manager: loading failed" );
117 }
118 } );
119
120 wxDir systemPluginsDir( PATHS::GetStockPluginsPath() );
121
122 if( systemPluginsDir.IsOpened() )
123 {
124 wxLogTrace( traceApi, wxString::Format( "Manager: scanning system path (%s) for plugins...",
125 systemPluginsDir.GetName() ) );
126 systemPluginsDir.Traverse( loader );
127 }
128
129 wxString thirdPartyPath;
130 const ENV_VAR_MAP& env = Pgm().GetLocalEnvVariables();
131
132 if( std::optional<wxString> v = ENV_VAR::GetVersionedEnvVarValue( env, wxT( "3RD_PARTY" ) ) )
133 thirdPartyPath = *v;
134 else
135 thirdPartyPath = PATHS::GetDefault3rdPartyPath();
136
137 wxDir thirdParty( thirdPartyPath );
138
139 if( thirdParty.IsOpened() )
140 {
141 wxLogTrace( traceApi, wxString::Format( "Manager: scanning PCM path (%s) for plugins...",
142 thirdParty.GetName() ) );
143 thirdParty.Traverse( loader );
144 }
145
146 wxDir userPluginsDir( PATHS::GetUserPluginsPath() );
147
148 if( userPluginsDir.IsOpened() )
149 {
150 wxLogTrace( traceApi, wxString::Format( "Manager: scanning user path (%s) for plugins...",
151 userPluginsDir.GetName() ) );
152 userPluginsDir.Traverse( loader );
153 }
154
156
157 wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_AVAILABILITY_CHANGED, wxID_ANY );
158 m_parent->QueueEvent( evt );
159}
160
161
162void API_PLUGIN_MANAGER::RecreatePluginEnvironment( const wxString& aIdentifier )
163{
164 if( !m_pluginsCache.contains( aIdentifier ) )
165 return;
166
167 const API_PLUGIN* plugin = m_pluginsCache.at( aIdentifier );
168 wxCHECK( plugin, /* void */ );
169
170 std::optional<wxString> env = PYTHON_MANAGER::GetPythonEnvironment( plugin->Identifier() );
171 wxCHECK( env.has_value(), /* void */ );
172
173 wxFileName envConfigPath( *env, wxS( "pyvenv.cfg" ) );
174 envConfigPath.MakeAbsolute();
175
176 if( envConfigPath.DirExists() && envConfigPath.Rmdir( wxPATH_RMDIR_RECURSIVE ) )
177 {
178 wxLogTrace( traceApi,
179 wxString::Format( "Manager: Removed existing Python environment at %s for %s",
180 envConfigPath.GetPath(), plugin->Identifier() ) );
181
182 JOB job;
184 job.identifier = plugin->Identifier();
185 job.plugin_path = plugin->BasePath();
186 job.env_path = envConfigPath.GetPath();
187 m_jobs.emplace_back( job );
188
189 wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
190 QueueEvent( evt );
191 }
192}
193
194
195std::optional<const PLUGIN_ACTION*> API_PLUGIN_MANAGER::GetAction( const wxString& aIdentifier )
196{
197 if( !m_actionsCache.contains( aIdentifier ) )
198 return std::nullopt;
199
200 return m_actionsCache.at( aIdentifier );
201}
202
203
204void API_PLUGIN_MANAGER::InvokeAction( const wxString& aIdentifier )
205{
206 if( !m_actionsCache.contains( aIdentifier ) )
207 return;
208
209 const PLUGIN_ACTION* action = m_actionsCache.at( aIdentifier );
210 const API_PLUGIN& plugin = action->plugin;
211
212 if( !m_readyPlugins.count( plugin.Identifier() ) )
213 {
214 wxLogTrace( traceApi, wxString::Format( "Manager: Plugin %s is not ready",
215 plugin.Identifier() ) );
216 return;
217 }
218
219 wxFileName pluginFile( plugin.BasePath(), action->entrypoint );
220 pluginFile.Normalize( wxPATH_NORM_ABSOLUTE | wxPATH_NORM_SHORTCUT | wxPATH_NORM_DOTS
221 | wxPATH_NORM_TILDE, plugin.BasePath() );
222 wxString pluginPath = pluginFile.GetFullPath();
223
224 std::vector<const wchar_t*> args;
225 std::optional<wxString> py;
226
227 switch( plugin.Runtime().type )
228 {
229 case PLUGIN_RUNTIME_TYPE::PYTHON:
230 {
231 py = PYTHON_MANAGER::GetVirtualPython( plugin.Identifier() );
232
233 if( !py )
234 {
235 wxLogTrace( traceApi, wxString::Format( "Manager: Python interpreter for %s not found",
236 plugin.Identifier() ) );
237 return;
238 }
239
240 args.push_back( py->wc_str() );
241
242 if( !pluginFile.IsFileReadable() )
243 {
244 wxLogTrace( traceApi, wxString::Format( "Manager: Python entrypoint %s is not readable",
245 pluginFile.GetFullPath() ) );
246 return;
247 }
248
249 break;
250 }
251
252 case PLUGIN_RUNTIME_TYPE::EXEC:
253 {
254 if( !pluginFile.IsFileExecutable() )
255 {
256 wxLogTrace( traceApi, wxString::Format( "Manager: Exec entrypoint %s is not executable",
257 pluginFile.GetFullPath() ) );
258 return;
259 }
260
261 break;
262 };
263
264 default:
265 wxLogTrace( traceApi, wxString::Format( "Manager: unhandled runtime for action %s",
266 action->identifier ) );
267 return;
268 }
269
270 args.emplace_back( pluginPath.wc_str() );
271
272 for( const wxString& arg : action->args )
273 args.emplace_back( arg.wc_str() );
274
275 args.emplace_back( nullptr );
276
277 wxExecuteEnv env;
278 wxGetEnvMap( &env.env );
279 env.env[ wxS( "KICAD_API_SOCKET" ) ] = Pgm().GetApiServer().SocketPath();
280 env.env[ wxS( "KICAD_API_TOKEN" ) ] = Pgm().GetApiServer().Token();
281 env.cwd = pluginFile.GetPath();
282
283 long p = wxExecute( const_cast<wchar_t**>( args.data() ), wxEXEC_ASYNC, nullptr, &env );
284
285 if( !p )
286 {
287 wxLogTrace( traceApi, wxString::Format( "Manager: launching action %s failed",
288 action->identifier ) );
289 }
290 else
291 {
292 wxLogTrace( traceApi, wxString::Format( "Manager: launching action %s -> pid %ld",
293 action->identifier, p ) );
294 }
295}
296
297
298std::vector<const PLUGIN_ACTION*> API_PLUGIN_MANAGER::GetActionsForScope( PLUGIN_ACTION_SCOPE aScope )
299{
300 std::vector<const PLUGIN_ACTION*> actions;
301
302 for( auto& [identifier, action] : m_actionsCache )
303 {
304 if( !m_readyPlugins.count( action->plugin.Identifier() ) )
305 continue;
306
307 if( action->scopes.count( aScope ) )
308 actions.emplace_back( action );
309 }
310
311 return actions;
312}
313
314
316{
317 bool addedAnyJobs = false;
318
319 for( const std::unique_ptr<API_PLUGIN>& plugin : m_plugins )
320 {
321 if( m_busyPlugins.contains( plugin->Identifier() ) )
322 continue;
323
324 wxLogTrace( traceApi, wxString::Format( "Manager: processing dependencies for %s",
325 plugin->Identifier() ) );
326 m_environmentCache[plugin->Identifier()] = wxEmptyString;
327
328 if( plugin->Runtime().type != PLUGIN_RUNTIME_TYPE::PYTHON )
329 {
330 wxLogTrace( traceApi, wxString::Format( "Manager: %s is not a Python plugin, all set",
331 plugin->Identifier() ) );
332 m_readyPlugins.insert( plugin->Identifier() );
333 continue;
334 }
335
336 std::optional<wxString> env = PYTHON_MANAGER::GetPythonEnvironment( plugin->Identifier() );
337
338 if( !env )
339 {
340 wxLogTrace( traceApi, wxString::Format( "Manager: could not create env for %s",
341 plugin->Identifier() ) );
342 continue;
343 }
344
345 m_busyPlugins.insert( plugin->Identifier() );
346
347 wxFileName envConfigPath( *env, wxS( "pyvenv.cfg" ) );
348 envConfigPath.MakeAbsolute();
349
350 if( envConfigPath.IsFileReadable() )
351 {
352 wxLogTrace( traceApi, wxString::Format( "Manager: Python env for %s exists at %s",
353 plugin->Identifier(),
354 envConfigPath.GetPath() ) );
355 JOB job;
357 job.identifier = plugin->Identifier();
358 job.plugin_path = plugin->BasePath();
359 job.env_path = envConfigPath.GetPath();
360 m_jobs.emplace_back( job );
361 addedAnyJobs = true;
362 continue;
363 }
364
365 wxLogTrace( traceApi, wxString::Format( "Manager: will create Python env for %s at %s",
366 plugin->Identifier(), envConfigPath.GetPath() ) );
367 JOB job;
369 job.identifier = plugin->Identifier();
370 job.plugin_path = plugin->BasePath();
371 job.env_path = envConfigPath.GetPath();
372 m_jobs.emplace_back( job );
373 addedAnyJobs = true;
374 }
375
376 if( addedAnyJobs )
377 {
378 wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
379 QueueEvent( evt );
380 }
381}
382
383
384void API_PLUGIN_MANAGER::processNextJob( wxCommandEvent& aEvent )
385{
386 if( m_jobs.empty() )
387 {
388 wxLogTrace( traceApi, "Manager: no more jobs to process" );
389 return;
390 }
391
392 wxLogTrace( traceApi, wxString::Format( "Manager: begin processing; %zu jobs left in queue",
393 m_jobs.size() ) );
394
395 JOB& job = m_jobs.front();
396
397 if( job.type == JOB_TYPE::CREATE_ENV )
398 {
399 wxLogTrace( traceApi, "Manager: Using Python interpreter at %s",
400 Pgm().GetCommonSettings()->m_Api.python_interpreter );
401 wxLogTrace( traceApi, wxString::Format( "Manager: creating Python env at %s",
402 job.env_path ) );
403 PYTHON_MANAGER manager( Pgm().GetCommonSettings()->m_Api.python_interpreter );
404
405 manager.Execute(
406 wxString::Format( wxS( "-m venv --system-site-packages '%s'" ),
407 job.env_path ),
408 [this]( int aRetVal, const wxString& aOutput, const wxString& aError )
409 {
410 wxLogTrace( traceApi,
411 wxString::Format( "Manager: venv (%d): %s", aRetVal, aOutput ) );
412
413 if( !aError.IsEmpty() )
414 wxLogTrace( traceApi, wxString::Format( "Manager: venv err: %s", aError ) );
415
416 wxCommandEvent* evt =
417 new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
418 QueueEvent( evt );
419 } );
420
421 JOB nextJob( job );
422 nextJob.type = JOB_TYPE::SETUP_ENV;
423 m_jobs.emplace_back( nextJob );
424 }
425 else if( job.type == JOB_TYPE::SETUP_ENV )
426 {
427 wxLogTrace( traceApi, wxString::Format( "Manager: setting up environment for %s",
428 job.plugin_path ) );
429
430 std::optional<wxString> pythonHome = PYTHON_MANAGER::GetPythonEnvironment( job.identifier );
431 std::optional<wxString> python = PYTHON_MANAGER::GetVirtualPython( job.identifier );
432
433 if( !python )
434 {
435 wxLogTrace( traceApi, wxString::Format( "Manager: error: python not found at %s",
436 job.env_path ) );
437 }
438 else
439 {
440 PYTHON_MANAGER manager( *python );
441 wxExecuteEnv env;
442
443 if( pythonHome )
444 env.env[wxS( "VIRTUAL_ENV" )] = *pythonHome;
445
446 wxString cmd = wxS( "-m pip install --upgrade pip" );
447 wxLogTrace( traceApi, "Manager: calling python %s", cmd );
448
449 manager.Execute( cmd,
450 [this]( int aRetVal, const wxString& aOutput, const wxString& aError )
451 {
452 wxLogTrace( traceApi, wxString::Format( "Manager: upgrade pip (%d): %s",
453 aRetVal, aOutput ) );
454
455 if( !aError.IsEmpty() )
456 {
457 wxLogTrace( traceApi,
458 wxString::Format( "Manager: upgrade pip stderr: %s", aError ) );
459 }
460
461 wxCommandEvent* evt =
462 new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
463 QueueEvent( evt );
464 }, &env );
465
466 JOB nextJob( job );
468 m_jobs.emplace_back( nextJob );
469 }
470 }
471 else if( job.type == JOB_TYPE::INSTALL_REQUIREMENTS )
472 {
473 wxLogTrace( traceApi, wxString::Format( "Manager: installing dependencies for %s",
474 job.plugin_path ) );
475
476 std::optional<wxString> pythonHome = PYTHON_MANAGER::GetPythonEnvironment( job.identifier );
477 std::optional<wxString> python = PYTHON_MANAGER::GetVirtualPython( job.identifier );
478 wxFileName reqs = wxFileName( job.plugin_path, "requirements.txt" );
479
480 if( !python )
481 {
482 wxLogTrace( traceApi, wxString::Format( "Manager: error: python not found at %s",
483 job.env_path ) );
484 }
485 else if( !reqs.IsFileReadable() )
486 {
487 wxLogTrace( traceApi,
488 wxString::Format( "Manager: error: requirements.txt not found at %s",
489 job.plugin_path ) );
490 }
491 else
492 {
493 wxLogTrace( traceApi, "Manager: Python exe '%s'", *python );
494
495 PYTHON_MANAGER manager( *python );
496 wxExecuteEnv env;
497
498 if( pythonHome )
499 env.env[wxS( "VIRTUAL_ENV" )] = *pythonHome;
500
501 wxString cmd = wxString::Format(
502 wxS( "-m pip install --no-input --isolated --prefer-binary --require-virtualenv "
503 "--exists-action i -r '%s'" ),
504 reqs.GetFullPath() );
505
506 wxLogTrace( traceApi, "Manager: calling python %s", cmd );
507
508 manager.Execute( cmd,
509 [this, job]( int aRetVal, const wxString& aOutput, const wxString& aError )
510 {
511 if( !aOutput.IsEmpty() )
512 wxLogTrace( traceApi, wxString::Format( "Manager: pip: %s", aOutput ) );
513
514 if( !aError.IsEmpty() )
515 wxLogTrace( traceApi, wxString::Format( "Manager: pip stderr: %s", aError ) );
516
517 if( aRetVal == 0 )
518 {
519 wxLogTrace( traceApi, wxString::Format( "Manager: marking %s as ready",
520 job.identifier ) );
521 m_readyPlugins.insert( job.identifier );
522 m_busyPlugins.erase( job.identifier );
523 wxCommandEvent* availabilityEvt =
524 new wxCommandEvent( EDA_EVT_PLUGIN_AVAILABILITY_CHANGED, wxID_ANY );
525 wxTheApp->QueueEvent( availabilityEvt );
526 }
527
528 wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED,
529 wxID_ANY );
530
531 QueueEvent( evt );
532 }, &env );
533 }
534
535 wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
536 QueueEvent( evt );
537 }
538
539 m_jobs.pop_front();
540 wxLogTrace( traceApi, wxString::Format( "Manager: finished job; %zu left in queue",
541 m_jobs.size() ) );
542}
PLUGIN_ACTION_SCOPE
Definition: api_plugin.h:55
wxDEFINE_EVENT(EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxCommandEvent)
const KICOMMON_API wxEventTypeTag< wxCommandEvent > EDA_EVT_PLUGIN_AVAILABILITY_CHANGED
Notifies other parts of KiCad when plugin availability changes.
std::set< std::unique_ptr< API_PLUGIN >, CompareApiPluginIdentifiers > m_plugins
std::map< wxString, wxString > m_environmentCache
Map of plugin identifier to a path for the plugin's virtual environment, if it has one.
std::deque< JOB > m_jobs
std::vector< const PLUGIN_ACTION * > GetActionsForScope(PLUGIN_ACTION_SCOPE aScope)
std::map< int, wxString > m_menuBindings
Map of menu wx item id to action identifier.
std::map< int, wxString > m_buttonBindings
Map of button wx item id to action identifier.
void RecreatePluginEnvironment(const wxString &aIdentifier)
std::map< wxString, const API_PLUGIN * > m_pluginsCache
std::optional< const PLUGIN_ACTION * > GetAction(const wxString &aIdentifier)
void processNextJob(wxCommandEvent &aEvent)
std::set< wxString > m_readyPlugins
std::map< wxString, const PLUGIN_ACTION * > m_actionsCache
void InvokeAction(const wxString &aIdentifier)
API_PLUGIN_MANAGER(wxEvtHandler *aParent)
std::set< wxString > m_busyPlugins
wxEvtHandler * m_parent
A plugin that is invoked by KiCad and runs as an external process; communicating with KiCad via the I...
Definition: api_plugin.h:105
const PLUGIN_RUNTIME & Runtime() const
Definition: api_plugin.cpp:193
const wxString & Identifier() const
Definition: api_plugin.cpp:175
wxString BasePath() const
Definition: api_plugin.cpp:205
static wxString GetUserPluginsPath()
Gets the user path for plugins.
Definition: paths.cpp:54
static wxString GetStockPluginsPath()
Gets the stock (install) plugins path.
Definition: paths.cpp:288
static wxString GetDefault3rdPartyPath()
Gets the default path for PCM packages.
Definition: paths.cpp:130
virtual ENV_VAR_MAP & GetLocalEnvVariables() const
Definition: pgm_base.cpp:924
std::function< void(const wxFileName &)> m_action
PLUGIN_TRAVERSER(std::function< void(const wxFileName &)> aAction)
wxDirTraverseResult OnDir(const wxString &dirPath) override
wxDirTraverseResult OnFile(const wxString &aFilePath) override
Functions related to environment variables, including help functions.
const wxChar *const traceApi
Flag to enable debug output related to the IPC API and its plugin system.
Definition: api_utils.cpp:26
std::map< wxString, ENV_VAR_ITEM > ENV_VAR_MAP
KICOMMON_API std::optional< wxString > GetVersionedEnvVarValue(const std::map< wxString, ENV_VAR_ITEM > &aMap, const wxString &aBaseName)
Attempts to retrieve the value of a versioned environment variable, such as KICAD8_TEMPLATE_DIR.
Definition: env_vars.cpp:83
STL namespace.
PGM_BASE & Pgm()
The global Program "get" accessor.
Definition: pgm_base.cpp:1060
see class PGM_BASE
An action performed by a plugin via the IPC API (not to be confused with ACTION_PLUGIN,...
Definition: api_plugin.h:81
const API_PLUGIN & plugin
Definition: api_plugin.h:96
wxString identifier
Definition: api_plugin.h:86
wxString entrypoint
Definition: api_plugin.h:90
std::vector< wxString > args
Definition: api_plugin.h:92
PLUGIN_RUNTIME_TYPE type
Definition: api_plugin.h:69