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 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
21#include <fstream>
22
23#include <env_vars.h>
24#include <fmt/format.h>
25#include <wx/dir.h>
26#include <wx/log.h>
27#include <wx/process.h>
28#include <wx/timer.h>
29#include <wx/utils.h>
30
32#include <api/api_server.h>
33#include <api/api_utils.h>
34#include <gestfich.h>
35#include <paths.h>
36#include <pgm_base.h>
37#include <api/python_manager.h>
38#include <reporter.h>
41
42
43wxDEFINE_EVENT( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxCommandEvent );
45
46
47class ACTION_PROCESS : public wxProcess
48{
49public:
50 ACTION_PROCESS( std::function<void( int, const wxString&, const wxString& )> aCallback ) :
51 wxProcess(),
52 m_callback( std::move( aCallback ) )
53 {}
54
55 void OnTerminate( int aPid, int aStatus ) override
56 {
57 if( m_callback )
58 {
59 wxString output, error;
60
61 if( wxInputStream* processOut = GetInputStream() )
62 {
63 while( processOut->CanRead() )
64 {
65 char buffer[4096];
66 buffer[ processOut->Read( buffer, sizeof( buffer ) - 1 ).LastRead() ] = '\0';
67 output.append( buffer, processOut->LastRead() );
68 }
69 }
70
71 if( wxInputStream* processErr = GetErrorStream() )
72 {
73 while( processErr->CanRead() )
74 {
75 char buffer[4096];
76 buffer[ processErr->Read( buffer, sizeof( buffer ) - 1 ).LastRead() ] = '\0';
77 error.append( buffer, processErr->LastRead() );
78 }
79 }
80
81 m_callback( aStatus, output, error );
82 }
83
84 wxProcess::OnTerminate( aPid, aStatus );
85 }
86
87private:
88 std::function<void( int, const wxString&, const wxString& )> m_callback;
89};
90
91
92static void reportPluginActionMessage( REPORTER* aReporter, const wxString& aActionName,
93 const wxString& aMessage )
94{
95 if( !aReporter || aMessage.IsEmpty() )
96 return;
97
98 aReporter->Report( wxString::Format( _( "Plugin action '%s': %s" ), aActionName, aMessage ),
100}
101
102
103static void reportPluginLoadMessage( REPORTER* aReporter, const wxString& aPluginName,
104 const wxString& aMessage )
105{
106 if( !aReporter || aMessage.IsEmpty() )
107 return;
108
109 aReporter->Report( wxString::Format( _( "Plugin '%s': %s" ), aPluginName, aMessage ),
111}
112
113
114static void reportPluginActionResult( REPORTER* aReporter, const wxString& aActionName,
115 int aRetVal, const wxString& aError )
116{
117 wxString trimmedError = aError;
118 trimmedError.Trim();
119 trimmedError.Trim( false );
120
121 if( aRetVal != 0 )
122 {
123 reportPluginActionMessage( aReporter, aActionName,
124 wxString::Format( _( "exited with code %d" ), aRetVal ) );
125 }
126
127 if( !trimmedError.IsEmpty() )
128 reportPluginActionMessage( aReporter, aActionName, trimmedError );
129}
130
131
132API_PLUGIN_MANAGER::API_PLUGIN_MANAGER( wxEvtHandler* aEvtHandler ) :
133 wxEvtHandler(),
134 m_parent( aEvtHandler ),
135 m_lastPid( 0 ),
136 m_raiseTimer( nullptr )
137{
138 // Read and store pcm schema
139 wxFileName schemaFile( PATHS::GetStockDataPath( true ), wxS( "api.v1.schema.json" ) );
140 schemaFile.Normalize( FN_NORMALIZE_FLAGS | wxPATH_NORM_ENV_VARS );
141 schemaFile.AppendDir( wxS( "schemas" ) );
142
143 m_schema_validator = std::make_unique<JSON_SCHEMA_VALIDATOR>( schemaFile );
144
145 Bind( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, &API_PLUGIN_MANAGER::processNextJob, this );
146}
147
148
149class PLUGIN_TRAVERSER : public wxDirTraverser
150{
151private:
152 std::function<void( const wxFileName& )> m_action;
153
154public:
155 explicit PLUGIN_TRAVERSER( std::function<void( const wxFileName& )> aAction )
156 : m_action( std::move( aAction ) )
157 {
158 }
159
160 wxDirTraverseResult OnFile( const wxString& aFilePath ) override
161 {
162 wxFileName file( aFilePath );
163
164 if( file.GetFullName() == wxS( "plugin.json" ) )
165 m_action( file );
166
167 return wxDIR_CONTINUE;
168 }
169
170 wxDirTraverseResult OnDir( const wxString& dirPath ) override
171 {
172 return wxDIR_CONTINUE;
173 }
174};
175
176
177void API_PLUGIN_MANAGER::ReloadPlugins( std::optional<wxString> aDirectoryToScan,
178 std::shared_ptr<REPORTER> aReporter )
179{
180 m_reloadReporter = std::move( aReporter );
181
182 m_plugins.clear();
183 m_pluginsCache.clear();
184 m_actionsCache.clear();
185 m_environmentCache.clear();
186 m_buttonBindings.clear();
187 m_menuBindings.clear();
188 m_readyPlugins.clear();
189
190 PLUGIN_TRAVERSER loader(
191 [&]( const wxFileName& aFile )
192 {
193 wxLogTrace( traceApi, wxString::Format( "Manager: loading plugin from %s",
194 aFile.GetFullPath() ) );
195
196 auto plugin = std::make_unique<API_PLUGIN>( aFile, *m_schema_validator );
197
198 if( plugin->IsOk() )
199 {
200 const wxString& id = plugin->Identifier();
201
202 if( m_pluginsCache.contains( id ) )
203 {
204 wxLogTrace( traceApi, wxString::Format( "Manager: identifier %s already present, reloading", id ) );
205
206 for( const PLUGIN_ACTION& action : m_pluginsCache[id]->Actions() )
207 m_actionsCache[action.identifier] = &action;
208
209 m_pluginsCache.erase( id );
210 return;
211 }
212
213 m_pluginsCache[id] = plugin.get();
214
215 for( const PLUGIN_ACTION& action : plugin->Actions() )
216 m_actionsCache[action.identifier] = &action;
217
218 m_plugins.insert( std::move( plugin ) );
219 }
220 else
221 {
222 wxLogTrace( traceApi, "Manager: loading failed" );
223
224 reportPluginLoadMessage( m_reloadReporter.get(), aFile.GetFullPath(),
225 plugin->ErrorMessage() );
226 }
227 } );
228
229 if( aDirectoryToScan )
230 {
231 wxDir customDir( *aDirectoryToScan );
232 wxLogTrace( traceApi, wxString::Format( "Manager: scanning custom path (%s) for plugins...",
233 customDir.GetName() ) );
234 customDir.Traverse( loader );
235 }
236 else
237 {
238 wxDir systemPluginsDir( PATHS::GetStockPluginsPath() );
239
240 if( systemPluginsDir.IsOpened() )
241 {
242 wxLogTrace( traceApi, wxString::Format( "Manager: scanning system path (%s) for plugins...",
243 systemPluginsDir.GetName() ) );
244 systemPluginsDir.Traverse( loader );
245 }
246
247 wxString thirdPartyPath;
248 const ENV_VAR_MAP& env = Pgm().GetLocalEnvVariables();
249
250 if( std::optional<wxString> v = ENV_VAR::GetVersionedEnvVarValue( env, wxT( "3RD_PARTY" ) ) )
251 thirdPartyPath = *v;
252 else
253 thirdPartyPath = PATHS::GetDefault3rdPartyPath();
254
255 wxDir thirdParty( thirdPartyPath );
256
257 if( thirdParty.IsOpened() )
258 {
259 wxLogTrace( traceApi, wxString::Format( "Manager: scanning PCM path (%s) for plugins...",
260 thirdParty.GetName() ) );
261 thirdParty.Traverse( loader );
262 }
263
264 wxDir userPluginsDir( PATHS::GetUserPluginsPath() );
265
266 if( userPluginsDir.IsOpened() )
267 {
268 wxLogTrace( traceApi, wxString::Format( "Manager: scanning user path (%s) for plugins...",
269 userPluginsDir.GetName() ) );
270 userPluginsDir.Traverse( loader );
271 }
272 }
273
275
276 if( !Busy() )
277 m_reloadReporter.reset();
278
279 wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_AVAILABILITY_CHANGED, wxID_ANY );
280 m_parent->QueueEvent( evt );
281}
282
283
284void API_PLUGIN_MANAGER::RecreatePluginEnvironment( const wxString& aIdentifier )
285{
286 if( !m_pluginsCache.contains( aIdentifier ) )
287 return;
288
289 const API_PLUGIN* plugin = m_pluginsCache.at( aIdentifier );
290 wxCHECK( plugin, /* void */ );
291
292 if( plugin->Runtime().type != PLUGIN_RUNTIME_TYPE::PYTHON )
293 return;
294
295 std::optional<wxString> env = PYTHON_MANAGER::GetPythonEnvironment( plugin->Identifier() );
296 wxCHECK( env.has_value(), /* void */ );
297
298 wxFileName envConfigPath( *env, wxS( "pyvenv.cfg" ) );
299 envConfigPath.MakeAbsolute();
300
301 if( envConfigPath.DirExists() && envConfigPath.Rmdir( wxPATH_RMDIR_RECURSIVE ) )
302 {
303 wxLogTrace( traceApi,
304 wxString::Format( "Manager: Removed existing Python environment at %s for %s",
305 envConfigPath.GetPath(), plugin->Identifier() ) );
306
307 JOB job;
309 job.identifier = plugin->Identifier();
310 job.plugin_path = plugin->BasePath();
311 job.env_path = envConfigPath.GetPath();
312 m_jobs.emplace_back( job );
313
314 wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
315 QueueEvent( evt );
316 }
317}
318
319
320std::optional<const PLUGIN_ACTION*> API_PLUGIN_MANAGER::GetAction( const wxString& aIdentifier )
321{
322 if( !m_actionsCache.contains( aIdentifier ) )
323 return std::nullopt;
324
325 return m_actionsCache.at( aIdentifier );
326}
327
328
329int API_PLUGIN_MANAGER::doInvokeAction( const wxString& aIdentifier, std::vector<wxString> aExtraArgs,
330 bool aSync, wxString* aStdout, wxString* aStderr,
331 std::shared_ptr<REPORTER> aReporter )
332{
333 if( !m_actionsCache.contains( aIdentifier ) )
334 {
335 reportPluginActionMessage( aReporter.get(), aIdentifier, _( "action is not registered" ) );
336 return -1;
337 }
338
339 const PLUGIN_ACTION* action = m_actionsCache.at( aIdentifier );
340 const API_PLUGIN& plugin = action->plugin;
341
342 if( !m_readyPlugins.count( plugin.Identifier() ) )
343 {
344 wxLogTrace( traceApi, wxString::Format( "Manager: Plugin %s is not ready",
345 plugin.Identifier() ) );
346 return -1;
347 }
348
349 wxFileName pluginFile( action->entrypoint );
350 pluginFile.MakeAbsolute( plugin.BasePath() );
351 pluginFile.Normalize( wxPATH_NORM_ABSOLUTE | wxPATH_NORM_SHORTCUT | wxPATH_NORM_DOTS
352 | wxPATH_NORM_TILDE, plugin.BasePath() );
353 wxString pluginPath = pluginFile.GetFullPath();
354
355 std::vector<const wchar_t*> args;
356 std::optional<wxString> py;
357
358 switch( plugin.Runtime().type )
359 {
361 {
363
364 if( !py )
365 {
366 wxLogTrace( traceApi, wxString::Format( "Manager: Python interpreter for %s not found",
367 plugin.Identifier() ) );
368 reportPluginActionMessage( aReporter.get(), action->name,
369 _( "missing plugin environment" ) );
370 return -1;
371 }
372
373 if( !pluginFile.IsFileReadable() )
374 {
375 wxLogTrace( traceApi, wxString::Format( "Manager: Python entrypoint %s is not readable",
376 pluginFile.GetFullPath() ) );
377 reportPluginActionMessage( aReporter.get(), action->name,
378 wxString::Format( _( "entrypoint '%s' could not be read" ),
379 pluginFile.GetFullPath() ) );
380 return -1;
381 }
382
383 std::optional<wxString> pythonHome =
385
386 PYTHON_MANAGER manager( *py );
387 wxExecuteEnv env;
388 wxGetEnvMap( &env.env );
389
390 if( Pgm().ApiServerOrNull() )
391 {
392 env.env[wxS( "KICAD_API_SOCKET" )] = Pgm().GetApiServer().SocketPath();
393 env.env[wxS( "KICAD_API_TOKEN" )] = Pgm().GetApiServer().Token();
394 }
395
396 env.cwd = pluginFile.GetPath();
397
398#ifdef _WIN32
399 wxString systemRoot;
400 wxGetEnv( wxS( "SYSTEMROOT" ), &systemRoot );
401 env.env[wxS( "SYSTEMROOT" )] = systemRoot;
402
403 if( Pgm().GetCommonSettings()->m_Api.python_interpreter == FindKicadFile( "pythonw.exe" )
404 || wxGetEnv( wxT( "KICAD_RUN_FROM_BUILD_DIR" ), nullptr ) )
405 {
406 wxLogTrace( traceApi, "Configured Python is the KiCad one; erasing path overrides..." );
407 env.env.erase( "PYTHONHOME" );
408 env.env.erase( "PYTHONPATH" );
409 }
410#endif
411
412 if( pythonHome )
413 env.env[wxS( "VIRTUAL_ENV" )] = *pythonHome;
414
415 std::vector<wxString> pyArgs( aExtraArgs );
416 pyArgs.insert( pyArgs.begin(), pluginFile.GetFullPath() );
417
418 if( aSync )
419 {
420 wxString stdOut;
421 wxString stdErr;
422 wxString* stdoutSink = aStdout ? aStdout : &stdOut;
423 wxString* stderrSink = aStderr ? aStderr : &stdErr;
424 int ret = manager.ExecuteSync( pyArgs, stdoutSink, stderrSink, &env );
425 reportPluginActionResult( aReporter.get(), action->name, ret, *stderrSink );
426 return ret;
427 }
428
429 [[maybe_unused]] long pid = manager.Execute( pyArgs,
430 [aReporter, action]( int aRetVal, const wxString& aOutput,
431 const wxString& aError )
432 {
433 wxLogTrace( traceApi,
434 wxString::Format( "Manager: action exited with code %d", aRetVal ) );
435
436 if( !aError.IsEmpty() )
437 wxLogTrace( traceApi, wxString::Format( "Manager: action stderr: %s", aError ) );
438
439 reportPluginActionResult( aReporter.get(), action->name, aRetVal, aError );
440 },
441 &env, true );
442
443 if( !pid )
444 {
445 reportPluginActionMessage( aReporter.get(), action->name,
446 _( "process could not be created" ) );
447 return -1;
448 }
449
450#ifdef __WXMAC__
451 if( pid )
452 {
453 if( !m_raiseTimer )
454 {
455 m_raiseTimer = new wxTimer( this );
456
457 Bind( wxEVT_TIMER,
458 [&]( wxTimerEvent& )
459 {
460 wxString script = wxString::Format(
461 wxS( "tell application \"System Events\"\n"
462 " set plist to every process whose unix id is %ld\n"
463 " repeat with proc in plist\n"
464 " set the frontmost of proc to true\n"
465 " end repeat\n"
466 "end tell" ), m_lastPid );
467
468 wxString cmd = wxString::Format( "osascript -e '%s'", script );
469 wxLogTrace( traceApi, wxString::Format( "Execute: %s", cmd ) );
470 wxExecute( cmd );
471 },
472 m_raiseTimer->GetId() );
473 }
474
475 m_lastPid = pid;
476 m_raiseTimer->StartOnce( 250 );
477 }
478#endif
479
480 break;
481 }
482
484 {
485 if( !pluginFile.IsFileExecutable() )
486 {
487 wxLogTrace( traceApi, wxString::Format( "Manager: Exec entrypoint %s is not executable",
488 pluginFile.GetFullPath() ) );
489 reportPluginActionMessage( aReporter.get(), action->name,
490 wxString::Format( _( "entrypoint '%s' is not executable" ),
491 pluginFile.GetFullPath() ) );
492 return -1;
493 }
494
495 wxExecuteEnv env;
496 wxGetEnvMap( &env.env );
497
498 if( Pgm().ApiServerOrNull() )
499 {
500 env.env[wxS( "KICAD_API_SOCKET" )] = Pgm().GetApiServer().SocketPath();
501 env.env[wxS( "KICAD_API_TOKEN" )] = Pgm().GetApiServer().Token();
502 }
503
504 env.cwd = pluginFile.GetPath();
505
506 long pidOrRetCode = 0;
507
508 if( aSync )
509 {
510 wxString cmd = pluginPath;
511
512 for( const wxString& arg : action->args )
513 cmd << " " << arg;
514
515 wxArrayString out, err;
516
517 pidOrRetCode = wxExecute( cmd, out, err, wxEXEC_BLOCK, &env );
518
519 if( aStdout )
520 {
521 for( const wxString& line : out )
522 *aStdout << line << "\n";
523 }
524
525 wxString stdErr;
526
527 for( const wxString& line : err )
528 stdErr << line << "\n";
529
530 if( aStderr )
531 *aStderr = stdErr;
532
533 reportPluginActionResult( aReporter.get(), action->name, pidOrRetCode, stdErr );
534 return pidOrRetCode;
535 }
536 else
537 {
539 [aReporter, action]( int aRetVal, const wxString& aOutput,
540 const wxString& aError )
541 {
542 wxLogTrace( traceApi,
543 wxString::Format( "Manager: action exited with code %d", aRetVal ) );
544
545 if( !aError.IsEmpty() )
546 wxLogTrace( traceApi,
547 wxString::Format( "Manager: action stderr: %s", aError ) );
548
549 reportPluginActionResult( aReporter.get(), action->name, aRetVal, aError );
550 } );
551
552 process->Redirect();
553 args.emplace_back( pluginPath.wc_str() );
554
555 for( const wxString& arg : action->args )
556 args.emplace_back( arg.wc_str() );
557
558 args.emplace_back( nullptr );
559
560 pidOrRetCode = wxExecute( const_cast<wchar_t**>( args.data() ),
561 wxEXEC_ASYNC | wxEXEC_HIDE_CONSOLE, process, &env );
562
563 if( !pidOrRetCode )
564 delete process;
565 }
566
567 if( !pidOrRetCode )
568 {
569 wxLogTrace( traceApi, wxString::Format( "Manager: launching action %s failed",
570 action->identifier ) );
571 reportPluginActionMessage( aReporter.get(), action->name, _( "could not launch plugin" ) );
572 }
573 else
574 {
575 wxLogTrace( traceApi, wxString::Format( "Manager: launching action %s -> pid %ld",
576 action->identifier, pidOrRetCode ) );
577 }
578 break;
579 }
580
581 default:
582 wxLogTrace( traceApi, wxString::Format( "Manager: unhandled runtime for action %s",
583 action->identifier ) );
584 }
585
586 return -1;
587}
588
589
590void API_PLUGIN_MANAGER::InvokeAction( const wxString& aIdentifier,
591 std::shared_ptr<REPORTER> aReporter )
592{
593 doInvokeAction( aIdentifier, {}, false, nullptr, nullptr, std::move( aReporter ) );
594}
595
596
597int API_PLUGIN_MANAGER::InvokeActionSync( const wxString& aIdentifier, std::vector<wxString> aExtraArgs,
598 wxString* aStdout, wxString* aStderr,
599 std::shared_ptr<REPORTER> aReporter )
600{
601 return doInvokeAction( aIdentifier, aExtraArgs, true, aStdout, aStderr,
602 std::move( aReporter ) );
603}
604
605
606std::vector<const PLUGIN_ACTION*> API_PLUGIN_MANAGER::GetActionsForScope( PLUGIN_ACTION_SCOPE aScope )
607{
608 std::vector<const PLUGIN_ACTION*> actions;
609
610 for( auto& [identifier, action] : m_actionsCache )
611 {
612 if( !m_readyPlugins.count( action->plugin.Identifier() ) )
613 continue;
614
615 if( action->scopes.count( aScope ) )
616 actions.emplace_back( action );
617 }
618
619 return actions;
620}
621
622
624{
625 bool addedAnyJobs = false;
626
627 for( const std::unique_ptr<API_PLUGIN>& plugin : m_plugins )
628 {
629 if( m_busyPlugins.contains( plugin->Identifier() ) )
630 continue;
631
632 wxLogTrace( traceApi, wxString::Format( "Manager: processing dependencies for %s",
633 plugin->Identifier() ) );
634 m_environmentCache[plugin->Identifier()] = wxEmptyString;
635
636 if( plugin->Runtime().type != PLUGIN_RUNTIME_TYPE::PYTHON )
637 {
638 wxLogTrace( traceApi, wxString::Format( "Manager: %s is not a Python plugin, all set",
639 plugin->Identifier() ) );
640 m_readyPlugins.insert( plugin->Identifier() );
641 continue;
642 }
643
644 std::optional<wxString> env = PYTHON_MANAGER::GetPythonEnvironment( plugin->Identifier() );
645
646 if( !env )
647 {
648 wxLogTrace( traceApi, wxString::Format( "Manager: could not create env for %s",
649 plugin->Identifier() ) );
650 continue;
651 }
652
653 m_busyPlugins.insert( plugin->Identifier() );
654
655 wxFileName envConfigPath( *env, wxS( "pyvenv.cfg" ) );
656 envConfigPath.MakeAbsolute();
657
658 if( envConfigPath.IsFileReadable() )
659 {
660 wxLogTrace( traceApi, wxString::Format( "Manager: Python env for %s exists at %s",
661 plugin->Identifier(),
662 envConfigPath.GetPath() ) );
663 JOB job;
665 job.identifier = plugin->Identifier();
666 job.plugin_path = plugin->BasePath();
667 job.env_path = envConfigPath.GetPath();
668 m_jobs.emplace_back( job );
669 addedAnyJobs = true;
670 continue;
671 }
672
673 wxLogTrace( traceApi, wxString::Format( "Manager: will create Python env for %s at %s",
674 plugin->Identifier(), envConfigPath.GetPath() ) );
675 JOB job;
677 job.identifier = plugin->Identifier();
678 job.plugin_path = plugin->BasePath();
679 job.env_path = envConfigPath.GetPath();
680 m_jobs.emplace_back( job );
681 addedAnyJobs = true;
682 }
683
684 if( addedAnyJobs )
685 {
686 wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
687 QueueEvent( evt );
688 }
689}
690
691
692void API_PLUGIN_MANAGER::processNextJob( wxCommandEvent& aEvent )
693{
694 if( m_jobs.empty() )
695 {
696 wxLogTrace( traceApi, "Manager: no more jobs to process" );
697 return;
698 }
699
700 wxLogTrace( traceApi, wxString::Format( "Manager: begin processing; %zu jobs left in queue",
701 m_jobs.size() ) );
702
703 JOB& job = m_jobs.front();
704
705 if( job.type == JOB_TYPE::CREATE_ENV )
706 {
707 wxLogTrace( traceApi, "Manager: Using Python interpreter at %s",
708 Pgm().GetCommonSettings()->m_Api.python_interpreter );
709 wxLogTrace( traceApi, wxString::Format( "Manager: creating Python env at %s",
710 job.env_path ) );
711 PYTHON_MANAGER manager( Pgm().GetCommonSettings()->m_Api.python_interpreter );
712 wxExecuteEnv env;
713
714#ifdef _WIN32
715 wxString systemRoot;
716 wxGetEnv( wxS( "SYSTEMROOT" ), &systemRoot );
717 env.env[wxS( "SYSTEMROOT" )] = systemRoot;
718
719 if( Pgm().GetCommonSettings()->m_Api.python_interpreter == FindKicadFile( "pythonw.exe" )
720 || wxGetEnv( wxT( "KICAD_RUN_FROM_BUILD_DIR" ), nullptr ) )
721 {
722 wxLogTrace( traceApi, "Configured Python is the KiCad one; erasing path overrides..." );
723 env.env.erase( "PYTHONHOME" );
724 env.env.erase( "PYTHONPATH" );
725 }
726#endif
727 std::vector<wxString> args = {
728 "-m",
729 "venv",
730 "--system-site-packages",
731 job.env_path
732 };
733
734 manager.Execute( args,
735 [this, job]( int aRetVal, const wxString& aOutput, const wxString& aError )
736 {
737 wxLogTrace( traceApi,
738 wxString::Format( "Manager: created venv (python returned %d)", aRetVal ) );
739
740 if( !aError.IsEmpty() )
741 wxLogTrace( traceApi, wxString::Format( "Manager: venv err: %s", aError ) );
742
743 if( aRetVal != 0 )
744 {
745 wxString error = aError;
746 error.Trim().Trim( false );
747
748 if( error.IsEmpty() )
749 error = wxString::Format( _( "error code %d" ), aRetVal );
750
751 error = wxString::Format( _( "could not create plugin environment: %s" ), error );
752
754 }
755
756 wxCommandEvent* evt =
757 new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
758 QueueEvent( evt );
759 }, &env );
760
761 JOB nextJob( job );
762 nextJob.type = JOB_TYPE::SETUP_ENV;
763 m_jobs.emplace_back( nextJob );
764 }
765 else if( job.type == JOB_TYPE::SETUP_ENV )
766 {
767 wxLogTrace( traceApi, wxString::Format( "Manager: setting up environment for %s",
768 job.plugin_path ) );
769
770 std::optional<wxString> pythonHome = PYTHON_MANAGER::GetPythonEnvironment( job.identifier );
771 std::optional<wxString> python = PYTHON_MANAGER::GetVirtualPython( job.identifier );
772
773 if( !python )
774 {
775 wxLogTrace( traceApi, wxString::Format( "Manager: error: python not found at %s",
776 job.env_path ) );
777
779 _( "missing plugin environment" ) );
780 }
781 else
782 {
783 PYTHON_MANAGER manager( *python );
784 wxExecuteEnv env;
785
786 if( pythonHome )
787 env.env[wxS( "VIRTUAL_ENV" )] = *pythonHome;
788
789#ifdef _WIN32
790 wxString systemRoot;
791 wxGetEnv( wxS( "SYSTEMROOT" ), &systemRoot );
792 env.env[wxS( "SYSTEMROOT" )] = systemRoot;
793
794 if( Pgm().GetCommonSettings()->m_Api.python_interpreter
795 == FindKicadFile( "pythonw.exe" )
796 || wxGetEnv( wxT( "KICAD_RUN_FROM_BUILD_DIR" ), nullptr ) )
797 {
798 wxLogTrace( traceApi,
799 "Configured Python is the KiCad one; erasing path overrides..." );
800 env.env.erase( "PYTHONHOME" );
801 env.env.erase( "PYTHONPATH" );
802 }
803#endif
804
805 std::vector<wxString> args = {
806 "-m",
807 "pip",
808 "install",
809 "--upgrade",
810 "pip"
811 };
812
813 manager.Execute( args,
814 [this, job]( int aRetVal, const wxString& aOutput, const wxString& aError )
815 {
816 wxLogTrace( traceApi, wxString::Format( "Manager: upgrade pip returned %d",
817 aRetVal ) );
818
819 if( !aError.IsEmpty() )
820 {
821 wxLogTrace( traceApi,
822 wxString::Format( "Manager: upgrade pip stderr: %s", aError ) );
823 }
824
825 if( aRetVal != 0 )
826 {
827 wxString error = aError;
828 error.Trim().Trim( false );
829
830 if( error.IsEmpty() )
831 error = wxString::Format( _( "error code %d" ), aRetVal );
832
833 error = wxString::Format( _( "could not create plugin environment: %s" ), error );
834
836 }
837
838 wxCommandEvent* evt =
839 new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
840 QueueEvent( evt );
841 }, &env );
842
843 JOB nextJob( job );
845 m_jobs.emplace_back( nextJob );
846 }
847 }
848 else if( job.type == JOB_TYPE::INSTALL_REQUIREMENTS )
849 {
850 wxLogTrace( traceApi, wxString::Format( "Manager: installing dependencies for %s",
851 job.plugin_path ) );
852
853 std::optional<wxString> pythonHome = PYTHON_MANAGER::GetPythonEnvironment( job.identifier );
854 std::optional<wxString> python = PYTHON_MANAGER::GetVirtualPython( job.identifier );
855 wxFileName reqs = wxFileName( job.plugin_path, "requirements.txt" );
856
857 if( !python )
858 {
859 wxLogTrace( traceApi, wxString::Format( "Manager: error: python not found at %s",
860 job.env_path ) );
861
863 _( "missing plugin environment" ) );
864 }
865 else if( !reqs.IsFileReadable() )
866 {
867 wxLogTrace( traceApi,
868 wxString::Format( "Manager: error: requirements.txt not found at %s",
869 job.plugin_path ) );
870
872 _( "requirements.txt could not be read" ) );
873 }
874 else
875 {
876 wxLogTrace( traceApi, "Manager: Python exe '%s'", *python );
877
878 PYTHON_MANAGER manager( *python );
879 wxExecuteEnv env;
880
881#ifdef _WIN32
882 wxString systemRoot;
883 wxGetEnv( wxS( "SYSTEMROOT" ), &systemRoot );
884 env.env[wxS( "SYSTEMROOT" )] = systemRoot;
885
886 // If we are using the KiCad-shipped Python interpreter we have to do hacks
887 env.env.erase( "PYTHONHOME" );
888 env.env.erase( "PYTHONPATH" );
889#endif
890
891 if( pythonHome )
892 env.env[wxS( "VIRTUAL_ENV" )] = *pythonHome;
893
894 std::vector<wxString> args = {
895 "-m",
896 "pip",
897 "install",
898 "--no-input",
899 "--isolated",
900 "--only-binary",
901 ":all:",
902 "--require-virtualenv",
903 "--exists-action",
904 "i",
905 "-r",
906 reqs.GetFullPath()
907 };
908
909 manager.Execute( args,
910 [this, job]( int aRetVal, const wxString& aOutput, const wxString& aError )
911 {
912 if( !aError.IsEmpty() )
913 wxLogTrace( traceApi, wxString::Format( "Manager: pip stderr: %s", aError ) );
914
915 if( aRetVal != 0 )
916 {
917 wxString error = aError;
918 error.Trim().Trim( false );
919
920 if( error.IsEmpty() )
921 error = wxString::Format( _( "error code %d" ), aRetVal );
922
923 error = wxString::Format( _( "could not create plugin environment: %s" ), error );
924
926 }
927
928 if( aRetVal == 0 )
929 {
930 wxLogTrace( traceApi, wxString::Format( "Manager: marking %s as ready",
931 job.identifier ) );
932 m_readyPlugins.insert( job.identifier );
933
934 wxCommandEvent* availabilityEvt =
935 new wxCommandEvent( EDA_EVT_PLUGIN_AVAILABILITY_CHANGED, wxID_ANY );
936 wxTheApp->QueueEvent( availabilityEvt );
937 }
938
939 m_busyPlugins.erase( job.identifier );
940
941 wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED,
942 wxID_ANY );
943
944 QueueEvent( evt );
945 }, &env );
946 }
947
948 wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
949 QueueEvent( evt );
950 }
951
952 m_jobs.pop_front();
953
954 if( !Busy() )
955 m_reloadReporter.reset();
956
957 wxLogTrace( traceApi, wxString::Format( "Manager: finished job; %zu left in queue",
958 m_jobs.size() ) );
959}
960
961
963{
964 return !m_jobs.empty() || !m_busyPlugins.empty();
965}
static void reportPluginLoadMessage(REPORTER *aReporter, const wxString &aPluginName, const wxString &aMessage)
static void reportPluginActionResult(REPORTER *aReporter, const wxString &aActionName, int aRetVal, const wxString &aError)
wxDEFINE_EVENT(EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxCommandEvent)
static void reportPluginActionMessage(REPORTER *aReporter, const wxString &aActionName, const wxString &aMessage)
const KICOMMON_API wxEventTypeTag< wxCommandEvent > EDA_EVT_PLUGIN_AVAILABILITY_CHANGED
Notifies other parts of KiCad when plugin availability changes.
ACTION_PROCESS(std::function< void(int, const wxString &, const wxString &)> aCallback)
void OnTerminate(int aPid, int aStatus) override
std::function< void(int, const wxString &, const wxString &)> m_callback
std::set< std::unique_ptr< API_PLUGIN >, CompareApiPluginIdentifiers > m_plugins
int InvokeActionSync(const wxString &aIdentifier, std::vector< wxString > aExtraArgs, wxString *aStdout=nullptr, wxString *aStderr=nullptr, std::shared_ptr< REPORTER > aReporter=nullptr)
Invokes an action synchronously, capturing its output.
void ReloadPlugins(std::optional< wxString > aDirectoryToScan=std::nullopt, std::shared_ptr< REPORTER > aReporter=nullptr)
Clears the loaded plugins and actions and re-scans the filesystem to register new ones.
std::map< wxString, wxString > m_environmentCache
Map of plugin identifier to a path for the plugin's virtual environment, if it has one.
std::shared_ptr< REPORTER > m_reloadReporter
void InvokeAction(const wxString &aIdentifier, std::shared_ptr< REPORTER > aReporter=nullptr)
std::unique_ptr< JSON_SCHEMA_VALIDATOR > m_schema_validator
std::deque< JOB > m_jobs
int doInvokeAction(const wxString &aIdentifier, std::vector< wxString > aExtraArgs, bool aSync=false, wxString *aStdout=nullptr, wxString *aStderr=nullptr, std::shared_ptr< REPORTER > aReporter=nullptr)
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
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:96
const PLUGIN_RUNTIME & Runtime() const
const wxString & Identifier() const
wxString BasePath() const
static wxString GetUserPluginsPath()
Gets the user path for plugins.
Definition paths.cpp:49
static wxString GetStockPluginsPath()
Gets the stock (install) plugins path.
Definition paths.cpp:377
static wxString GetDefault3rdPartyPath()
Gets the default path for PCM packages.
Definition paths.cpp:126
static wxString GetStockDataPath(bool aRespectRunFromBuildDir=true)
Gets the stock (install) data path, which is the base path for things like scripting,...
Definition paths.cpp:233
virtual ENV_VAR_MAP & GetLocalEnvVariables() const
Definition pgm_base.cpp:787
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
static std::optional< wxString > GetPythonEnvironment(const wxString &aNamespace)
long Execute(const std::vector< wxString > &aArgs, const std::function< void(int, const wxString &, const wxString &)> &aCallback, const wxExecuteEnv *aEnv=nullptr, bool aSaveOutput=false)
Launches the Python interpreter with the given arguments.
static std::optional< wxString > GetVirtualPython(const wxString &aNamespace)
Returns a full path to the python binary in a venv, if it exists.
long ExecuteSync(const std::vector< wxString > &aArgs, wxString *aStdout=nullptr, wxString *aStderr=nullptr, const wxExecuteEnv *aEnv=nullptr)
A pure virtual class used to derive REPORTER objects from.
Definition reporter.h:75
virtual REPORTER & Report(const wxString &aText, SEVERITY aSeverity=RPT_SEVERITY_UNDEFINED)
Report a string with a given severity.
Definition reporter.h:104
#define _(s)
Functions related to environment variables, including help functions.
wxString FindKicadFile(const wxString &shortname)
Search the executable file shortname in KiCad binary path and return full file name if found or short...
Definition gestfich.cpp:62
const wxChar *const traceApi
Flag to enable debug output related to the IPC API and its plugin system.
Definition api_utils.cpp:29
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)
Attempt to retrieve the value of a versioned environment variable, such as KICAD8_TEMPLATE_DIR.
Definition env_vars.cpp:86
STL namespace.
static PGM_BASE * process
PGM_BASE & Pgm()
The global program "get" accessor.
see class PGM_BASE
PLUGIN_ACTION_SCOPE
@ RPT_SEVERITY_ERROR
An action performed by a plugin via the IPC API.
Definition api_plugin.h:72
const API_PLUGIN & plugin
Definition api_plugin.h:87
wxString name
Definition api_plugin.h:78
wxString identifier
Definition api_plugin.h:77
wxString entrypoint
Definition api_plugin.h:81
std::vector< wxString > args
Definition api_plugin.h:83
PLUGIN_RUNTIME_TYPE type
Definition api_plugin.h:62
nlohmann::json output
#define FN_NORMALIZE_FLAGS
Default flags to pass to wxFileName::Normalize().
Definition wx_filename.h:39