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