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 <fmt/format.h>
22#include <wx/dir.h>
23#include <wx/log.h>
24#include <wx/utils.h>
25
27#include <api/api_server.h>
28#include <paths.h>
29#include <pgm_base.h>
30#include <python_manager.h>
33
34const wxChar* const traceApi = wxT( "KICAD_API" );
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 // TODO support system-provided plugins
88 wxDir userPluginsDir( PATHS::GetUserPluginsPath() );
89
90 PLUGIN_TRAVERSER loader(
91 [&]( const wxFileName& aFile )
92 {
93 wxLogTrace( traceApi, wxString::Format( "Manager: loading plugin from %s",
94 aFile.GetFullPath() ) );
95
96 auto plugin = std::make_unique<API_PLUGIN>( aFile );
97
98 if( plugin->IsOk() )
99 {
100 if( m_pluginsCache.count( plugin->Identifier() ) )
101 {
102 wxLogTrace( traceApi,
103 wxString::Format( "Manager: identifier %s already present!",
104 plugin->Identifier() ) );
105 return;
106 }
107 else
108 {
109 m_pluginsCache[plugin->Identifier()] = plugin.get();
110 }
111
112 for( const PLUGIN_ACTION& action : plugin->Actions() )
113 m_actionsCache[action.identifier] = &action;
114
115 m_plugins.insert( std::move( plugin ) );
116 }
117 else
118 {
119 wxLogTrace( traceApi, "Manager: loading failed" );
120 }
121 } );
122
123 if( userPluginsDir.IsOpened() )
124 {
125 wxLogTrace( traceApi, wxString::Format( "Manager: scanning user path (%s) for plugins...",
126 userPluginsDir.GetName() ) );
127 userPluginsDir.Traverse( loader );
129 }
130
131 wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_AVAILABILITY_CHANGED, wxID_ANY );
132 m_parent->QueueEvent( evt );
133}
134
135
136void API_PLUGIN_MANAGER::InvokeAction( const wxString& aIdentifier )
137{
138 if( !m_actionsCache.count( aIdentifier ) )
139 return;
140
141 const PLUGIN_ACTION* action = m_actionsCache.at( aIdentifier );
142 const API_PLUGIN& plugin = action->plugin;
143
144 if( !m_readyPlugins.count( plugin.Identifier() ) )
145 {
146 wxLogTrace( traceApi, wxString::Format( "Manager: Plugin %s is not ready",
147 plugin.Identifier() ) );
148 return;
149 }
150
151 wxFileName pluginFile( plugin.BasePath(), action->entrypoint );
152 pluginFile.Normalize( wxPATH_NORM_ABSOLUTE | wxPATH_NORM_SHORTCUT | wxPATH_NORM_DOTS
153 | wxPATH_NORM_TILDE, plugin.BasePath() );
154 wxString pluginPath = pluginFile.GetFullPath();
155
156 std::vector<const wchar_t*> args;
157 std::optional<wxString> py;
158
159 switch( plugin.Runtime().type )
160 {
161 case PLUGIN_RUNTIME_TYPE::PYTHON:
162 {
163 py = PYTHON_MANAGER::GetVirtualPython( plugin.Identifier() );
164
165 if( !py )
166 {
167 wxLogTrace( traceApi, wxString::Format( "Manager: Python interpreter for %s not found",
168 plugin.Identifier() ) );
169 return;
170 }
171
172 args.push_back( py->wc_str() );
173
174 if( !pluginFile.IsFileReadable() )
175 {
176 wxLogTrace( traceApi, wxString::Format( "Manager: Python entrypoint %s is not readable",
177 pluginFile.GetFullPath() ) );
178 return;
179 }
180
181 break;
182 }
183
184 case PLUGIN_RUNTIME_TYPE::EXEC:
185 {
186 if( !pluginFile.IsFileExecutable() )
187 {
188 wxLogTrace( traceApi, wxString::Format( "Manager: Exec entrypoint %s is not executable",
189 pluginFile.GetFullPath() ) );
190 return;
191 }
192
193 break;
194 };
195
196 default:
197 wxLogTrace( traceApi, wxString::Format( "Manager: unhandled runtime for action %s",
198 action->identifier ) );
199 return;
200 }
201
202 args.emplace_back( pluginPath.wc_str() );
203
204 for( const wxString& arg : action->args )
205 args.emplace_back( arg.wc_str() );
206
207 args.emplace_back( nullptr );
208
209 wxExecuteEnv env;
210 wxGetEnvMap( &env.env );
211 env.env[ wxS( "KICAD_API_SOCKET" ) ] = Pgm().GetApiServer().SocketPath();
212 env.env[ wxS( "KICAD_API_TOKEN" ) ] = Pgm().GetApiServer().Token();
213 env.cwd = pluginFile.GetPath();
214
215 long p = wxExecute( const_cast<wchar_t**>( args.data() ), wxEXEC_ASYNC, nullptr, &env );
216
217 if( !p )
218 {
219 wxLogTrace( traceApi, wxString::Format( "Manager: launching action %s failed",
220 action->identifier ) );
221 }
222 else
223 {
224 wxLogTrace( traceApi, wxString::Format( "Manager: launching action %s -> pid %ld",
225 action->identifier, p ) );
226 }
227}
228
229
230std::vector<const PLUGIN_ACTION*> API_PLUGIN_MANAGER::GetActionsForScope( PLUGIN_ACTION_SCOPE aScope )
231{
232 std::vector<const PLUGIN_ACTION*> actions;
233
234 for( auto& [identifier, action] : m_actionsCache )
235 {
236 if( !m_readyPlugins.count( action->plugin.Identifier() ) )
237 continue;
238
239 if( action->scopes.count( aScope ) )
240 actions.emplace_back( action );
241 }
242
243 return actions;
244}
245
246
248{
249 for( const std::unique_ptr<API_PLUGIN>& plugin : m_plugins )
250 {
251 m_environmentCache[plugin->Identifier()] = wxEmptyString;
252
253 if( plugin->Runtime().type != PLUGIN_RUNTIME_TYPE::PYTHON )
254 {
255 m_readyPlugins.insert( plugin->Identifier() );
256 continue;
257 }
258
259 std::optional<wxString> env = PYTHON_MANAGER::GetPythonEnvironment( plugin->Identifier() );
260
261 if( !env )
262 {
263 wxLogTrace( traceApi, wxString::Format( "Manager: could not create env for %s",
264 plugin->Identifier() ) );
265 continue;
266 }
267
268 wxFileName envConfigPath( *env, wxS( "pyvenv.cfg" ) );
269
270 if( envConfigPath.IsFileReadable() )
271 {
272 wxLogTrace( traceApi, wxString::Format( "Manager: Python env for %s exists at %s",
273 plugin->Identifier(), *env ) );
274 JOB job;
276 job.identifier = plugin->Identifier();
277 job.plugin_path = plugin->BasePath();
278 job.env_path = *env;
279 m_jobs.emplace_back( job );
280 continue;
281 }
282
283 wxLogTrace( traceApi, wxString::Format( "Manager: will create Python env for %s at %s",
284 plugin->Identifier(), *env ) );
285 JOB job;
287 job.identifier = plugin->Identifier();
288 job.plugin_path = plugin->BasePath();
289 job.env_path = *env;
290 m_jobs.emplace_back( job );
291 }
292
293 wxCommandEvent evt;
294 processNextJob( evt );
295}
296
297
298void API_PLUGIN_MANAGER::processNextJob( wxCommandEvent& aEvent )
299{
300 if( m_jobs.empty() )
301 {
302 wxLogTrace( traceApi, "Manager: cleared job queue" );
303 return;
304 }
305
306 wxLogTrace( traceApi, wxString::Format( "Manager: begin processing; %zu jobs left in queue",
307 m_jobs.size() ) );
308
309 JOB& job = m_jobs.front();
310
311 if( job.type == JOB_TYPE::CREATE_ENV )
312 {
313 wxLogTrace( traceApi, "Manager: Python exe '%s'",
314 Pgm().GetCommonSettings()->m_Api.python_interpreter );
315 wxLogTrace( traceApi, wxString::Format( "Manager: creating Python env at %s",
316 job.env_path ) );
317 PYTHON_MANAGER manager( Pgm().GetCommonSettings()->m_Api.python_interpreter );
318
319 manager.Execute(
320 wxString::Format( wxS( "-m venv %s" ), job.env_path ),
321 [this]( int aRetVal, const wxString& aOutput, const wxString& aError )
322 {
323 wxLogTrace( traceApi,
324 wxString::Format( "Manager: venv (%d): %s", aRetVal, aOutput ) );
325
326 if( !aError.IsEmpty() )
327 wxLogTrace( traceApi, wxString::Format( "Manager: venv err: %s", aError ) );
328
329 wxCommandEvent* evt =
330 new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
331 QueueEvent( evt );
332 } );
333 }
334 else if( job.type == JOB_TYPE::INSTALL_REQUIREMENTS )
335 {
336 wxLogTrace( traceApi, wxString::Format( "Manager: installing dependencies for %s",
337 job.plugin_path ) );
338
339 std::optional<wxString> pythonHome = PYTHON_MANAGER::GetPythonEnvironment( job.identifier );
340 std::optional<wxString> python = PYTHON_MANAGER::GetVirtualPython( job.identifier );
341 wxFileName reqs = wxFileName( job.plugin_path, "requirements.txt" );
342
343 if( !python )
344 {
345 wxLogTrace( traceApi, wxString::Format( "Manager: error: python not found at %s",
346 job.env_path ) );
347 }
348 else if( !reqs.IsFileReadable() )
349 {
350 wxLogTrace( traceApi,
351 wxString::Format( "Manager: error: requirements.txt not found at %s",
352 job.plugin_path ) );
353 }
354 else
355 {
356 wxLogTrace( traceApi, "Manager: Python exe '%s'", *python );
357
358 PYTHON_MANAGER manager( *python );
359 wxExecuteEnv env;
360
361 if( pythonHome )
362 env.env[wxS( "VIRTUAL_ENV" )] = *pythonHome;
363
364 wxString cmd = wxS( "-m ensurepip" );
365 wxLogTrace( traceApi, "Manager: calling python `%s`", cmd );
366
367 manager.Execute( cmd,
368 [=]( int aRetVal, const wxString& aOutput, const wxString& aError )
369 {
370 wxLogTrace( traceApi, wxString::Format( "Manager: ensurepip (%d): %s",
371 aRetVal, aOutput ) );
372
373 if( !aError.IsEmpty() )
374 {
375 wxLogTrace( traceApi,
376 wxString::Format( "Manager: ensurepip err: %s", aError ) );
377 }
378 }, &env );
379
380 cmd = wxString::Format(
381 wxS( "-m pip install --no-input --isolated --require-virtualenv "
382 "--exists-action i -r '%s'" ),
383 reqs.GetFullPath() );
384
385 wxLogTrace( traceApi, "Manager: calling python `%s`", cmd );
386
387 manager.Execute( cmd,
388 [this, job]( int aRetVal, const wxString& aOutput, const wxString& aError )
389 {
390 wxLogTrace( traceApi, wxString::Format( "Manager: pip (%d): %s",
391 aRetVal, aOutput ) );
392
393 if( !aError.IsEmpty() )
394 wxLogTrace( traceApi, wxString::Format( "Manager: pip err: %s", aError ) );
395
396 if( aRetVal == 0 )
397 {
398 wxLogTrace( traceApi, wxString::Format( "Manager: marking %s as ready",
399 job.identifier ) );
400 m_readyPlugins.insert( job.identifier );
401 wxCommandEvent* availabilityEvt =
402 new wxCommandEvent( EDA_EVT_PLUGIN_AVAILABILITY_CHANGED, wxID_ANY );
403 wxTheApp->QueueEvent( availabilityEvt );
404 }
405
406 wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED,
407 wxID_ANY );
408
409 QueueEvent( evt );
410 }, &env );
411 }
412
413 wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
414 QueueEvent( evt );
415 }
416
417 m_jobs.pop_front();
418 wxLogTrace( traceApi, wxString::Format( "Manager: done processing; %zu jobs left in queue",
419 m_jobs.size() ) );
420}
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.
std::map< wxString, const API_PLUGIN * > m_pluginsCache
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)
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:192
const wxString & Identifier() const
Definition: api_plugin.cpp:174
wxString BasePath() const
Definition: api_plugin.cpp:204
static wxString GetUserPluginsPath()
Gets the user path for plugins.
Definition: paths.cpp:54
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
const wxChar *const traceApi
Flag to enable debug output related to the API plugin system.
STL namespace.
PGM_BASE & Pgm()
The global Program "get" accessor.
Definition: pgm_base.cpp:1059
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